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:
parent
6f47002f9d
commit
9dd2489000
8 changed files with 39 additions and 67 deletions
|
@ -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='← Done' link={true} onClick={() => window.history.back()} />
|
||||
<ExitSettingsButton />
|
||||
</div>
|
||||
|
||||
{/* Main container */}
|
||||
|
|
18
apps/admin-x-settings/src/components/ExitSettingsButton.tsx
Normal file
18
apps/admin-x-settings/src/components/ExitSettingsButton.tsx
Normal 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='← Done' link={true} onClick={() => confirmIfDirty(isDirty, navigateAway)} />
|
||||
);
|
||||
};
|
||||
|
||||
export default ExitSettingsButton;
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
};
|
||||
};
|
||||
|
|
|
@ -240,7 +240,6 @@ export default class AdminXSettings extends Component {
|
|||
<AdminXApp
|
||||
ghostVersion={config.APP.version}
|
||||
officialThemes={officialThemes}
|
||||
setDirty={this.args.setDirty}
|
||||
/>
|
||||
</Suspense>
|
||||
</ErrorHandler>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1 +1 @@
|
|||
<AdminX::Settings @setDirty={{this.setDirty}} />
|
||||
<AdminX::Settings />
|
Loading…
Add table
Reference in a new issue