0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-02-17 23:44:39 -05:00

Fixed bugs with AdminX navigation settings (#17340)

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

- Removed Ember dirty state from AdminX to prevent extra popups
- Fixed incorrect navigation popup new item errors
This commit is contained in:
Jono M 2023-07-13 10:12:31 +09:00 committed by GitHub
parent 6f47002f9d
commit 9dd2489000
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 39 additions and 67 deletions

View file

@ -1,5 +1,5 @@
import Button from './admin-x-ds/global/Button';
import DataProvider from './components/providers/DataProvider';
import ExitSettingsButton from './components/ExitSettingsButton';
import Heading from './admin-x-ds/global/Heading';
import NiceModal from '@ebay/nice-modal-react';
import RoutingProvider from './components/providers/RoutingProvider';
@ -12,21 +12,20 @@ import {Toaster} from 'react-hot-toast';
interface AppProps {
ghostVersion: string;
officialThemes: OfficialTheme[]
setDirty?: (dirty: boolean) => void;
officialThemes: OfficialTheme[];
}
function App({ghostVersion, officialThemes, setDirty}: AppProps) {
function App({ghostVersion, officialThemes}: AppProps) {
return (
<ServicesProvider ghostVersion={ghostVersion} officialThemes={officialThemes}>
<DataProvider>
<RoutingProvider>
<GlobalDirtyStateProvider setDirty={setDirty}>
<GlobalDirtyStateProvider>
<div className="admin-x-settings">
<Toaster />
<NiceModal.Provider>
<div className='fixed left-6 top-4'>
<Button label='&larr; Done' link={true} onClick={() => window.history.back()} />
<ExitSettingsButton />
</div>
{/* Main container */}

View file

@ -0,0 +1,18 @@
import Button from '../admin-x-ds/global/Button';
import React from 'react';
import useGlobalDirtyState from '../hooks/useGlobalDirtyState';
import {confirmIfDirty} from '../utils/modals';
const ExitSettingsButton: React.FC = () => {
const {isDirty} = useGlobalDirtyState();
const navigateAway = () => {
window.location.hash = '/dashboard';
};
return (
<Button label='&larr; Done' link={true} onClick={() => confirmIfDirty(isDirty, navigateAway)} />
);
};
export default ExitSettingsButton;

View file

@ -26,11 +26,13 @@ const useNavigationEditor = ({items, setItems}: {
items: NavigationItem[];
setItems: (newItems: NavigationItem[]) => void;
}): NavigationEditor => {
const hasNewItem = (newItem: NavigationItem) => Boolean((newItem.label && !newItem.label.match(/^\s*$/)) || newItem.url !== '/');
const list = useSortableIndexedList<Omit<EditableItem, 'id'>>({
items: items.map(item => ({...item, errors: {}})),
setItems: newItems => setItems(newItems.map(({url, label}) => ({url, label}))),
blank: {label: '', url: '/', errors: {}},
canAddNewItem: newItem => Boolean((newItem.label && !newItem.label.match(/^\s*$/)) || newItem.url !== '/')
canAddNewItem: hasNewItem
});
const urlRegex = new RegExp(/^(\/|#|[a-zA-Z0-9-]+:)/);
@ -107,11 +109,13 @@ const useNavigationEditor = ({items, setItems}: {
}
});
const newItemErrors = validateItem(list.newItem);
if (hasNewItem(list.newItem)) {
const newItemErrors = validateItem(list.newItem);
if (Object.values(newItemErrors).some(message => message)) {
isValid = false;
list.setNewItem({...list.newItem, errors: newItemErrors});
if (Object.values(newItemErrors).some(message => message)) {
isValid = false;
list.setNewItem({...list.newItem, errors: newItemErrors});
}
}
return isValid;

View file

@ -1,12 +1,13 @@
import React, {useCallback, useContext, useEffect, useId, useState} from 'react';
interface GlobalDirtyState {
isDirty: boolean
setGlobalDirtyState: (id: string, dirty: boolean) => void;
}
const GlobalDirtyStateContext = React.createContext<GlobalDirtyState>({setGlobalDirtyState: () => {}});
const GlobalDirtyStateContext = React.createContext<GlobalDirtyState>({isDirty: false, setGlobalDirtyState: () => {}});
export const GlobalDirtyStateProvider = ({setDirty, children}: {setDirty?: (dirty: boolean) => void; children: React.ReactNode}) => {
export const GlobalDirtyStateProvider = ({children}: {children: React.ReactNode}) => {
// Allows each component to register itself as dirty with a unique ID, so when one is reset/saved the overall page dirty state persists
const [dirtyIds, setDirtyIds] = useState<string[]>([]);
@ -24,12 +25,8 @@ export const GlobalDirtyStateProvider = ({setDirty, children}: {setDirty?: (dirt
});
}, []);
useEffect(() => {
setDirty?.(dirtyIds.length > 0);
}, [dirtyIds, setDirty]);
return (
<GlobalDirtyStateContext.Provider value={{setGlobalDirtyState}}>
<GlobalDirtyStateContext.Provider value={{isDirty: dirtyIds.length > 0, setGlobalDirtyState}}>
{children}
</GlobalDirtyStateContext.Provider>
);
@ -37,7 +34,7 @@ export const GlobalDirtyStateProvider = ({setDirty, children}: {setDirty?: (dirt
const useGlobalDirtyState = () => {
const id = useId();
const {setGlobalDirtyState} = useContext(GlobalDirtyStateContext);
const {isDirty, setGlobalDirtyState} = useContext(GlobalDirtyStateContext);
useEffect(() => {
// Make sure the state is reset when the component unmounts
@ -50,6 +47,7 @@ const useGlobalDirtyState = () => {
);
return {
isDirty,
setGlobalDirtyState: setDirty
};
};

View file

@ -240,7 +240,6 @@ export default class AdminXSettings extends Component {
<AdminXApp
ghostVersion={config.APP.version}
officialThemes={officialThemes}
setDirty={this.args.setDirty}
/>
</Suspense>
</ErrorHandler>

View file

@ -2,21 +2,13 @@ import AboutModal from '../components/modals/settings/about';
import Controller from '@ember/controller';
import {action} from '@ember/object';
import {inject as service} from '@ember/service';
import {tracked} from '@glimmer/tracking';
export default class SettingsController extends Controller {
@service modals;
@service upgradeStatus;
@tracked dirty = false;
@action
openAbout() {
this.advancedModal = this.modals.open(AboutModal);
}
@action
setDirty(dirty) {
this.dirty = dirty;
}
}

View file

@ -1,6 +1,4 @@
import AuthenticatedRoute from 'ghost-admin/routes/authenticated';
import ConfirmUnsavedChangesModal from '../components/modals/confirm-unsaved-changes';
import {action} from '@ember/object';
import {inject as service} from '@ember/service';
export default class SettingsXRoute extends AuthenticatedRoute {
@ -31,40 +29,4 @@ export default class SettingsXRoute extends AuthenticatedRoute {
super.deactivate(...arguments);
this.ui.set('isFullScreen', false);
}
@action
async willTransition(transition) {
if (this.hasConfirmed) {
this.hasConfirmed = false;
return true;
}
transition.abort();
// wait for any existing confirm modal to be closed before allowing transition
if (this.confirmModal) {
return;
}
const shouldLeave = await this.confirmUnsavedChanges();
if (shouldLeave) {
this.hasConfirmed = true;
return transition.retry();
}
}
async confirmUnsavedChanges() {
if (this.controller.dirty) {
this.confirmModal = this.modals
.open(ConfirmUnsavedChangesModal)
.finally(() => {
this.confirmModal = null;
});
return this.confirmModal;
}
return true;
}
}

View file

@ -1 +1 @@
<AdminX::Settings @setDirty={{this.setDirty}} />
<AdminX::Settings />