Wired up Slack integration in AdminX (#17781)

refs https://github.com/TryGhost/Product/issues/3729

This pull request improves the Slack integration settings by allowing
users to test the webhook URL and save the settings from a modal. It
also refactors the `SlackModal` component and adds a new API function
`useTestSlack` to handle the test message.
This commit is contained in:
Jono M 2023-08-22 12:15:38 +01:00 committed by GitHub
parent e61b62e6be
commit 8e24ca51ad
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 146 additions and 10 deletions

View File

@ -0,0 +1,6 @@
import {createMutation} from '../utils/apiRequests';
export const useTestSlack = createMutation<unknown, null>({
method: 'POST',
path: () => '/slack/test/'
});

View File

@ -4,23 +4,71 @@ import IntegrationHeader from './IntegrationHeader';
import Modal from '../../../../admin-x-ds/global/modal/Modal';
import NiceModal from '@ebay/nice-modal-react';
import TextField from '../../../../admin-x-ds/global/form/TextField';
import toast from 'react-hot-toast';
import useRouting from '../../../../hooks/useRouting';
import useSettingGroup from '../../../../hooks/useSettingGroup';
import validator from 'validator';
import {ReactComponent as Icon} from '../../../../assets/icons/slack.svg';
import {getSettingValues} from '../../../../api/settings';
import {showToast} from '../../../../admin-x-ds/global/Toast';
import {useTestSlack} from '../../../../api/slack';
const SlackModal = NiceModal.create(() => {
const {updateRoute} = useRouting();
const modal = NiceModal.useModal();
const {localSettings, updateSetting, handleSave, validate, errors, clearError} = useSettingGroup({
onValidate: () => {
const newErrors: Record<string, string> = {};
if (slackUrl && !validator.isURL(slackUrl, {require_protocol: true})) {
newErrors.slackUrl = 'The URL must be in a format like https://hooks.slack.com/services/<your personal key>';
}
return newErrors;
}
});
const [slackUrl, slackUsername] = getSettingValues<string>(localSettings, ['slack_url', 'slack_username']);
const {mutateAsync: testSlack} = useTestSlack();
const handleTestClick = async () => {
toast.remove();
if (await handleSave()) {
await testSlack(null);
showToast({
message: 'Check your Slack channel for the test message',
type: 'neutral'
});
} else {
showToast({
type: 'pageError',
message: 'Can\'t save Slack settings! One or more fields have errors, please doublecheck you filled all mandatory fields'
});
}
};
return (
<Modal
afterClose={() => {
updateRoute('integrations');
}}
dirty={localSettings.some(setting => setting.dirty)}
okColor='black'
okLabel='Save & close'
testId='slack-modal'
title=''
onOk={() => {
modal.remove();
onOk={async () => {
toast.remove();
if (await handleSave()) {
modal.remove();
updateRoute('integrations');
} else {
showToast({
type: 'pageError',
message: 'Can\'t save Slack settings! One or more fields have errors, please doublecheck you filled all mandatory fields'
});
}
}}
>
<IntegrationHeader
@ -31,19 +79,26 @@ const SlackModal = NiceModal.create(() => {
<div className='mt-7'>
<Form marginBottom={false} title='Slack configuration' grouped>
<TextField
hint={<>
Automatically send newly published posts to a channel in Slack or any Slack-compatible service like Discord or Mattermost. Set up a new incoming webhook here <strong className='text-red'>[&larr; link to be set]</strong>, and grab the URL.
error={Boolean(errors.slackUrl)}
hint={errors.slackUrl || <>
Automatically send newly published posts to a channel in Slack or any Slack-compatible service like Discord or Mattermost. Set up a new incoming webhook <a href='https://my.slack.com/apps/new/A0F7XDUAZ-incoming-webhooks'>here</a>, and grab the URL.
</>}
placeholder='https://hooks.slack.com/services/...'
title='Webhook URL'
value={slackUrl}
onBlur={validate}
onChange={e => updateSetting('slack_url', e.target.value)}
onKeyDown={() => clearError('slackUrl')}
/>
<div className='flex w-full items-center gap-2'>
<TextField
containerClassName='flex-grow'
hint='The username to display messages from'
title='Username'
value={slackUsername}
onChange={e => updateSetting('slack_username', e.target.value)}
/>
<Button color='outline' label='Send test notification' />
<Button color='outline' label='Send test notification' onClick={handleTestClick} />
</div>
</Form>
</div>
@ -51,4 +106,4 @@ const SlackModal = NiceModal.create(() => {
);
});
export default SlackModal;
export default SlackModal;

View File

@ -19,9 +19,12 @@ export interface SettingGroupHook {
handleCancel: () => void;
updateSetting: (key: string, value: SettingValue) => void;
handleEditingChange: (newState: boolean) => void;
validate: () => boolean;
errors: Record<string, string>;
clearError: (key: string) => void;
}
const useSettingGroup = (): SettingGroupHook => {
const useSettingGroup = ({onValidate}: {onValidate?: () => Record<string, string>} = {}): SettingGroupHook => {
// create a ref to focus the input field
const focusRef = useRef<HTMLInputElement>(null);
@ -30,11 +33,12 @@ const useSettingGroup = (): SettingGroupHook => {
const [isEditing, setEditing] = useState(false);
const {formState: localSettings, saveState, handleSave, updateForm, reset} = useForm<LocalSetting[]>({
const {formState: localSettings, saveState, handleSave, updateForm, reset, validate, errors, clearError} = useForm<LocalSetting[]>({
initialState: settings || [],
onSave: async () => {
await editSettings?.(changedSettings());
}
},
onValidate
});
const {setGlobalDirtyState} = useGlobalDirtyState();
@ -98,7 +102,10 @@ const useSettingGroup = (): SettingGroupHook => {
},
handleCancel,
updateSetting,
handleEditingChange
handleEditingChange,
validate,
errors,
clearError
};
};

View File

@ -0,0 +1,68 @@
import {expect, test} from '@playwright/test';
import {globalDataRequests, mockApi, updatedSettingsResponse} from '../../../utils/e2e';
test.describe('Slack integration', async () => {
test('Supports updating Slack settings', async ({page}) => {
const {lastApiRequests} = await mockApi({page, requests: {
...globalDataRequests,
editSettings: {method: 'PUT', path: '/settings/', response: updatedSettingsResponse([
{key: 'slack_url', value: 'https://hooks.slack.com/services/123456789/123456789/123456789'},
{key: 'slack_username', value: 'My site'}
])}
}});
await page.goto('/');
const section = page.getByTestId('integrations');
const slackElement = section.getByText('Slack').last();
await slackElement.hover();
await section.getByRole('button', {name: 'Configure'}).click();
const slackModal = page.getByTestId('slack-modal');
await slackModal.getByLabel('Webhook URL').fill('https://hooks.slack.com/services/123456789/123456789/123456789');
await slackModal.getByLabel('Username').fill('My site');
await slackModal.getByRole('button', {name: 'Save & close'}).click();
await expect(slackModal).toHaveCount(0);
expect(lastApiRequests.editSettings?.body).toEqual({
settings: [
{key: 'slack_url', value: 'https://hooks.slack.com/services/123456789/123456789/123456789'},
{key: 'slack_username', value: 'My site'}
]
});
});
test('Supports testing Slack messages', async ({page}) => {
const {lastApiRequests} = await mockApi({page, requests: {
...globalDataRequests,
editSettings: {method: 'PUT', path: '/settings/', response: updatedSettingsResponse([
{key: 'slack_url', value: 'https://hooks.slack.com/services/123456789/123456789/123456789'},
{key: 'slack_username', value: 'My site'}
])},
testSlack: {method: 'POST', path: '/slack/test/', responseStatus: 204, response: ''}
}});
await page.goto('/');
const section = page.getByTestId('integrations');
const slackElement = section.getByText('Slack').last();
await slackElement.hover();
await section.getByRole('button', {name: 'Configure'}).click();
const slackModal = page.getByTestId('slack-modal');
await slackModal.getByLabel('Webhook URL').fill('https://hooks.slack.com/services/123456789/123456789/123456789');
await slackModal.getByLabel('Username').fill('My site');
await slackModal.getByRole('button', {name: 'Send test notification'}).click();
await expect(page.getByTestId('toast')).toHaveText(/Check your Slack channel for the test message/);
expect(lastApiRequests.editSettings?.body).toEqual({
settings: [
{key: 'slack_url', value: 'https://hooks.slack.com/services/123456789/123456789/123456789'},
{key: 'slack_username', value: 'My site'}
]
});
expect(lastApiRequests.testSlack).toBeTruthy();
});
});