Updated integration modals buttons (#20502)

DES-27

Updated buttons in integrations from [Cancel] and [Save & close] to
[Close] and [Save] to be consistent with the rest of the Settings UI.
This commit is contained in:
Peter Zimon 2024-07-01 17:29:53 +02:00 committed by GitHub
parent c285b0a0f1
commit fca8941740
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 125 additions and 61 deletions

View File

@ -27,6 +27,7 @@ export interface ModalProps {
cancelLabel?: string;
leftButtonProps?: ButtonProps;
buttonsDisabled?: boolean;
okDisabled?: boolean;
footer?: boolean | React.ReactNode;
header?: boolean;
padding?: boolean;
@ -62,6 +63,7 @@ const Modal: React.FC<ModalProps> = ({
header,
leftButtonProps,
buttonsDisabled,
okDisabled,
padding = true,
onOk,
okColor = 'black',
@ -179,7 +181,7 @@ const Modal: React.FC<ModalProps> = ({
color: okColor,
className: 'min-w-[80px]',
onClick: onOk,
disabled: buttonsDisabled,
disabled: buttonsDisabled || okDisabled,
loading: okLoading
});
}

View File

@ -13,11 +13,11 @@ const AmpModal = NiceModal.create(() => {
const {settings} = useGlobalData();
const [ampEnabled] = getSettingValues<boolean>(settings, ['amp']);
const [ampId] = getSettingValues<string>(settings, ['amp_gtag_id']);
const modal = NiceModal.useModal();
const [enabled, setEnabled] = useState(false);
const [trackingId, setTrackingId] = useState<string | null>('');
const {mutateAsync: editSettings} = useEditSettings();
const handleError = useHandleError();
const [okLabel, setOkLabel] = useState('Save');
const [enabled, setEnabled] = useState<boolean>(!!ampEnabled);
useEffect(() => {
setEnabled(ampEnabled || false);
@ -30,26 +30,36 @@ const AmpModal = NiceModal.create(() => {
{key: 'amp_gtag_id', value: trackingId}
];
try {
await editSettings(updates);
setOkLabel('Saving...');
await Promise.all([
editSettings(updates),
new Promise((resolve) => {
setTimeout(resolve, 1000);
})
]);
setOkLabel('Saved');
} catch (e) {
handleError(e);
} finally {
setTimeout(() => setOkLabel('Save'), 1000);
}
};
const isDirty = !(enabled === ampEnabled) || !(trackingId === ampId);
return (
<Modal
afterClose={() => {
updateRoute('integrations');
}}
dirty={!(enabled === ampEnabled) || !(trackingId === ampId)}
okColor='black'
okLabel='Save & close'
cancelLabel='Close'
dirty={isDirty}
okColor={okLabel === 'Saved' ? 'green' : 'black'}
okLabel={okLabel}
testId='amp-modal'
title=''
onOk={async () => {
await handleSave();
modal.remove();
updateRoute('integrations');
}}
>
<IntegrationHeader

View File

@ -28,7 +28,6 @@ const CustomIntegrationModalContent: React.FC<{integration: Integration}> = ({in
await editIntegration(formState);
},
onSavedStateReset: () => {
modal.remove();
updateRoute('integrations');
},
onSaveError: handleError,
@ -82,9 +81,10 @@ const CustomIntegrationModalContent: React.FC<{integration: Integration}> = ({in
updateRoute('integrations');
}}
buttonsDisabled={okProps.disabled}
cancelLabel='Close'
dirty={saveState === 'unsaved'}
okColor={okProps.color}
okLabel={okProps.label || 'Save & close'}
okLabel={okProps.label || 'Save'}
size='md'
testId='custom-integration-modal'
title={formState.name || 'Custom integration'}

View File

@ -10,18 +10,19 @@ import {useRouting} from '@tryghost/admin-x-framework/routing';
const FirstpromoterModal = NiceModal.create(() => {
const {updateRoute} = useRouting();
const modal = NiceModal.useModal();
const {settings} = useGlobalData();
const {mutateAsync: editSettings} = useEditSettings();
const handleError = useHandleError();
const [accountId, setAccountId] = useState<string | null>('');
const [enabled, setEnabled] = useState(false);
const [firstPromoterEnabled] = getSettingValues<boolean>(settings, ['firstpromoter']);
const [firstPromoterId] = getSettingValues<string>(settings, ['firstpromoter_id']);
const [okLabel, setOkLabel] = useState('Save');
const [enabled, setEnabled] = useState<boolean>(!!firstPromoterEnabled);
useEffect(() => {
setEnabled(firstPromoterEnabled || false);
setAccountId(firstPromoterId || null);
@ -38,8 +39,20 @@ const FirstpromoterModal = NiceModal.create(() => {
value: accountId
}
];
await editSettings(updates);
try {
setOkLabel('Saving...');
await Promise.all([
editSettings(updates),
new Promise((resolve) => {
setTimeout(resolve, 1000);
})
]);
setOkLabel('Saved');
} catch (e) {
handleError(e);
} finally {
setTimeout(() => setOkLabel('Save'), 1000);
}
};
return (
@ -47,16 +60,15 @@ const FirstpromoterModal = NiceModal.create(() => {
afterClose={() => {
updateRoute('integrations');
}}
cancelLabel='Close'
dirty={enabled !== firstPromoterEnabled || accountId !== firstPromoterId}
okColor='black'
okLabel='Save & close'
okColor={okLabel === 'Saved' ? 'green' : 'black'}
okLabel={okLabel}
testId='firstpromoter-modal'
title=''
onOk={async () => {
try {
await handleSave();
updateRoute('integrations');
modal.remove();
} catch (e) {
handleError(e);
}

View File

@ -12,8 +12,6 @@ import {useUploadFile} from '@tryghost/admin-x-framework/api/files';
const PinturaModal = NiceModal.create(() => {
const {updateRoute} = useRouting();
const modal = NiceModal.useModal();
const [enabled, setEnabled] = useState(false);
const [uploadingState, setUploadingState] = useState({
js: false,
css: false
@ -29,6 +27,29 @@ const PinturaModal = NiceModal.create(() => {
setEnabled(pinturaEnabled || false);
}, [pinturaEnabled]);
const [okLabel, setOkLabel] = useState('Save');
const [enabled, setEnabled] = useState<boolean>(!!pinturaEnabled);
const handleToggleChange = async () => {
const updates: Setting[] = [
{key: 'pintura', value: (enabled)}
];
try {
setOkLabel('Saving...');
await Promise.all([
editSettings(updates),
new Promise((resolve) => {
setTimeout(resolve, 1000);
})
]);
setOkLabel('Saved');
} catch (error) {
handleError(error);
} finally {
setTimeout(() => setOkLabel('Save'), 1000);
}
};
const jsUploadRef = useRef<HTMLInputElement>(null);
const cssUploadRef = useRef<HTMLInputElement>(null);
const triggerUpload = (form: string) => {
@ -70,23 +91,20 @@ const PinturaModal = NiceModal.create(() => {
}
};
const isDirty = !(enabled === pinturaEnabled);
return (
<Modal
afterClose={() => {
updateRoute('integrations');
}}
cancelLabel=''
okColor='black'
okLabel='Save'
cancelLabel='Close'
dirty={isDirty}
okColor={okLabel === 'Saved' ? 'green' : 'black'}
okLabel={okLabel}
testId='pintura-modal'
title=''
onOk={async () => {
modal.remove();
updateRoute('integrations');
await editSettings([
{key: 'pintura', value: enabled}
]);
}}
onOk={handleToggleChange}
>
<IntegrationHeader
detail='Advanced image editing'

View File

@ -10,9 +10,8 @@ import {useRouting} from '@tryghost/admin-x-framework/routing';
const SlackModal = NiceModal.create(() => {
const {updateRoute} = useRouting();
const modal = NiceModal.useModal();
const {localSettings, updateSetting, handleSave, validate, errors, clearError} = useSettingGroup({
const {localSettings, updateSetting, handleSave, validate, errors, clearError, okProps} = useSettingGroup({
onValidate: () => {
const newErrors: Record<string, string> = {};
@ -21,7 +20,8 @@ const SlackModal = NiceModal.create(() => {
}
return newErrors;
}
},
savingDelay: 500
});
const [slackUrl, slackUsername] = getSettingValues<string>(localSettings, ['slack_url', 'slack_username']);
@ -38,22 +38,22 @@ const SlackModal = NiceModal.create(() => {
}
};
const isDirty = localSettings.some(setting => setting.dirty);
return (
<Modal
afterClose={() => {
updateRoute('integrations');
}}
dirty={localSettings.some(setting => setting.dirty)}
okColor='black'
okLabel='Save & close'
cancelLabel='Close'
dirty={isDirty}
okColor={okProps.color}
okLabel={okProps.label || 'Save'}
testId='slack-modal'
title=''
onOk={async () => {
toast.remove();
if (await handleSave()) {
modal.remove();
updateRoute('integrations');
}
await handleSave();
}}
>
<IntegrationHeader

View File

@ -3,42 +3,58 @@ import NiceModal from '@ebay/nice-modal-react';
import {Form, Modal, Toggle} from '@tryghost/admin-x-design-system';
import {ReactComponent as Icon} from '../../../../assets/icons/unsplash.svg';
import {Setting, getSettingValues, useEditSettings} from '@tryghost/admin-x-framework/api/settings';
import {useEffect, useState} from 'react';
import {useGlobalData} from '../../../providers/GlobalDataProvider';
import {useHandleError} from '@tryghost/admin-x-framework/hooks';
import {useRouting} from '@tryghost/admin-x-framework/routing';
const UnsplashModal = NiceModal.create(() => {
const {updateRoute} = useRouting();
const modal = NiceModal.useModal();
const {settings} = useGlobalData();
const [unsplashEnabled] = getSettingValues<boolean>(settings, ['unsplash']);
const {mutateAsync: editSettings} = useEditSettings();
const handleError = useHandleError();
const [okLabel, setOkLabel] = useState('Save');
const [enabled, setEnabled] = useState<boolean>(!!unsplashEnabled);
const handleToggleChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
useEffect(() => {
setEnabled(unsplashEnabled || false);
}, [unsplashEnabled]);
const handleToggleChange = async () => {
const updates: Setting[] = [
{key: 'unsplash', value: (e.target.checked)}
{key: 'unsplash', value: (enabled)}
];
try {
await editSettings(updates);
setOkLabel('Saving...');
await Promise.all([
editSettings(updates),
new Promise((resolve) => {
setTimeout(resolve, 1000);
})
]);
setOkLabel('Saved');
} catch (error) {
handleError(error);
} finally {
setTimeout(() => setOkLabel('Save'), 1000);
}
};
const isDirty = !(enabled === unsplashEnabled);
return (
<Modal
afterClose={() => {
updateRoute('integrations');
}}
okColor='black'
okLabel='Save & close'
cancelLabel='Close'
dirty={isDirty}
okColor={okLabel === 'Saved' ? 'green' : 'black'}
okLabel={okLabel}
testId='unsplash-modal'
title=''
onOk={() => {
modal.remove();
updateRoute('integrations');
}}
onOk={handleToggleChange}
>
<IntegrationHeader
detail='Beautiful, free photos'
@ -48,11 +64,13 @@ const UnsplashModal = NiceModal.create(() => {
<div className='mt-7'>
<Form marginBottom={false} grouped>
<Toggle
checked={unsplashEnabled}
checked={enabled}
direction='rtl'
hint={<>Enable <a className='text-green' href="https://unsplash.com" rel="noopener noreferrer" target="_blank">Unsplash</a> image integration for your posts</>}
label='Enable Unsplash'
onChange={handleToggleChange}
onChange={(e) => {
setEnabled(e.target.checked);
}}
/>
</Form>
</div>

View File

@ -51,7 +51,7 @@ test.describe('AMP integration', async () => {
const ampToggle = ampModal.getByRole('switch');
await ampToggle.click();
await ampModal.getByRole('button', {name: 'Cancel'}).click();
await ampModal.getByRole('button', {name: 'Close'}).click();
await expect(page.getByTestId('confirmation-modal')).toHaveText(/leave/i);

View File

@ -140,7 +140,7 @@ test.describe('Custom integrations', async () => {
await modal.getByLabel('Description').fill('Test description');
await modal.getByRole('button', {name: 'Cancel'}).click();
await modal.getByRole('button', {name: 'Close'}).click();
await expect(page.getByTestId('confirmation-modal')).toHaveText(/leave/i);
@ -190,7 +190,8 @@ test.describe('Custom integrations', async () => {
// Edit integration
await modal.getByLabel('Description').fill('Test description');
await modal.getByRole('button', {name: 'Save & close'}).click();
await modal.getByRole('button', {name: 'Save'}).click();
await modal.getByRole('button', {name: 'Close'}).click();
await expect(integrationsSection).toHaveText(/Test description/);

View File

@ -51,7 +51,7 @@ test.describe('First Promoter integration', async () => {
const fpToggle = fpModal.getByRole('switch');
await fpToggle.click();
await fpModal.getByRole('button', {name: 'Cancel'}).click();
await fpModal.getByRole('button', {name: 'Close'}).click();
await expect(page.getByTestId('confirmation-modal')).toHaveText(/leave/i);

View File

@ -23,14 +23,15 @@ test.describe('Slack integration', async () => {
// Failing validation
await slackModal.getByLabel('Webhook URL').fill('badurl');
await slackModal.getByRole('button', {name: 'Save & close'}).click();
await slackModal.getByRole('button', {name: 'Save'}).click();
await expect(slackModal).toContainText('The URL must be in a format like https://hooks.slack.com/services/<your personal key>');
// Successful save
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 slackModal.getByRole('button', {name: 'Save'}).click();
await slackModal.getByRole('button', {name: 'Close'}).click();
await expect(slackModal).toHaveCount(0);
@ -59,7 +60,7 @@ test.describe('Slack integration', async () => {
await slackModal.getByLabel('Webhook URL').fill('https://hooks.slack.com/services/123456789/123456789/123456789');
await slackModal.getByRole('button', {name: 'Cancel'}).click();
await slackModal.getByRole('button', {name: 'Close'}).click();
await expect(page.getByTestId('confirmation-modal')).toHaveText(/leave/i);
@ -90,7 +91,7 @@ test.describe('Slack integration', async () => {
// Doesn't send the request when validation fails
await slackModal.getByLabel('Webhook URL').fill('badurl');
await slackModal.getByRole('button', {name: 'Save & close'}).click();
await slackModal.getByRole('button', {name: 'Save'}).click();
await expect(slackModal).toContainText('The URL must be in a format like https://hooks.slack.com/services/<your personal key>');
expect(lastApiRequests.testSlack).toBeUndefined();

View File

@ -21,6 +21,8 @@ test.describe('Unsplash integration', async () => {
const unsplashToggle = unsplashModal.getByRole('switch');
await unsplashToggle.click();
await unsplashModal.getByRole('button', {name: 'Save'}).click();
expect(lastApiRequests.editSettings?.body).toEqual({
settings: [
{key: 'unsplash', value: false}