Adding CMD/CTRL+S to AdminX (#18466)

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

- it wasn't possible to save setting values in AdminX with CMD/CTR+S
This commit is contained in:
Peter Zimon 2023-10-04 13:36:06 +02:00 committed by GitHub
parent 8bc653802d
commit 223a1d7767
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 68 additions and 27 deletions

View File

@ -43,6 +43,7 @@ export interface ModalProps {
dirty?: boolean; dirty?: boolean;
animate?: boolean; animate?: boolean;
formSheet?: boolean; formSheet?: boolean;
enableCMDS?: boolean;
} }
export const topLevelBackdropClasses = 'bg-[rgba(98,109,121,0.2)] backdrop-blur-[3px]'; export const topLevelBackdropClasses = 'bg-[rgba(98,109,121,0.2)] backdrop-blur-[3px]';
@ -74,7 +75,8 @@ const Modal: React.FC<ModalProps> = ({
scrolling = true, scrolling = true,
dirty = false, dirty = false,
animate = true, animate = true,
formSheet = false formSheet = false,
enableCMDS = true
}) => { }) => {
const modal = useModal(); const modal = useModal();
const {setGlobalDirtyState} = useGlobalDirtyState(); const {setGlobalDirtyState} = useGlobalDirtyState();
@ -116,6 +118,21 @@ const Modal: React.FC<ModalProps> = ({
return () => clearTimeout(timeout); return () => clearTimeout(timeout);
}, []); }, []);
useEffect(() => {
const handleCMDS = (e: KeyboardEvent) => {
if ((e.metaKey || e.ctrlKey) && e.key === 's') {
e.preventDefault();
onOk!();
}
};
if (enableCMDS) {
window.addEventListener('keydown', handleCMDS);
return () => {
window.removeEventListener('keydown', handleCMDS);
};
}
});
let buttons: ButtonProps[] = []; let buttons: ButtonProps[] = [];
let footerClasses, contentClasses; let footerClasses, contentClasses;

View File

@ -39,6 +39,7 @@ export interface PreviewModalProps {
sidebarHeader?: React.ReactNode; sidebarHeader?: React.ReactNode;
sidebarPadding?: boolean; sidebarPadding?: boolean;
sidebarContentClasses?: string; sidebarContentClasses?: string;
enableCMDS?: boolean;
onCancel?: () => void; onCancel?: () => void;
onOk?: () => void; onOk?: () => void;
@ -73,6 +74,7 @@ export const PreviewModalContent: React.FC<PreviewModalProps> = ({
sidebarHeader, sidebarHeader,
sidebarPadding = true, sidebarPadding = true,
sidebarContentClasses, sidebarContentClasses,
enableCMDS = true,
onCancel, onCancel,
onOk, onOk,
@ -88,6 +90,21 @@ export const PreviewModalContent: React.FC<PreviewModalProps> = ({
setGlobalDirtyState(dirty); setGlobalDirtyState(dirty);
}, [dirty, setGlobalDirtyState]); }, [dirty, setGlobalDirtyState]);
useEffect(() => {
const handleCMDS = (e: KeyboardEvent) => {
if ((e.metaKey || e.ctrlKey) && e.key === 's') {
e.preventDefault();
onOk!();
}
};
if (enableCMDS) {
window.addEventListener('keydown', handleCMDS);
return () => {
window.removeEventListener('keydown', handleCMDS);
};
}
});
const [view, setView] = useState('desktop'); const [view, setView] = useState('desktop');
if (view === 'mobile' && deviceSelector) { if (view === 'mobile' && deviceSelector) {

View File

@ -39,6 +39,7 @@ interface SettingGroupProps {
onEditingChange?: (isEditing: boolean) => void onEditingChange?: (isEditing: boolean) => void
onSave?: () => void onSave?: () => void
onCancel?: () => void onCancel?: () => void
enableCMDS?: boolean
} }
const SettingGroup: React.FC<SettingGroupProps> = ({ const SettingGroup: React.FC<SettingGroupProps> = ({
@ -59,7 +60,8 @@ const SettingGroup: React.FC<SettingGroupProps> = ({
styles, styles,
onEditingChange, onEditingChange,
onSave, onSave,
onCancel onCancel,
enableCMDS = true
}) => { }) => {
const {checkVisible} = useSearch(); const {checkVisible} = useSearch();
const {route} = useRouting(); const {route} = useRouting();
@ -148,6 +150,21 @@ const SettingGroup: React.FC<SettingGroupProps> = ({
} }
}, [highlight]); }, [highlight]);
useEffect(() => {
const handleCMDS = (e: KeyboardEvent) => {
if ((e.metaKey || e.ctrlKey) && e.key === 's') {
e.preventDefault();
handleSave();
}
};
if (enableCMDS) {
window.addEventListener('keydown', handleCMDS);
return () => {
window.removeEventListener('keydown', handleCMDS);
};
}
});
const containerClasses = clsx( const containerClasses = clsx(
'relative flex-col gap-6 rounded', 'relative flex-col gap-6 rounded',
border && 'border p-5 md:p-7', border && 'border p-5 md:p-7',

View File

@ -442,7 +442,6 @@ const NewsletterDetailModalContent: React.FC<{newsletter: Newsletter; onlyOne: b
initialState: newsletter, initialState: newsletter,
onSave: async () => { onSave: async () => {
const {newsletters, meta} = await editNewsletter(formState); const {newsletters, meta} = await editNewsletter(formState);
if (meta?.sent_email_verification) { if (meta?.sent_email_verification) {
NiceModal.show(ConfirmationModal, { NiceModal.show(ConfirmationModal, {
title: 'Confirm newsletter email address', title: 'Confirm newsletter email address',
@ -458,8 +457,6 @@ const NewsletterDetailModalContent: React.FC<{newsletter: Newsletter; onlyOne: b
modal.remove(); modal.remove();
} }
}); });
} else {
modal.remove();
} }
}, },
onSaveError: handleError, onSaveError: handleError,
@ -491,10 +488,11 @@ const NewsletterDetailModalContent: React.FC<{newsletter: Newsletter; onlyOne: b
return <PreviewModalContent return <PreviewModalContent
afterClose={() => updateRoute('newsletters')} afterClose={() => updateRoute('newsletters')}
buttonsDisabled={saveState === 'saving'}
cancelLabel='Close' cancelLabel='Close'
deviceSelector={false} deviceSelector={false}
dirty={saveState === 'unsaved'} dirty={saveState === 'unsaved'}
okLabel='Save' okLabel={saveState === 'saved' ? 'Saved' : (saveState === 'saving' ? 'Saving...' : 'Save')}
preview={preview} preview={preview}
previewBgColor={'grey'} previewBgColor={'grey'}
previewToolbar={false} previewToolbar={false}
@ -503,9 +501,7 @@ const NewsletterDetailModalContent: React.FC<{newsletter: Newsletter; onlyOne: b
testId='newsletter-modal' testId='newsletter-modal'
title='Newsletter' title='Newsletter'
onOk={async () => { onOk={async () => {
if (await handleSave()) { if (!(await handleSave())) {
updateRoute('newsletters');
} else {
showToast({ showToast({
type: 'pageError', type: 'pageError',
message: 'Can\'t save newsletter, please double check that you\'ve filled all mandatory fields.' message: 'Can\'t save newsletter, please double check that you\'ve filled all mandatory fields.'

View File

@ -115,7 +115,6 @@ const Sidebar: React.FC<SidebarProps> = ({
const AnnouncementBarModal: React.FC = () => { const AnnouncementBarModal: React.FC = () => {
const {siteData} = useGlobalData(); const {siteData} = useGlobalData();
const modal = NiceModal.useModal();
const {localSettings, updateSetting, handleSave} = useSettingGroup(); const {localSettings, updateSetting, handleSave} = useSettingGroup();
const [announcementContent] = getSettingValues<string>(localSettings, ['announcement_content']); const [announcementContent] = getSettingValues<string>(localSettings, ['announcement_content']);
const [accentColor] = getSettingValues<string>(localSettings, ['accent_color']); const [accentColor] = getSettingValues<string>(localSettings, ['accent_color']);
@ -207,11 +206,10 @@ const AnnouncementBarModal: React.FC = () => {
return <PreviewModalContent return <PreviewModalContent
afterClose={() => { afterClose={() => {
modal.remove();
updateRoute('announcement-bar'); updateRoute('announcement-bar');
}} }}
cancelLabel='Close' cancelLabel='Close'
deviceSelector={false} deviceSelector={true}
dirty={false} dirty={false}
okLabel='Save' okLabel='Save'
preview={preview} preview={preview}
@ -223,10 +221,8 @@ const AnnouncementBarModal: React.FC = () => {
title='Announcement' title='Announcement'
titleHeadingLevel={5} titleHeadingLevel={5}
onOk={async () => { onOk={async () => {
if (await handleSave()) { if (!(await handleSave())) {
modal.remove();
updateRoute('announcement-bar'); updateRoute('announcement-bar');
} else {
showToast({ showToast({
type: 'pageError', type: 'pageError',
message: 'An error occurred while saving your changes. Please try again.' message: 'An error occurred while saving your changes. Please try again.'

View File

@ -66,17 +66,15 @@ const AnnouncementBarPreview: React.FC<AnnouncementBarSettings> = ({announcement
}; };
return ( return (
<div className='h-screen w-screen overflow-hidden'>
<IframeBuffering <IframeBuffering
addDelay={true} addDelay={true}
className="absolute left-0 top-0 h-full w-full" className="absolute h-[110%] w-[110%] origin-top-left scale-[.90909] max-[1600px]:h-[130%] max-[1600px]:w-[130%] max-[1600px]:scale-[.76923]"
generateContent={injectContentIntoIframe} generateContent={injectContentIntoIframe}
height='100%' height='100%'
parentClassName="relative h-full w-full" parentClassName="relative h-full w-full"
testId='announcement-bar-preview-iframe' testId='announcement-bar-preview-iframe'
width='100%' width='100%'
/> />
</div>
); );
}; };