Skip to content

Commit 905cf01

Browse files
Routing: Add styles page as a route (WordPress#73197)
Co-authored-by: youknowriad <youknowriad@git.wordpress.org> Co-authored-by: ntsekouras <ntsekouras@git.wordpress.org>
1 parent a82b2ad commit 905cf01

File tree

20 files changed

+421
-48
lines changed

20 files changed

+421
-48
lines changed

lib/experimental/pages/gutenberg-boot.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,5 +26,6 @@ function gutenberg_register_boot_admin_page() {
2626
function gutenberg_boot_register_default_menu_items() {
2727
register_gutenberg_boot_menu_item( 'home', __( 'Home', 'gutenberg' ), '/', '' );
2828
register_gutenberg_boot_menu_item( 'posts', __( 'Posts', 'gutenberg' ), '/types/post', '' );
29+
register_gutenberg_boot_menu_item( 'styles', __( 'Styles', 'gutenberg' ), '/styles', '' );
2930
}
3031
add_action( 'gutenberg-boot_init', 'gutenberg_boot_register_default_menu_items', 5 );

package-lock.json

Lines changed: 19 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/boot/src/components/app/router.tsx

Lines changed: 40 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,9 @@ import {
1818
* Internal dependencies
1919
*/
2020
import Root from '../root';
21-
import type { Route, RouteLoaderContext } from '../../store/types';
21+
import type { CanvasData, Route, RouteLoaderContext } from '../../store/types';
2222
import { unlock } from '../../lock-unlock';
23+
import Canvas from '../canvas';
2324

2425
const {
2526
createRouter,
@@ -28,6 +29,7 @@ const {
2829
RouterProvider,
2930
createBrowserHistory,
3031
parseHref,
32+
useMatches,
3133
} = unlock( routePrivateApis );
3234

3335
// Not found component
@@ -44,10 +46,20 @@ function NotFoundComponent() {
4446
function RouteComponent( {
4547
stage: Stage,
4648
inspector: Inspector,
49+
canvas: CustomCanvas,
4750
}: {
4851
stage?: ComponentType;
4952
inspector?: ComponentType;
53+
canvas?: ComponentType;
5054
} ) {
55+
// Get canvas data from the current route's loader
56+
const matches = useMatches();
57+
const currentMatch = matches[ matches.length - 1 ];
58+
const canvasData = ( currentMatch?.loaderData as any )?.canvas as
59+
| CanvasData
60+
| null
61+
| undefined;
62+
5163
return (
5264
<>
5365
{ Stage && (
@@ -60,6 +72,18 @@ function RouteComponent( {
6072
<Inspector />
6173
</div>
6274
) }
75+
{ /* Render custom canvas when canvas() returns null */ }
76+
{ canvasData === null && CustomCanvas && (
77+
<div className="boot-layout__canvas">
78+
<CustomCanvas />
79+
</div>
80+
) }
81+
{ /* Render default canvas when canvas() returns CanvasData */ }
82+
{ canvasData && (
83+
<div className="boot-layout__canvas">
84+
<Canvas canvas={ canvasData } />
85+
</div>
86+
) }
6387
</>
6488
);
6589
}
@@ -75,21 +99,21 @@ async function createRouteFromDefinition(
7599
route: Route,
76100
parentRoute: AnyRoute
77101
) {
78-
// Create lazy components for stage and inspector surfaces
79-
const SurfacesModule = route.content_module
80-
? lazy( async () => {
81-
const module = await import( route.content_module! );
82-
// Return a component that renders the surfaces
83-
return {
84-
default: () => (
85-
<RouteComponent
86-
stage={ module.stage }
87-
inspector={ module.inspector }
88-
/>
89-
),
90-
};
91-
} )
92-
: () => null;
102+
// Create lazy components for stage, inspector, and canvas surfaces
103+
const SurfacesModule = lazy( async () => {
104+
const module = route.content_module
105+
? await import( route.content_module )
106+
: {};
107+
return {
108+
default: () => (
109+
<RouteComponent
110+
stage={ module.stage }
111+
inspector={ module.inspector }
112+
canvas={ module.canvas }
113+
/>
114+
),
115+
};
116+
} );
93117

94118
// Load route module for lifecycle functions if specified
95119
let routeConfig: {

packages/boot/src/components/root/index.tsx

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ import { privateApis as themePrivateApis } from '@wordpress/theme';
1515
* Internal dependencies
1616
*/
1717
import Sidebar from '../sidebar';
18-
import Canvas from '../canvas';
1918
import SavePanel from '../save-panel';
2019
import { unlock } from '../../lock-unlock';
2120
import type { CanvasData } from '../../store/types';
@@ -29,6 +28,7 @@ export default function Root() {
2928
const currentMatch = matches[ matches.length - 1 ];
3029
const canvas = ( currentMatch?.loaderData as any )?.canvas as
3130
| CanvasData
31+
| null
3232
| undefined;
3333
const isFullScreen = canvas && ! canvas.isPreview;
3434

@@ -37,7 +37,7 @@ export default function Root() {
3737
<ThemeProvider color={ { bg: '#1d2327', primary: '#3858e9' } }>
3838
<div
3939
className={ clsx( 'boot-layout', {
40-
'has-canvas': !! canvas,
40+
'has-canvas': !! canvas || canvas === null,
4141
'has-full-canvas': isFullScreen,
4242
} ) }
4343
>
@@ -53,11 +53,6 @@ export default function Root() {
5353
color={ { bg: '#ffffff', primary: '#3858e9' } }
5454
>
5555
<Outlet />
56-
{ canvas && (
57-
<div className="boot-layout__canvas">
58-
<Canvas canvas={ canvas } />
59-
</div>
60-
) }
6156
</ThemeProvider>
6257
</div>
6358
</div>

packages/boot/src/components/root/single-page.tsx

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import { privateApis as themePrivateApis } from '@wordpress/theme';
1414
/**
1515
* Internal dependencies
1616
*/
17-
import Canvas from '../canvas';
1817
import SavePanel from '../save-panel';
1918
import { unlock } from '../../lock-unlock';
2019
import type { CanvasData } from '../../store/types';
@@ -32,6 +31,7 @@ export default function RootSinglePage() {
3231
const currentMatch = matches[ matches.length - 1 ];
3332
const canvas = ( currentMatch?.loaderData as any )?.canvas as
3433
| CanvasData
34+
| null
3535
| undefined;
3636
const isFullScreen = canvas && ! canvas.isPreview;
3737

@@ -40,7 +40,7 @@ export default function RootSinglePage() {
4040
<ThemeProvider color={ { bg: '#1d2327', primary: '#3858e9' } }>
4141
<div
4242
className={ clsx( 'boot-layout boot-layout--single-page', {
43-
'has-canvas': !! canvas,
43+
'has-canvas': !! canvas || canvas === null,
4444
'has-full-canvas': isFullScreen,
4545
} ) }
4646
>
@@ -51,11 +51,6 @@ export default function RootSinglePage() {
5151
color={ { bg: '#ffffff', primary: '#3858e9' } }
5252
>
5353
<Outlet />
54-
{ canvas && (
55-
<div className="boot-layout__canvas">
56-
<Canvas canvas={ canvas } />
57-
</div>
58-
) }
5954
</ThemeProvider>
6055
</div>
6156
</div>
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
/**
2+
* WordPress dependencies
3+
*/
4+
import { useEffect, useState } from '@wordpress/element';
5+
import { useSelect } from '@wordpress/data';
6+
import { _n, __, sprintf } from '@wordpress/i18n';
7+
import { store as coreStore } from '@wordpress/core-data';
8+
import { displayShortcut, rawShortcut } from '@wordpress/keycodes';
9+
import { check } from '@wordpress/icons';
10+
import { EntitiesSavedStates } from '@wordpress/editor';
11+
import { Button, Modal, Tooltip } from '@wordpress/components';
12+
13+
/**
14+
* Internal dependencies
15+
*/
16+
import './style.scss';
17+
import useSaveShortcut from '../save-panel/use-save-shortcut';
18+
19+
export default function SaveButton() {
20+
const [ isSaveViewOpen, setIsSaveViewOpened ] = useState( false );
21+
const { isSaving, dirtyEntityRecordsCount } = useSelect( ( select ) => {
22+
const { isSavingEntityRecord, __experimentalGetDirtyEntityRecords } =
23+
select( coreStore );
24+
const dirtyEntityRecords = __experimentalGetDirtyEntityRecords();
25+
return {
26+
isSaving: dirtyEntityRecords.some( ( record ) =>
27+
isSavingEntityRecord( record.kind, record.name, record.key )
28+
),
29+
dirtyEntityRecordsCount: dirtyEntityRecords.length,
30+
};
31+
}, [] );
32+
const [ showSavedState, setShowSavedState ] = useState( false );
33+
34+
useEffect( () => {
35+
if ( isSaving ) {
36+
// Proactively expect to show saved state. This is done once save
37+
// starts to avoid race condition where setting it after would cause
38+
// the button to be unmounted before state is updated.
39+
setShowSavedState( true );
40+
}
41+
}, [ isSaving ] );
42+
43+
const hasChanges = dirtyEntityRecordsCount > 0;
44+
45+
// Handle save failure case: If we were showing saved state but saving
46+
// failed, reset to show changes again.
47+
useEffect( () => {
48+
if ( ! isSaving && hasChanges ) {
49+
setShowSavedState( false );
50+
}
51+
}, [ isSaving, hasChanges ] );
52+
53+
function hideSavedState() {
54+
if ( showSavedState ) {
55+
setShowSavedState( false );
56+
}
57+
}
58+
59+
const shouldShowButton = hasChanges || showSavedState;
60+
61+
useSaveShortcut( { openSavePanel: () => setIsSaveViewOpened( true ) } );
62+
63+
if ( ! shouldShowButton ) {
64+
return null;
65+
}
66+
67+
const isInSavedState = showSavedState && ! hasChanges;
68+
const disabled = isSaving || isInSavedState;
69+
70+
const getLabel = () => {
71+
if ( isInSavedState ) {
72+
return __( 'Saved' );
73+
}
74+
return sprintf(
75+
// translators: %d: number of unsaved changes (number).
76+
_n(
77+
'Review %d change…',
78+
'Review %d changes…',
79+
dirtyEntityRecordsCount
80+
),
81+
dirtyEntityRecordsCount
82+
);
83+
};
84+
const label = getLabel();
85+
86+
return (
87+
<>
88+
<Tooltip
89+
text={ hasChanges ? label : undefined }
90+
shortcut={ displayShortcut.primary( 's' ) }
91+
>
92+
<Button
93+
variant="primary"
94+
size="compact"
95+
onClick={ () => setIsSaveViewOpened( true ) }
96+
onBlur={ hideSavedState }
97+
disabled={ disabled }
98+
accessibleWhenDisabled
99+
isBusy={ isSaving }
100+
aria-keyshortcuts={ rawShortcut.primary( 's' ) }
101+
className="boot-save-button"
102+
icon={ isInSavedState ? check : undefined }
103+
>
104+
{ label }
105+
</Button>
106+
</Tooltip>
107+
{ isSaveViewOpen && (
108+
<Modal
109+
title={ __( 'Review changes' ) }
110+
onRequestClose={ () => setIsSaveViewOpened( false ) }
111+
size="small"
112+
>
113+
<EntitiesSavedStates
114+
close={ () => setIsSaveViewOpened( false ) }
115+
variant="inline"
116+
/>
117+
</Modal>
118+
) }
119+
</>
120+
);
121+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
.boot-save-button {
2+
width: 100%;
3+
}

packages/boot/src/components/sidebar/index.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
*/
44
import SiteHub from '../site-hub';
55
import Navigation from '../navigation';
6+
import SaveButton from '../save-button';
67
import './style.scss';
78

89
export default function Sidebar() {
@@ -12,6 +13,9 @@ export default function Sidebar() {
1213
<div className="boot-sidebar__content">
1314
<Navigation />
1415
</div>
16+
<div className="boot-sidebar__footer">
17+
<SaveButton />
18+
</div>
1519
</div>
1620
);
1721
}

packages/boot/src/components/sidebar/style.scss

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,7 @@
1313
contain: content;
1414
position: relative;
1515
}
16+
17+
.boot-sidebar__footer {
18+
padding: variables.$grid-unit-20 variables.$grid-unit-10 variables.$grid-unit-10 variables.$grid-unit-20;
19+
}

packages/boot/src/store/types.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,11 @@ export interface Route {
5858
path: string;
5959

6060
/**
61-
* Module path for lazy loading the route's surfaces (stage, inspector).
62-
* The module must export: RouteSurfaces (stage and/or inspector components)
61+
* Module path for lazy loading the route's surfaces.
62+
* The module can export:
63+
* - stage?: Main content component (ComponentType)
64+
* - inspector?: Sidebar component (ComponentType)
65+
* - canvas?: Custom canvas component (ComponentType)
6366
* This enables code splitting for better performance.
6467
*/
6568
content_module?: string;
@@ -69,7 +72,10 @@ export interface Route {
6972
* The module should export a named export `route` containing:
7073
* - beforeLoad?: Pre-navigation hook (authentication, validation, redirects)
7174
* - loader?: Data preloading function
72-
* - canvas?: Function that returns canvas data for rendering an editor
75+
* - canvas?: Function that returns canvas data for rendering
76+
* - Returns CanvasData to use default editor canvas
77+
* - Returns null to use custom canvas component from content_module
78+
* - Returns undefined to show no canvas
7379
*/
7480
route_module?: string;
7581
}

0 commit comments

Comments
 (0)