Fixed provider ordering to prevent bugs loading modals on refresh (#19089)

refs https://github.com/TryGhost/Product/issues/4174
This commit is contained in:
Jono M 2023-11-22 07:51:10 +00:00 committed by GitHub
parent 8038c5854e
commit 32dacd9ff7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 49 additions and 23 deletions

View File

@ -1,6 +1,7 @@
import MainContent from './MainContent';
import {DesignSystemApp, DesignSystemAppProps} from '@tryghost/admin-x-design-system';
import {FrameworkProvider, TopLevelFrameworkProps} from '@tryghost/admin-x-framework';
import {RoutingProvider} from '@tryghost/admin-x-framework/routing';
interface AppProps {
framework: TopLevelFrameworkProps;
@ -16,10 +17,12 @@ const modals = {
const App: React.FC<AppProps> = ({framework, designSystem}) => {
return (
<FrameworkProvider basePath='demo-x' modals={modals} {...framework}>
<DesignSystemApp className='admin-x-demo' {...designSystem}>
<MainContent />
</DesignSystemApp>
<FrameworkProvider {...framework}>
<RoutingProvider basePath='demo-x' modals={modals}>
<DesignSystemApp className='admin-x-demo' {...designSystem}>
<MainContent />
</DesignSystemApp>
</RoutingProvider>
</FrameworkProvider>
);
};

View File

@ -2,13 +2,11 @@ import {ErrorBoundary as SentryErrorBoundary} from '@sentry/react';
import {QueryClientProvider} from '@tanstack/react-query';
import {ReactNode, createContext, useContext} from 'react';
import queryClient from '../utils/queryClient';
import RoutingProvider, {RoutingProviderProps} from './RoutingProvider';
import {ExternalLink} from './RoutingProvider';
export interface FrameworkProviderProps {
basePath: string;
ghostVersion: string;
externalNavigate: RoutingProviderProps['externalNavigate'];
modals?: RoutingProviderProps['modals'];
externalNavigate: (link: ExternalLink) => void;
unsplashConfig: {
Authorization: string;
'Accept-Version': string;
@ -24,13 +22,13 @@ export interface FrameworkProviderProps {
children: ReactNode;
}
// children, basePath and modals should be provided by each app, while others are passed in from Ember
export type TopLevelFrameworkProps = Omit<FrameworkProviderProps, 'children' | 'basePath' | 'modals'>;
export type TopLevelFrameworkProps = Omit<FrameworkProviderProps, 'children'>;
export type FrameworkContextType = Omit<FrameworkProviderProps, 'basePath' | 'externalNavigate' | 'modals' | 'children'>;
export type FrameworkContextType = Omit<FrameworkProviderProps, 'children'>;
const FrameworkContext = createContext<FrameworkContextType>({
ghostVersion: '',
externalNavigate: () => {},
unsplashConfig: {
Authorization: '',
'Accept-Version': '',
@ -44,14 +42,12 @@ const FrameworkContext = createContext<FrameworkContextType>({
onDelete: () => {}
});
function FrameworkProvider({externalNavigate, basePath, modals, children, ...props}: FrameworkProviderProps) {
function FrameworkProvider({children, ...props}: FrameworkProviderProps) {
return (
<SentryErrorBoundary>
<QueryClientProvider client={queryClient}>
<FrameworkContext.Provider value={props}>
<RoutingProvider basePath={basePath} externalNavigate={externalNavigate} modals={modals}>
{children}
</RoutingProvider>
{children}
</FrameworkContext.Provider>
</QueryClientProvider>
</SentryErrorBoundary>

View File

@ -1,5 +1,6 @@
import NiceModal, {NiceModalHocProps} from '@ebay/nice-modal-react';
import React, {createContext, useCallback, useContext, useEffect, useState} from 'react';
import {useFramework} from './FrameworkProvider';
export type RouteParams = Record<string, string>
@ -92,12 +93,12 @@ const matchRoute = (pathname: string, routeDefinition: string) => {
export interface RoutingProviderProps {
basePath: string;
externalNavigate: (link: ExternalLink) => void;
modals?: {paths: Record<string, string>, load: () => Promise<ModalsModule>}
children: React.ReactNode;
}
const RoutingProvider: React.FC<RoutingProviderProps> = ({basePath, externalNavigate, modals, children}) => {
const RoutingProvider: React.FC<RoutingProviderProps> = ({basePath, modals, children}) => {
const {externalNavigate} = useFramework();
const [route, setRoute] = useState<string | undefined>(undefined);
const [loadingModal, setLoadingModal] = useState(false);
const [eventTarget] = useState(new EventTarget());

View File

@ -1,3 +1,3 @@
export {useRouteChangeCallback, useRouting} from './providers/RoutingProvider';
export {default as RoutingProvider, useRouteChangeCallback, useRouting} from './providers/RoutingProvider';
export type {ExternalLink, InternalLink, ModalComponent, RoutingModalProps} from './providers/RoutingProvider';

View File

@ -3,6 +3,7 @@ import SettingsAppProvider, {OfficialTheme, UpgradeStatusType} from './component
import SettingsRouter, {loadModals, modalPaths} from './components/providers/SettingsRouter';
import {DesignSystemApp, DesignSystemAppProps} from '@tryghost/admin-x-design-system';
import {FrameworkProvider, TopLevelFrameworkProps} from '@tryghost/admin-x-framework';
import {RoutingProvider} from '@tryghost/admin-x-framework/routing';
import {ZapierTemplate} from './components/settings/advanced/integrations/ZapierModal';
interface AppProps {
@ -15,12 +16,14 @@ interface AppProps {
function App({framework, designSystem, officialThemes, zapierTemplates, upgradeStatus}: AppProps) {
return (
<FrameworkProvider basePath='settings' modals={{paths: modalPaths, load: loadModals}} {...framework}>
<FrameworkProvider {...framework}>
<SettingsAppProvider officialThemes={officialThemes} upgradeStatus={upgradeStatus} zapierTemplates={zapierTemplates}>
<DesignSystemApp className='admin-x-settings' {...designSystem}>
<SettingsRouter />
<MainContent />
</DesignSystemApp>
<RoutingProvider basePath='settings' modals={{paths: modalPaths, load: loadModals}}>
<DesignSystemApp className='admin-x-settings' {...designSystem}>
<SettingsRouter />
<MainContent />
</DesignSystemApp>
</RoutingProvider>
</SettingsAppProvider>
</FrameworkProvider>
);

View File

@ -0,0 +1,23 @@
import {expect, test} from '@playwright/test';
import {globalDataRequests, mockApi} from '../utils/acceptance';
test.describe('Routing', async () => {
test('Reopens the opened modal when refreshing the page', async ({page}) => {
await mockApi({page, requests: globalDataRequests});
await page.goto('/');
const section = page.getByTestId('portal');
await section.getByRole('button', {name: 'Customize'}).click();
await page.waitForSelector('[data-testid="portal-modal"]');
expect(page.url()).toMatch(/\/portal\/edit$/);
await page.reload();
await page.waitForSelector('[data-testid="portal-modal"]');
expect(page.url()).toMatch(/\/portal\/edit$/);
});
});