mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-11-24 14:43:08 +03:00
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:
parent
e61b62e6be
commit
8e24ca51ad
6
apps/admin-x-settings/src/api/slack.ts
Normal file
6
apps/admin-x-settings/src/api/slack.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import {createMutation} from '../utils/apiRequests';
|
||||
|
||||
export const useTestSlack = createMutation<unknown, null>({
|
||||
method: 'POST',
|
||||
path: () => '/slack/test/'
|
||||
});
|
@ -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={() => {
|
||||
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'>[← 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>
|
||||
|
@ -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
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue
Block a user