diff --git a/changelog.txt b/changelog.txt index 8ca20b7f166a1..02864f4ea4bf2 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,5 +1,324 @@ == Changelog == += 18.1.0 = + +## Changelog + +### Enhancements + +#### Block Editor +- Zoom out: Invoke zoom out mode when opening the patterns tab, and move the code to do so to a shared hook. ([59775](https://github.com/WordPress/gutenberg/pull/59775)) +- Block Previews: Update shadows in different contexts. ([60161](https://github.com/WordPress/gutenberg/pull/60161)) +- Update: Move post actions to the editor package. ([60092](https://github.com/WordPress/gutenberg/pull/60092)) +- Try: Show copy shortcut in block options. ([60339](https://github.com/WordPress/gutenberg/pull/60339)) +- Update image role description text to fix spacing. ([60338](https://github.com/WordPress/gutenberg/pull/60338)) + +#### Site Editor +- Add rename page action. ([60230](https://github.com/WordPress/gutenberg/pull/60230)) +- Center the document title. ([59134](https://github.com/WordPress/gutenberg/pull/59134)) +- Consolidate when showing revisions link or action. ([60194](https://github.com/WordPress/gutenberg/pull/60194)) +- Editor: Update hover color of editor document title. ([60113](https://github.com/WordPress/gutenberg/pull/60113)) +- Improve the frame animation. ([60363](https://github.com/WordPress/gutenberg/pull/60363)) +- Try selecting closest editable block when clicking on a disabled block. ([60016](https://github.com/WordPress/gutenberg/pull/60016)) +- Update index view for pages. ([59950](https://github.com/WordPress/gutenberg/pull/59950)) + +#### Patterns +- Add content schema to pattern editing view. ([59977](https://github.com/WordPress/gutenberg/pull/59977)) +- Close inspector on pattern category select. ([60004](https://github.com/WordPress/gutenberg/pull/60004)) +- Focus block selection button only in navigation mode. ([60207](https://github.com/WordPress/gutenberg/pull/60207)) +- Pattern Shuffling: Make the results deterministic. ([60074](https://github.com/WordPress/gutenberg/pull/60074)) +- Patterns page: Enable table layout. ([60337](https://github.com/WordPress/gutenberg/pull/60337)) +- Prevent reordering of header and footer template parts when zoomed out. ([60054](https://github.com/WordPress/gutenberg/pull/60054)) +- Remove manage all of my patterns link. ([60345](https://github.com/WordPress/gutenberg/pull/60345)) + +#### Block Library +- Add `__next40pxDefaultSize` to Image block Title Attribute. ([60117](https://github.com/WordPress/gutenberg/pull/60117)) +- Add support "HTML Element" to Site Tagline. ([59654](https://github.com/WordPress/gutenberg/pull/59654)) +- Image: Remove temporary image check for rendering controls. ([60212](https://github.com/WordPress/gutenberg/pull/60212)) +- Quote block: Button for cite add/remove. ([59073](https://github.com/WordPress/gutenberg/pull/59073)) +- Quote block: Remove appender. ([60307](https://github.com/WordPress/gutenberg/pull/60307)) +- Reduce specificity of block library styles conflicting with block supports. ([59457](https://github.com/WordPress/gutenberg/pull/59457)) +- Update navigation blocks to use consistent link UI labels and field sizes. ([60116](https://github.com/WordPress/gutenberg/pull/60116)) +- Summary: Polish featured image. ([60110](https://github.com/WordPress/gutenberg/pull/60110)) + +#### Post Editor +- Block Editor: Deprecate `__experimentalGetReusableBlockTitle` selector. ([60278](https://github.com/WordPress/gutenberg/pull/60278)) +- Editor: Move PluginPostPublishPanel and PluginPrePublishPanel to editor package. ([60344](https://github.com/WordPress/gutenberg/pull/60344)) +- Editor: Move publish panel handling to `editor` store. ([60340](https://github.com/WordPress/gutenberg/pull/60340)) +- Editor: Unify publish sidebar preference. ([60334](https://github.com/WordPress/gutenberg/pull/60334)) + +#### Global Styles +- Add background to global styles changes output. ([60229](https://github.com/WordPress/gutenberg/pull/60229)) +- Background UI controls. ([59454](https://github.com/WordPress/gutenberg/pull/59454)) +- Follow up design tweaks for global styles presets. ([60031](https://github.com/WordPress/gutenberg/pull/60031)) +- Try reducing specificity of global styles selectors only. ([60106](https://github.com/WordPress/gutenberg/pull/60106)) + +#### Data Views +- DataViews: Add a utility to share filtering, sorting and pagination logic. ([59897](https://github.com/WordPress/gutenberg/pull/59897)) +- Data views: Remove the `enumeration` type as redundant. ([60084](https://github.com/WordPress/gutenberg/pull/60084)) +- Data views: Update template actions. ([60075](https://github.com/WordPress/gutenberg/pull/60075)) +- Data views: Add confirmation modal for clearing customizations in templates. ([60119](https://github.com/WordPress/gutenberg/pull/60119)) +- Data views: Make trash a quick action again. ([60165](https://github.com/WordPress/gutenberg/pull/60165)) + +#### List View +- Add keyboard shortcut to collapse list view items other than the focused item. ([59978](https://github.com/WordPress/gutenberg/pull/59978)) +- Adjust the List View close icon to resemble the Inspector close icon. ([59999](https://github.com/WordPress/gutenberg/pull/59999)) +- Update "Actions" string to "Options" in List View. ([60136](https://github.com/WordPress/gutenberg/pull/60136)) + +#### Templates +- Add filter to allow extending the list of post content blocks. ([60068](https://github.com/WordPress/gutenberg/pull/60068)) +- Render non-editable preview of template part when user does not have capability to edit template part. ([60326](https://github.com/WordPress/gutenberg/pull/60326)) +- Template Parts: Remove pattern title from sidebar. ([60160](https://github.com/WordPress/gutenberg/pull/60160)) +- Template Parts: Update replace flow to separate template parts from patterns. ([60203](https://github.com/WordPress/gutenberg/pull/60203)) +- Template Parts: Update the 'Replace' label to 'Design'. ([60156](https://github.com/WordPress/gutenberg/pull/60156)) + +#### Zoom Out +- Add a delete control to toolbar on zoomed out mode. ([60214](https://github.com/WordPress/gutenberg/pull/60214)) +- Media dialog push content in zoomed out mode. ([60170](https://github.com/WordPress/gutenberg/pull/60170)) + +#### Components +- Popover / ToggleGroupControl: Use `useReducedMotion()` from `@wordpress/compose`. ([60168](https://github.com/WordPress/gutenberg/pull/60168)) +- date-fns: Bump to v3.6. ([60163](https://github.com/WordPress/gutenberg/pull/60163)) + +#### REST API +- Allow view access of template rest endpoint to anyone with the `edit_post` capability. ([60317](https://github.com/WordPress/gutenberg/pull/60317)) + +#### Commands +- Polish Command Palette. ([60134](https://github.com/WordPress/gutenberg/pull/60134)) + +#### Page Content Focus +- Allow selecting template parts in page content focus mode. ([60010](https://github.com/WordPress/gutenberg/pull/60010)) + +#### Inspector Controls +- Add: PostCardPanel component. ([59870](https://github.com/WordPress/gutenberg/pull/59870)) + +#### Package and utility updates +- Router: Update history package to 5.3.0, fix query string generation. ([60271](https://github.com/WordPress/gutenberg/pull/60271)) +- Create block: Add new namespacePascalCase template variable. ([60223](https://github.com/WordPress/gutenberg/pull/60223)) + +### New APIs + +#### Extensibility +- Extensibility: Support PluginBlockSettingsMenuItem in the site editor. ([60033](https://github.com/WordPress/gutenberg/pull/60033)) + +### Bug Fixes + +#### Block Library +- Fix enqueuing block theme styles when separate asset loading is enabled. ([60098](https://github.com/WordPress/gutenberg/pull/60098)) +- Fix lightbox UI disallow editing. ([59890](https://github.com/WordPress/gutenberg/pull/59890)) +- Fix navigation link ui close focus management. ([59925](https://github.com/WordPress/gutenberg/pull/59925)) +- Removed pointer-events none inline style due it blocking crop action. ([60305](https://github.com/WordPress/gutenberg/pull/60305)) +- Search Block: Apply font-related style inheritance to input field. ([60321](https://github.com/WordPress/gutenberg/pull/60321)) +- Columns block: Fix arrow up into it. ([55197](https://github.com/WordPress/gutenberg/pull/55197)) + +#### Site Editor +- Fix rendering PluginTemplateSettingPanel when we're editing a template. ([60215](https://github.com/WordPress/gutenberg/pull/60215)) +- Fix: Use `viewportWidth` in pattern preview data view. ([60315](https://github.com/WordPress/gutenberg/pull/60315)) +- Templates: Fix deferred rendering. ([60361](https://github.com/WordPress/gutenberg/pull/60361)) +- Site Editor: Consolidate save button functionality. ([60077](https://github.com/WordPress/gutenberg/pull/60077)) +- Revert #60300: Make sure the CSS class id-dark-theme is added to the editor iframe body. ([60616](https://github.com/WordPress/gutenberg/pull/60616)) + +#### Block Editor +- Make sure the CSS class is-dark-theme is added to the editor iframe body. ([60300](https://github.com/WordPress/gutenberg/pull/60300)) +- Raw handling: Preserve class. ([60331](https://github.com/WordPress/gutenberg/pull/60331)) +- Raw handling: Preserve empty paragraphs. ([59476](https://github.com/WordPress/gutenberg/pull/59476)) +- Wiriting flow: Backspace at beginning of first paragraph block prevents block from being deleted. ([56329](https://github.com/WordPress/gutenberg/pull/56329)) +- DOM: Fix return types of focus.tabbable methods. ([60274](https://github.com/WordPress/gutenberg/pull/60274)) + +#### Components +- CustomSelectControlV2: Fix hint behavior in legacy. ([60183](https://github.com/WordPress/gutenberg/pull/60183)) +- InputControl: Ignore IME events when `isPressEnterToChange`. ([60090](https://github.com/WordPress/gutenberg/pull/60090)) +- TextControl: Apply zero margin to input element. ([60282](https://github.com/WordPress/gutenberg/pull/60282)) + +#### Global Styles +- Fix missing class for Global Styles > Colors. ([60094](https://github.com/WordPress/gutenberg/pull/60094)) +- Reset specificity of body selector when processing with postcss. ([60266](https://github.com/WordPress/gutenberg/pull/60266)) +- Shadow: Revert shadow default presets opt-in to opt-out. ([60204](https://github.com/WordPress/gutenberg/pull/60204)) +- Global Styles: Make strings translatable. ([60127](https://github.com/WordPress/gutenberg/pull/60127)) +- Skip outputting base layout rules that reference content or wide sizes if no layout sizes exist. ([60489](https://github.com/WordPress/gutenberg/pull/60489)) + +#### Zoom Out +- Fix zoom out mode toggling between pattern category selection. ([60225](https://github.com/WordPress/gutenberg/pull/60225)) + +#### Data Views +- Fix focus outline visibility and truncation in data view record titles. ([60191](https://github.com/WordPress/gutenberg/pull/60191)) + +#### Layout +- Fix horizontal flex layout in classic themes. ([60154](https://github.com/WordPress/gutenberg/pull/60154)) + +#### Post Editor +- Editor: Memoize 'getInsertionPoint' selector. ([60015](https://github.com/WordPress/gutenberg/pull/60015)) +- Fix block toolbar dropdown separator color. ([60336](https://github.com/WordPress/gutenberg/pull/60336)) +- Backport r57868 (Editor: Prevent font folder naive filtering causing infinite loops) from WordPress-Develop. ([60141](https://github.com/WordPress/gutenberg/pull/60141)) + +#### Block templates +- Add null check to prevent errors in `get_block_template` filter. ([60491](https://github.com/WordPress/gutenberg/pull/60491)) + +#### Utilities +- URL: Return early in getFilename where URL argument is falsy. ([60265](https://github.com/WordPress/gutenberg/pull/60265)) + + +### Accessibility + +#### Data Views +- Add click-to-select behavior on table rows. ([59803](https://github.com/WordPress/gutenberg/pull/59803)) +- Data views list layout: Apply focus styles to items on `focus-visible` rather than `focus`. ([60253](https://github.com/WordPress/gutenberg/pull/60253)) +- Update field display in grid layout. ([60083](https://github.com/WordPress/gutenberg/pull/60083)) +- Data Views: Updating keyboard navigation in list layouts. ([59637](https://github.com/WordPress/gutenberg/pull/59637)) + +#### Site Editor +- Restore Style book close button tooltip. ([60177](https://github.com/WordPress/gutenberg/pull/60177)) + +#### Block Library +- Remove CSS order property from social icons placeholder UI. ([60032](https://github.com/WordPress/gutenberg/pull/60032)) + +### Performance + +### Block Editor +- Avoid fetching all reusable blocks (user patterns) on post/site editor load. ([58239](https://github.com/WordPress/gutenberg/pull/58239)) +- Block editor: Optimize hasSelectedInnerBlock selector. ([60330](https://github.com/WordPress/gutenberg/pull/60330)) +- Templates performance: Resolve patterns server side. ([60349](https://github.com/WordPress/gutenberg/pull/60349)) + +#### Block Library +- Template part: Avoid pattern fetch on mount. ([60297](https://github.com/WordPress/gutenberg/pull/60297)) + +#### Block Editor +- Inserter: Cache search normalization results. ([60080](https://github.com/WordPress/gutenberg/pull/60080)) +- Format library: Improve unknown format performance. ([48761](https://github.com/WordPress/gutenberg/pull/48761)) + +### Experiments + +#### Site Editor +- Zoom-out view: Disable canvas resizing. ([60104](https://github.com/WordPress/gutenberg/pull/60104)) + + +### Documentation + +- Add auto generated API documentation for `editor` package. ([60356](https://github.com/WordPress/gutenberg/pull/60356)) +- Add component props documentation. ([60350](https://github.com/WordPress/gutenberg/pull/60350)) +- Add php `@global` documentation. ([59931](https://github.com/WordPress/gutenberg/pull/59931)) +- Change heading level on troubleshooting section. ([60233](https://github.com/WordPress/gutenberg/pull/60233)) +- CustomSelectControlV2: Match v1 stories to test legacy component. ([60182](https://github.com/WordPress/gutenberg/pull/60182)) +- Docs (general): Fix some typos. ([60260](https://github.com/WordPress/gutenberg/pull/60260)) +- Fix `@todo` tags to follow standards in WordPress comments. ([60148](https://github.com/WordPress/gutenberg/pull/60148)) +- Fix Font Collection JSON schema definition. ([60285](https://github.com/WordPress/gutenberg/pull/60285)) +- Fix: Invalid documentation link to load JavaScript. ([60181](https://github.com/WordPress/gutenberg/pull/60181)) +- Fix: Invalid links to the block supports api. ([60199](https://github.com/WordPress/gutenberg/pull/60199)) +- Fix: Non existent link to submitting to the block directory. ([60389](https://github.com/WordPress/gutenberg/pull/60389)) +- Interactivity API: Variable name correction in the documentation. ([60056](https://github.com/WordPress/gutenberg/pull/60056)) +- Create Block: Update external template documentation to include variants. ([60095](https://github.com/WordPress/gutenberg/pull/60095)) + + +### Code Quality + + +#### Block Editor +- Add comment for unmemoized context. ([60272](https://github.com/WordPress/gutenberg/pull/60272)) +- Fix ESLint warning in BlockListBlock component. ([60064](https://github.com/WordPress/gutenberg/pull/60064)) +- RichText: Switch from disableEditing to standard html readonly attribute. ([60327](https://github.com/WordPress/gutenberg/pull/60327)) +- Site Editor: Reuse inserter search term normalization. ([60218](https://github.com/WordPress/gutenberg/pull/60218)) +- `canInsertBlockType`: Extract helper for selector dependants. ([60235](https://github.com/WordPress/gutenberg/pull/60235)) +- Fix editor canvas overflow on search results with position: Relative. ([60287](https://github.com/WordPress/gutenberg/pull/60287)) +- Editor: Move template areas to editor package. ([60179](https://github.com/WordPress/gutenberg/pull/60179)) + +#### Components +- `CustomSelectControlV2`: Rename for consistency. ([60178](https://github.com/WordPress/gutenberg/pull/60178)) +- Navigator: Fix two nits. ([60273](https://github.com/WordPress/gutenberg/pull/60273)) +- NavigatorProvider: Move all state management to one reducer. ([60190](https://github.com/WordPress/gutenberg/pull/60190)) +- Components: Try obviating Popover pointer event trap. ([59449](https://github.com/WordPress/gutenberg/pull/59449)) + +#### Post Editor +- Memoize the getTemplateInfo selector. ([60200](https://github.com/WordPress/gutenberg/pull/60200)) +- Update: Remove template summary component. ([60351](https://github.com/WordPress/gutenberg/pull/60351)) +- Update: Use getPostIcon selector on document bar. ([60128](https://github.com/WordPress/gutenberg/pull/60128)) +- Distraction free: Remove unwanted space from string. ([60108](https://github.com/WordPress/gutenberg/pull/60108)) + +#### Global Styles +- Additional CSS: Add code comments contextualising tranformStyles for clarity. ([60267](https://github.com/WordPress/gutenberg/pull/60267)) +- Global styles: output `:root` selector for CSS custom properties. ([42084](https://github.com/WordPress/gutenberg/pull/42084)) +- Style Engine: Continue get_classnames loop after adding the default classname. ([60153](https://github.com/WordPress/gutenberg/pull/60153)) + +#### Font Library +- Add test for Font Library and Theme Style Variations. ([60250](https://github.com/WordPress/gutenberg/pull/60250)) +- Update google fonts font collection data URL to the latest version available. ([60079](https://github.com/WordPress/gutenberg/pull/60079)) + +#### Block Library +- Image: Use the new 'useUploadMediaFromBlobURL' hook. ([60208](https://github.com/WordPress/gutenberg/pull/60208)) +- Navigation Block: Add test coverage to check that post content is not removed. ([60189](https://github.com/WordPress/gutenberg/pull/60189)) + +#### Site Editor +- DataViews: Don't memoize every callback 'PagePages' component. ([60103](https://github.com/WordPress/gutenberg/pull/60103)) +- History: Simplify the push and replace methods. ([60112](https://github.com/WordPress/gutenberg/pull/60112)) + +#### Rich Text +- RichText: Separate fallback instance ID for selection retrieval. ([60277](https://github.com/WordPress/gutenberg/pull/60277)) + +#### Block Locking +- E2E: Test BlockSwitcher availability in l-post-ul-group CPT. ([60254](https://github.com/WordPress/gutenberg/pull/60254)) + +#### Data Views +- DataViews: Fix react warning error in list layout. ([60101](https://github.com/WordPress/gutenberg/pull/60101)) + + +### Tools + +#### Testing +- Automated Testing: Remove Puppeteer CI Job. ([59311](https://github.com/WordPress/gutenberg/pull/59311)) +- CustomSelectControlV2: Stabilize tests. ([60133](https://github.com/WordPress/gutenberg/pull/60133)) +- E2E: Fix flaky Site Editor pages end-to-end test. ([60109](https://github.com/WordPress/gutenberg/pull/60109)) +- Font Library: Add upload font test. ([60221](https://github.com/WordPress/gutenberg/pull/60221)) + +#### Build Tooling +- Blocks: Fix double `gutenberg_` prefix in built dynamic blocks PHP. ([60288](https://github.com/WordPress/gutenberg/pull/60288)) + +## First-time contributors + +The following PRs were merged by first-time contributors 🎉 : + +- @interdevel: Fix `@todo` tags to follow standards in WordPress comments. ([60148](https://github.com/WordPress/gutenberg/pull/60148)) +- @mikeybinns: Add component props documentation. ([60350](https://github.com/WordPress/gutenberg/pull/60350)) +- @nirav7707: Editor: Update hover color of editor document title. ([60113](https://github.com/WordPress/gutenberg/pull/60113)) +- @steveariss: Interactivity API: Variable name correction in the documentation. ([60056](https://github.com/WordPress/gutenberg/pull/60056)) + + +## Contributors + +The following contributors merged PRs in this release: + +@aaronrobertshaw @afercia @andrewhayward @andrewserong @artemiomorales @bph @draganescu @ellatrix @fabiankaegy @geriux @getdave @glendaviesnz @interdevel @jameskoster @jasmussen @jeryj @jorgefilipecosta @jsnajdr @madhusudhand @MaggieCabrera @Mamaduka @matiasbenedetto @mcsf @mikachan @mikeybinns @mirka @mujuonly @n2erjo00 @nirav7707 @noisysocks @ntsekouras @oandregal @ockham @okmttdhr @pedro-mendonca @peterwilsoncc @ramonjd @richtabor @ryanwelcher @scruffian @shail-mehta @Soean @steveariss @stokesman @t-hamano @talldan @tellthemachines @torounit @tyxla @youknowriad + + + + += 18.0.1 = + +## Bug fixes + +Make sure display name is correctly formatted in comments. (#60579) + + += 18.1.0-rc.2 = + + +## Changelog + +### Bug Fixes + +- Add null check to prevent errors in `get_block_template` filter. ([60491](https://github.com/WordPress/gutenberg/pull/60491)) + +#### Layout +- Skip outputting base layout rules that reference content or wide sizes if no layout sizes exist. ([60489](https://github.com/WordPress/gutenberg/pull/60489)) + + + + +## Contributors + +The following contributors merged PRs in this release: + +@andrewserong @okmttdhr + + = 18.1.0-rc.1 = diff --git a/docs/contributors/code/coding-guidelines.md b/docs/contributors/code/coding-guidelines.md index 52fa92440b265..63114073b1b80 100644 --- a/docs/contributors/code/coding-guidelines.md +++ b/docs/contributors/code/coding-guidelines.md @@ -720,7 +720,7 @@ function getConfigurationValue( key ) { } ``` -When documenting a [function type](https://github.com/WordPress/gutenberg/blob/add/typescript-jsdoc-guidelines/docs/contributors/coding-guidelines.md#record-types), you must always include the `void` return value type, as otherwise the function is inferred to return a mixed ("any") value, not a void result. +When documenting a [function type](#generic-types), you must always include the `void` return value type, as otherwise the function is inferred to return a mixed ("any") value, not a void result. ```js /** diff --git a/gutenberg.php b/gutenberg.php index f21aed1c5fc34..8e7889224566c 100644 --- a/gutenberg.php +++ b/gutenberg.php @@ -5,7 +5,7 @@ * Description: Printing since 1440. This is the development plugin for the block editor, site editor, and other future WordPress core functionality. * Requires at least: 6.3 * Requires PHP: 7.0 - * Version: 18.1.0-rc.1 + * Version: 18.1.0 * Author: Gutenberg Team * Text Domain: gutenberg * diff --git a/lib/interactivity-api.php b/lib/interactivity-api.php index da349e1c03376..6f04a3ba8fc92 100644 --- a/lib/interactivity-api.php +++ b/lib/interactivity-api.php @@ -16,7 +16,7 @@ function gutenberg_reregister_interactivity_script_modules() { wp_register_script_module( '@wordpress/interactivity', - gutenberg_url( '/build/interactivity/index.min.js' ), + gutenberg_url( '/build/interactivity/' . ( SCRIPT_DEBUG ? 'debug.min.js' : 'index.min.js' ) ), array(), $default_version ); diff --git a/package-lock.json b/package-lock.json index aa35cf655f019..2819636e591fa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "gutenberg", - "version": "18.1.0-rc.1", + "version": "18.1.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "gutenberg", - "version": "18.1.0-rc.1", + "version": "18.1.0", "hasInstallScript": true, "license": "GPL-2.0-or-later", "dependencies": { @@ -99,7 +99,7 @@ "@octokit/rest": "16.26.0", "@octokit/types": "6.34.0", "@octokit/webhooks-types": "5.6.0", - "@playwright/test": "1.42.1", + "@playwright/test": "1.43.0", "@pmmmwh/react-refresh-webpack-plugin": "0.5.11", "@react-native/babel-preset": "0.73.10", "@react-native/metro-babel-transformer": "0.73.10", @@ -6979,12 +6979,12 @@ } }, "node_modules/@playwright/test": { - "version": "1.42.1", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.42.1.tgz", - "integrity": "sha512-Gq9rmS54mjBL/7/MvBaNOBwbfnh7beHvS6oS4srqXFcQHpQCV1+c8JXWE8VLPyRDhgS3H8x8A7hztqI9VnwrAQ==", + "version": "1.43.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.43.0.tgz", + "integrity": "sha512-Ebw0+MCqoYflop7wVKj711ccbNlrwTBCtjY5rlbiY9kHL2bCYxq+qltK6uPsVBGGAOb033H2VO0YobcQVxoW7Q==", "dev": true, "dependencies": { - "playwright": "1.42.1" + "playwright": "1.43.0" }, "bin": { "playwright": "cli.js" @@ -42214,12 +42214,12 @@ "dev": true }, "node_modules/playwright": { - "version": "1.42.1", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.42.1.tgz", - "integrity": "sha512-PgwB03s2DZBcNRoW+1w9E+VkLBxweib6KTXM0M3tkiT4jVxKSi6PmVJ591J+0u10LUrgxB7dLRbiJqO5s2QPMg==", + "version": "1.43.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.43.0.tgz", + "integrity": "sha512-SiOKHbVjTSf6wHuGCbqrEyzlm6qvXcv7mENP+OZon1I07brfZLGdfWV0l/efAzVx7TF3Z45ov1gPEkku9q25YQ==", "dev": true, "dependencies": { - "playwright-core": "1.42.1" + "playwright-core": "1.43.0" }, "bin": { "playwright": "cli.js" @@ -42232,9 +42232,9 @@ } }, "node_modules/playwright-core": { - "version": "1.42.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.42.1.tgz", - "integrity": "sha512-mxz6zclokgrke9p1vtdy/COWBH+eOZgYUVVU34C73M+4j4HLlQJHtfcqiqqxpP0o8HhMkflvfbquLX5dg6wlfA==", + "version": "1.43.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.43.0.tgz", + "integrity": "sha512-iWFjyBUH97+pUFiyTqSLd8cDMMOS0r2ZYz2qEsPjH8/bX++sbIJT35MSwKnp1r/OQBAqC5XO99xFbJ9XClhf4w==", "dev": true, "bin": { "playwright-core": "cli.js" @@ -43924,24 +43924,6 @@ "react": "^18.2.0" } }, - "node_modules/react-easy-crop": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/react-easy-crop/-/react-easy-crop-4.5.1.tgz", - "integrity": "sha512-MVzCWmKXTwZTK0iYqlF/gPLdLqvUGrLGX7SQ4g+DO3b/lCiVAwxZKLeZ1wjDfG+r/yEWUoL7At5a0kkDJeU+rQ==", - "dependencies": { - "normalize-wheel": "^1.0.1", - "tslib": "2.0.1" - }, - "peerDependencies": { - "react": ">=16.4.0", - "react-dom": ">=16.4.0" - } - }, - "node_modules/react-easy-crop/node_modules/tslib": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.1.tgz", - "integrity": "sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ==" - }, "node_modules/react-element-to-jsx-string": { "version": "15.0.0", "resolved": "https://registry.npmjs.org/react-element-to-jsx-string/-/react-element-to-jsx-string-15.0.0.tgz", @@ -53294,7 +53276,7 @@ "postcss-prefixwrap": "^1.41.0", "postcss-urlrebase": "^1.0.0", "react-autosize-textarea": "^7.1.0", - "react-easy-crop": "^4.5.1", + "react-easy-crop": "^5.0.6", "remove-accents": "^0.5.0" }, "engines": { @@ -53332,6 +53314,19 @@ "node": "^10 || ^12 || >=14" } }, + "packages/block-editor/node_modules/react-easy-crop": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/react-easy-crop/-/react-easy-crop-5.0.6.tgz", + "integrity": "sha512-LV8te8NGC72k3l8uAqPAw73D2i9AbRlZqyo1Xz8VetwiMfkSKYgyqE3IFEwf5h+1g7AS1nMxBKk6ZPdhvLw6MQ==", + "dependencies": { + "normalize-wheel": "^1.0.1", + "tslib": "2.0.1" + }, + "peerDependencies": { + "react": ">=16.4.0", + "react-dom": ">=16.4.0" + } + }, "packages/block-editor/node_modules/source-map-js": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", @@ -53340,6 +53335,11 @@ "node": ">=0.10.0" } }, + "packages/block-editor/node_modules/tslib": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.1.tgz", + "integrity": "sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ==" + }, "packages/block-library": { "name": "@wordpress/block-library", "version": "8.32.0", @@ -55377,7 +55377,7 @@ "npm": ">=6.14.4" }, "peerDependencies": { - "@playwright/test": "^1.42.1", + "@playwright/test": "^1.43.0", "react": "^18.0.0", "react-dom": "^18.0.0" } @@ -60722,12 +60722,12 @@ } }, "@playwright/test": { - "version": "1.42.1", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.42.1.tgz", - "integrity": "sha512-Gq9rmS54mjBL/7/MvBaNOBwbfnh7beHvS6oS4srqXFcQHpQCV1+c8JXWE8VLPyRDhgS3H8x8A7hztqI9VnwrAQ==", + "version": "1.43.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.43.0.tgz", + "integrity": "sha512-Ebw0+MCqoYflop7wVKj711ccbNlrwTBCtjY5rlbiY9kHL2bCYxq+qltK6uPsVBGGAOb033H2VO0YobcQVxoW7Q==", "dev": true, "requires": { - "playwright": "1.42.1" + "playwright": "1.43.0" } }, "@pmmmwh/react-refresh-webpack-plugin": { @@ -68627,7 +68627,7 @@ "postcss-prefixwrap": "^1.41.0", "postcss-urlrebase": "^1.0.0", "react-autosize-textarea": "^7.1.0", - "react-easy-crop": "^4.5.1", + "react-easy-crop": "^5.0.6", "remove-accents": "^0.5.0" }, "dependencies": { @@ -68641,10 +68641,24 @@ "source-map-js": "^1.2.0" } }, + "react-easy-crop": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/react-easy-crop/-/react-easy-crop-5.0.6.tgz", + "integrity": "sha512-LV8te8NGC72k3l8uAqPAw73D2i9AbRlZqyo1Xz8VetwiMfkSKYgyqE3IFEwf5h+1g7AS1nMxBKk6ZPdhvLw6MQ==", + "requires": { + "normalize-wheel": "^1.0.1", + "tslib": "2.0.1" + } + }, "source-map-js": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==" + }, + "tslib": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.1.tgz", + "integrity": "sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ==" } } }, @@ -88581,19 +88595,19 @@ "dev": true }, "playwright": { - "version": "1.42.1", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.42.1.tgz", - "integrity": "sha512-PgwB03s2DZBcNRoW+1w9E+VkLBxweib6KTXM0M3tkiT4jVxKSi6PmVJ591J+0u10LUrgxB7dLRbiJqO5s2QPMg==", + "version": "1.43.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.43.0.tgz", + "integrity": "sha512-SiOKHbVjTSf6wHuGCbqrEyzlm6qvXcv7mENP+OZon1I07brfZLGdfWV0l/efAzVx7TF3Z45ov1gPEkku9q25YQ==", "dev": true, "requires": { "fsevents": "2.3.2", - "playwright-core": "1.42.1" + "playwright-core": "1.43.0" } }, "playwright-core": { - "version": "1.42.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.42.1.tgz", - "integrity": "sha512-mxz6zclokgrke9p1vtdy/COWBH+eOZgYUVVU34C73M+4j4HLlQJHtfcqiqqxpP0o8HhMkflvfbquLX5dg6wlfA==", + "version": "1.43.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.43.0.tgz", + "integrity": "sha512-iWFjyBUH97+pUFiyTqSLd8cDMMOS0r2ZYz2qEsPjH8/bX++sbIJT35MSwKnp1r/OQBAqC5XO99xFbJ9XClhf4w==", "dev": true }, "please-upgrade-node": { @@ -89849,22 +89863,6 @@ "scheduler": "^0.23.0" } }, - "react-easy-crop": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/react-easy-crop/-/react-easy-crop-4.5.1.tgz", - "integrity": "sha512-MVzCWmKXTwZTK0iYqlF/gPLdLqvUGrLGX7SQ4g+DO3b/lCiVAwxZKLeZ1wjDfG+r/yEWUoL7At5a0kkDJeU+rQ==", - "requires": { - "normalize-wheel": "^1.0.1", - "tslib": "2.0.1" - }, - "dependencies": { - "tslib": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.1.tgz", - "integrity": "sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ==" - } - } - }, "react-element-to-jsx-string": { "version": "15.0.0", "resolved": "https://registry.npmjs.org/react-element-to-jsx-string/-/react-element-to-jsx-string-15.0.0.tgz", diff --git a/package.json b/package.json index e87baf3215cb5..c7043fa9cc06e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gutenberg", - "version": "18.1.0-rc.1", + "version": "18.1.0", "private": true, "description": "A new WordPress editor experience.", "author": "The WordPress Contributors", @@ -111,7 +111,7 @@ "@octokit/rest": "16.26.0", "@octokit/types": "6.34.0", "@octokit/webhooks-types": "5.6.0", - "@playwright/test": "1.42.1", + "@playwright/test": "1.43.0", "@pmmmwh/react-refresh-webpack-plugin": "0.5.11", "@react-native/babel-preset": "0.73.10", "@react-native/metro-babel-transformer": "0.73.10", diff --git a/packages/base-styles/_default-custom-properties.scss b/packages/base-styles/_default-custom-properties.scss index 5760753c48ce8..98bb9ae75ea67 100644 --- a/packages/base-styles/_default-custom-properties.scss +++ b/packages/base-styles/_default-custom-properties.scss @@ -5,5 +5,7 @@ @include admin-scheme(#007cba); --wp-block-synced-color: #7a00df; --wp-block-synced-color--rgb: #{hex-to-rgb(#7a00df)}; - --wp-bound-block-color: #9747ff; + // This CSS variable is not used in Gutenberg project, + // but is maintained for backwards compatibility. + --wp-bound-block-color: var(--wp-block-synced-color); } diff --git a/packages/base-styles/_z-index.scss b/packages/base-styles/_z-index.scss index 945755f512ae7..b5d6beabb4f60 100644 --- a/packages/base-styles/_z-index.scss +++ b/packages/base-styles/_z-index.scss @@ -130,6 +130,7 @@ $z-layers: ( ".block-editor-block-rename-modal": 1000001, ".edit-site-list__rename-modal": 1000001, ".dataviews-action-modal": 1000001, + ".editor-action-modal": 1000001, ".editor-post-template__swap-template-modal": 1000001, ".edit-site-template-panel__replace-template-modal": 1000001, @@ -190,7 +191,6 @@ $z-layers: ( // Site editor layout ".edit-site-layout__header-container": 4, ".edit-site-layout__hub": 3, - ".edit-site-layout__header": 2, ".edit-site-page-header": 2, ".edit-site-page-content": 1, ".edit-site-patterns__header": 2, diff --git a/packages/block-editor/package.json b/packages/block-editor/package.json index dd4a58f1c91e3..5a99129760d3c 100644 --- a/packages/block-editor/package.json +++ b/packages/block-editor/package.json @@ -75,7 +75,7 @@ "postcss-prefixwrap": "^1.41.0", "postcss-urlrebase": "^1.0.0", "react-autosize-textarea": "^7.1.0", - "react-easy-crop": "^4.5.1", + "react-easy-crop": "^5.0.6", "remove-accents": "^0.5.0" }, "peerDependencies": { diff --git a/packages/block-editor/src/components/block-bindings-toolbar-indicator/style.scss b/packages/block-editor/src/components/block-bindings-toolbar-indicator/style.scss index f4fb768b6ec37..e1ce8bac6064b 100644 --- a/packages/block-editor/src/components/block-bindings-toolbar-indicator/style.scss +++ b/packages/block-editor/src/components/block-bindings-toolbar-indicator/style.scss @@ -4,7 +4,7 @@ justify-content: center; padding: 6px; svg { - fill: var(--wp-bound-block-color); + fill: var(--wp-block-synced-color); } } diff --git a/packages/block-editor/src/components/block-edit/context.js b/packages/block-editor/src/components/block-edit/context.js index e8480912a87f0..6bb1861abd0a2 100644 --- a/packages/block-editor/src/components/block-edit/context.js +++ b/packages/block-editor/src/components/block-edit/context.js @@ -7,6 +7,7 @@ export const mayDisplayControlsKey = Symbol( 'mayDisplayControls' ); export const mayDisplayParentControlsKey = Symbol( 'mayDisplayParentControls' ); export const blockEditingModeKey = Symbol( 'blockEditingMode' ); export const blockBindingsKey = Symbol( 'blockBindings' ); +export const isPreviewModeKey = Symbol( 'isPreviewMode' ); export const DEFAULT_BLOCK_EDIT_CONTEXT = { name: '', diff --git a/packages/block-editor/src/components/block-edit/index.js b/packages/block-editor/src/components/block-edit/index.js index 4e94a8a427510..57df36c7c74a0 100644 --- a/packages/block-editor/src/components/block-edit/index.js +++ b/packages/block-editor/src/components/block-edit/index.js @@ -15,6 +15,7 @@ import { mayDisplayParentControlsKey, blockEditingModeKey, blockBindingsKey, + isPreviewModeKey, } from './context'; /** @@ -31,6 +32,7 @@ export default function BlockEdit( { mayDisplayControls, mayDisplayParentControls, blockEditingMode, + isPreviewMode, // The remaining props are passed through the BlockEdit filters and are thus // public API! ...props @@ -65,6 +67,7 @@ export default function BlockEdit( { [ mayDisplayParentControlsKey ]: mayDisplayParentControls, [ blockEditingModeKey ]: blockEditingMode, [ blockBindingsKey ]: bindings, + [ isPreviewModeKey ]: isPreviewMode, } ), [ name, @@ -77,6 +80,7 @@ export default function BlockEdit( { mayDisplayParentControls, blockEditingMode, bindings, + isPreviewMode, ] ) } > diff --git a/packages/block-editor/src/components/block-list/block.js b/packages/block-editor/src/components/block-list/block.js index 8d402f162d52d..82d6aac18cb6a 100644 --- a/packages/block-editor/src/components/block-list/block.js +++ b/packages/block-editor/src/components/block-list/block.js @@ -143,6 +143,7 @@ function BlockListBlock( { mayDisplayControls={ mayDisplayControls } mayDisplayParentControls={ mayDisplayParentControls } blockEditingMode={ context.blockEditingMode } + isPreviewMode={ context.isPreviewMode } /> ); @@ -572,6 +573,7 @@ function BlockListBlockProvider( props ) { } = getSettings(); const hasLightBlockWrapper = blockType?.apiVersion > 1; const previewContext = { + isPreviewMode, blockWithoutAttributes, name: blockName, attributes, @@ -642,7 +644,9 @@ function BlockListBlockProvider( props ) { __unstableHasActiveBlockOverlayActive( clientId ) && ! isDragging(), initialPosition: - _isSelected && __unstableGetEditorMode() === 'edit' + _isSelected && + ( __unstableGetEditorMode() === 'edit' || + __unstableGetEditorMode() === 'zoom-out' ) // Don't recalculate the initialPosition when toggling in/out of zoom-out mode ? getSelectedBlocksInitialCaretPosition() : undefined, isHighlighted: isBlockHighlighted( clientId ), @@ -671,6 +675,7 @@ function BlockListBlockProvider( props ) { ); const { + isPreviewMode, // Fill values that end up as a public API and may not be defined in // preview mode. mode = 'visual', @@ -728,6 +733,7 @@ function BlockListBlockProvider( props ) { } const privateContext = { + isPreviewMode, clientId, className, index, diff --git a/packages/block-editor/src/components/block-list/block.native.js b/packages/block-editor/src/components/block-list/block.native.js index 51d66245708b7..10c550913ca31 100644 --- a/packages/block-editor/src/components/block-list/block.native.js +++ b/packages/block-editor/src/components/block-list/block.native.js @@ -674,7 +674,7 @@ const applyWithDispatch = withDispatch( ( dispatch, ownProps, registry ) => { } } }, - onReplace( blocks, indexToSelect, initialPosition ) { + onReplace( blocks, indexToSelect, initialPosition, meta ) { if ( blocks.length && ! isUnmodifiedDefaultBlock( blocks[ blocks.length - 1 ] ) @@ -685,7 +685,8 @@ const applyWithDispatch = withDispatch( ( dispatch, ownProps, registry ) => { [ ownProps.clientId ], blocks, indexToSelect, - initialPosition + initialPosition, + meta ); }, toggleSelection( selectionEnabled ) { diff --git a/packages/block-editor/src/components/block-list/use-block-props/index.js b/packages/block-editor/src/components/block-list/use-block-props/index.js index c929c1014dc03..76e07ebbf0a75 100644 --- a/packages/block-editor/src/components/block-list/use-block-props/index.js +++ b/packages/block-editor/src/components/block-list/use-block-props/index.js @@ -130,7 +130,7 @@ export function useBlockProps( props = {}, { __unstableIsHtml } = {} ) { const hasBlockBindings = !! blockEditContext[ blockBindingsKey ]; const bindingsStyle = hasBlockBindings && canBindBlock( name ) - ? { '--wp-admin-theme-color': 'var(--wp-bound-block-color)' } + ? { '--wp-admin-theme-color': 'var(--wp-block-synced-color)' } : {}; // Ensures it warns only inside the `edit` implementation for the block. diff --git a/packages/block-editor/src/components/block-list/use-block-props/use-focus-first-element.js b/packages/block-editor/src/components/block-list/use-block-props/use-focus-first-element.js index b3f844cea5429..27f72d1a100d3 100644 --- a/packages/block-editor/src/components/block-list/use-block-props/use-focus-first-element.js +++ b/packages/block-editor/src/components/block-list/use-block-props/use-focus-first-element.js @@ -28,11 +28,16 @@ import { store as blockEditorStore } from '../../../store'; */ export function useFocusFirstElement( { clientId, initialPosition } ) { const ref = useRef(); - const { isBlockSelected, isMultiSelecting } = useSelect( blockEditorStore ); + const { isBlockSelected, isMultiSelecting, __unstableGetEditorMode } = + useSelect( blockEditorStore ); useEffect( () => { // Check if the block is still selected at the time this effect runs. - if ( ! isBlockSelected( clientId ) || isMultiSelecting() ) { + if ( + ! isBlockSelected( clientId ) || + isMultiSelecting() || + __unstableGetEditorMode() === 'zoom-out' + ) { return; } @@ -80,7 +85,6 @@ export function useFocusFirstElement( { clientId, initialPosition } ) { return; } } - placeCaretAtHorizontalEdge( target, isReverse ); }, [ initialPosition, clientId ] ); diff --git a/packages/block-editor/src/components/block-patterns-paging/index.js b/packages/block-editor/src/components/block-patterns-paging/index.js index 76f99e24bf244..2ad9ff3405e20 100644 --- a/packages/block-editor/src/components/block-patterns-paging/index.js +++ b/packages/block-editor/src/components/block-patterns-paging/index.js @@ -45,6 +45,7 @@ export default function Pagination( { onClick={ () => changePage( 1 ) } disabled={ currentPage === 1 } aria-label={ __( 'First page' ) } + __experimentalIsFocusable > « @@ -53,6 +54,7 @@ export default function Pagination( { onClick={ () => changePage( currentPage - 1 ) } disabled={ currentPage === 1 } aria-label={ __( 'Previous page' ) } + __experimentalIsFocusable > ‹ @@ -75,6 +77,7 @@ export default function Pagination( { onClick={ () => changePage( currentPage + 1 ) } disabled={ currentPage === numPages } aria-label={ __( 'Next page' ) } + __experimentalIsFocusable > › @@ -84,6 +87,7 @@ export default function Pagination( { disabled={ currentPage === numPages } aria-label={ __( 'Last page' ) } size="default" + __experimentalIsFocusable > » diff --git a/packages/block-editor/src/components/button-block-appender/index.native.js b/packages/block-editor/src/components/button-block-appender/index.native.js index ff6391278d0a0..ba510d07af659 100644 --- a/packages/block-editor/src/components/button-block-appender/index.native.js +++ b/packages/block-editor/src/components/button-block-appender/index.native.js @@ -9,6 +9,7 @@ import { View } from 'react-native'; import { withPreferredColorScheme } from '@wordpress/compose'; import { Button } from '@wordpress/components'; import { Icon, plusCircleFilled } from '@wordpress/icons'; +import { useCallback } from '@wordpress/element'; /** * Internal dependencies @@ -23,6 +24,20 @@ function ButtonBlockAppender( { isFloating = false, onAddBlock, } ) { + const onAppenderPress = useCallback( + ( onToggle ) => () => { + if ( onAddBlock ) { + onAddBlock(); + return; + } + + if ( onToggle ) { + onToggle(); + } + }, + [ onAddBlock ] + ); + const appenderStyle = { ...styles.appender, ...getStylesFromColorScheme( @@ -44,7 +59,7 @@ function ButtonBlockAppender( { renderToggle={ ( { onToggle, disabled, isOpen } ) => ( - - ) } - - + ! isDistractionFree && ( + ) } notices={ } diff --git a/packages/edit-post/src/components/sidebar/settings-sidebar/index.js b/packages/edit-post/src/components/sidebar/settings-sidebar/index.js index dd1a6a048d965..27b85cffa0864 100644 --- a/packages/edit-post/src/components/sidebar/settings-sidebar/index.js +++ b/packages/edit-post/src/components/sidebar/settings-sidebar/index.js @@ -27,6 +27,7 @@ import { PostTaxonomiesPanel, privateApis as editorPrivateApis, } from '@wordpress/editor'; +import { addQueryArgs } from '@wordpress/url'; /** * Internal dependencies @@ -39,7 +40,7 @@ import { store as editPostStore } from '../../../store'; import { privateApis as componentsPrivateApis } from '@wordpress/components'; import { unlock } from '../../../lock-unlock'; -const { PostCardPanel } = unlock( editorPrivateApis ); +const { PostCardPanel, PostActions } = unlock( editorPrivateApis ); const { Tabs } = unlock( componentsPrivateApis ); const { PatternOverridesPanel } = unlock( editorPrivateApis ); @@ -53,11 +54,16 @@ export const sidebars = { block: 'edit-post/block', }; -const SidebarContent = ( { - sidebarName, - keyboardShortcut, - isEditingTemplate, -} ) => { +function onActionPerformed( actionId, items ) { + if ( actionId === 'move-to-trash' ) { + const postType = items[ 0 ].type; + document.location.href = addQueryArgs( 'edit.php', { + post_type: postType, + } ); + } +} + +const SidebarContent = ( { tabName, keyboardShortcut, isEditingTemplate } ) => { const tabListRef = useRef( null ); // Because `PluginSidebarEditPost` renders a `ComplementaryArea`, we // need to forward the `Tabs` context so it can be passed through the @@ -76,7 +82,7 @@ const SidebarContent = ( { // We are purposefully using a custom `data-tab-id` attribute here // because we don't want rely on any assumptions about `Tabs` // component internals. - ( element ) => element.getAttribute( 'data-tab-id' ) === sidebarName + ( element ) => element.getAttribute( 'data-tab-id' ) === tabName ); const activeElement = selectedTabElement?.ownerDocument.activeElement; const tabsHasFocus = tabsElements.some( ( element ) => { @@ -89,11 +95,11 @@ const SidebarContent = ( { ) { selectedTabElement?.focus(); } - }, [ sidebarName ] ); + }, [ tabName ] ); return ( @@ -113,7 +119,13 @@ const SidebarContent = ( { > - + + } + /> { ! isEditingTemplate && ( <> @@ -137,41 +149,38 @@ const SidebarContent = ( { }; const SettingsSidebar = () => { - const { - sidebarName, - isSettingsSidebarActive, - keyboardShortcut, - isEditingTemplate, - } = useSelect( ( select ) => { - // The settings sidebar is used by the edit-post/document and edit-post/block sidebars. - // sidebarName represents the sidebar that is active or that should be active when the SettingsSidebar toggle button is pressed. - // If one of the two sidebars is active the component will contain the content of that sidebar. - // When neither of the two sidebars is active we can not simply return null, because the PluginSidebarEditPost - // component, besides being used to render the sidebar, also renders the toggle button. In that case sidebarName - // should contain the sidebar that will be active when the toggle button is pressed. If a block - // is selected, that should be edit-post/block otherwise it's edit-post/document. - let sidebar = select( interfaceStore ).getActiveComplementaryArea( - editPostStore.name - ); - let isSettingsSidebar = true; - if ( ! [ sidebars.document, sidebars.block ].includes( sidebar ) ) { - isSettingsSidebar = false; - if ( select( blockEditorStore ).getBlockSelectionStart() ) { - sidebar = sidebars.block; + const { tabName, keyboardShortcut, isEditingTemplate } = useSelect( + ( select ) => { + const shortcut = select( + keyboardShortcutsStore + ).getShortcutRepresentation( 'core/edit-post/toggle-sidebar' ); + + const sidebar = select( interfaceStore ).getActiveComplementaryArea( + editPostStore.name + ); + const _isEditorSidebarOpened = [ + sidebars.block, + sidebars.document, + ].includes( sidebar ); + let _tabName = sidebar; + if ( ! _isEditorSidebarOpened ) { + _tabName = !! select( + blockEditorStore + ).getBlockSelectionStart() + ? sidebars.block + : sidebars.document; } - sidebar = sidebars.document; - } - const shortcut = select( - keyboardShortcutsStore - ).getShortcutRepresentation( 'core/edit-post/toggle-sidebar' ); - return { - sidebarName: sidebar, - isSettingsSidebarActive: isSettingsSidebar, - keyboardShortcut: shortcut, - isEditingTemplate: - select( editorStore ).getCurrentPostType() === 'wp_template', - }; - }, [] ); + + return { + tabName: _tabName, + keyboardShortcut: shortcut, + isEditingTemplate: + select( editorStore ).getCurrentPostType() === + 'wp_template', + }; + }, + [] + ); const { openGeneralSidebar } = useDispatch( editPostStore ); @@ -186,17 +195,12 @@ const SettingsSidebar = () => { return ( diff --git a/packages/edit-post/src/store/selectors.js b/packages/edit-post/src/store/selectors.js index 7110750c2034d..791b384d1e818 100644 --- a/packages/edit-post/src/store/selectors.js +++ b/packages/edit-post/src/store/selectors.js @@ -562,36 +562,55 @@ export function areMetaBoxesInitialized( state ) { */ export const getEditedPostTemplate = createRegistrySelector( ( select ) => () => { + const { + id: postId, + type: postType, + slug, + } = select( editorStore ).getCurrentPost(); + const { getSite, getEditedEntityRecord, getEntityRecords } = + select( coreStore ); + const siteSettings = getSite(); + // First check if the current page is set as the posts page. + const isPostsPage = +postId === siteSettings?.page_for_posts; + if ( isPostsPage ) { + const defaultTemplateId = select( coreStore ).getDefaultTemplateId( + { slug: 'home' } + ); + return getEditedEntityRecord( + 'postType', + 'wp_template', + defaultTemplateId + ); + } const currentTemplate = select( editorStore ).getEditedPostAttribute( 'template' ); if ( currentTemplate ) { - const templateWithSameSlug = select( coreStore ) - .getEntityRecords( 'postType', 'wp_template', { per_page: -1 } ) - ?.find( ( template ) => template.slug === currentTemplate ); + const templateWithSameSlug = getEntityRecords( + 'postType', + 'wp_template', + { per_page: -1 } + )?.find( ( template ) => template.slug === currentTemplate ); if ( ! templateWithSameSlug ) { return templateWithSameSlug; } - return select( coreStore ).getEditedEntityRecord( + return getEditedEntityRecord( 'postType', 'wp_template', templateWithSameSlug.id ); } - - const post = select( editorStore ).getCurrentPost(); let slugToCheck; // In `draft` status we might not have a slug available, so we use the `single` // post type templates slug(ex page, single-post, single-product etc..). // Pages do not need the `single` prefix in the slug to be prioritized // through template hierarchy. - if ( post.slug ) { + if ( slug ) { slugToCheck = - post.type === 'page' - ? `${ post.type }-${ post.slug }` - : `single-${ post.type }-${ post.slug }`; + postType === 'page' + ? `${ postType }-${ slug }` + : `single-${ postType }-${ slug }`; } else { - slugToCheck = - post.type === 'page' ? 'page' : `single-${ post.type }`; + slugToCheck = postType === 'page' ? 'page' : `single-${ postType }`; } const defaultTemplateId = select( coreStore ).getDefaultTemplateId( { slug: slugToCheck, diff --git a/packages/edit-post/src/style.scss b/packages/edit-post/src/style.scss index be16158bc6b04..4ba063905d8b2 100644 --- a/packages/edit-post/src/style.scss +++ b/packages/edit-post/src/style.scss @@ -77,18 +77,3 @@ body.js.block-editor-page { } @include wordpress-admin-schemes(); - -// The edit-site package adds or removes the sidebar when it's opened or closed. -// The edit-post package, however, always has the sidebar in the canvas. -// These edit-post specific rules ensures there isn't a border on the right of -// the canvas when a sidebar is visually closed. -.interface-interface-skeleton__sidebar { - border-left: none; - - .is-sidebar-opened & { - @include break-medium() { - border-left: $border-width solid $gray-200; - overflow: hidden scroll; - } - } -} diff --git a/packages/edit-site/src/components/add-new-template/index.js b/packages/edit-site/src/components/add-new-template/index.js index 1f1e855ccf38d..700f9c17de6ed 100644 --- a/packages/edit-site/src/components/add-new-template/index.js +++ b/packages/edit-site/src/components/add-new-template/index.js @@ -1,31 +1,431 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; + /** * WordPress dependencies */ -import { useSelect } from '@wordpress/data'; +import { + Button, + Modal, + __experimentalGrid as Grid, + __experimentalText as Text, + __experimentalVStack as VStack, + Flex, + Icon, +} from '@wordpress/components'; +import { decodeEntities } from '@wordpress/html-entities'; +import { useState, memo } from '@wordpress/element'; +import { useSelect, useDispatch } from '@wordpress/data'; import { store as coreStore } from '@wordpress/core-data'; +import { + archive, + blockMeta, + calendar, + category, + commentAuthorAvatar, + edit, + home, + layout, + list, + media, + notFound, + page, + pin, + verse, + search, + tag, +} from '@wordpress/icons'; +import { __, sprintf } from '@wordpress/i18n'; +import { store as noticesStore } from '@wordpress/notices'; +import { privateApis as routerPrivateApis } from '@wordpress/router'; /** * Internal dependencies */ -import NewTemplate from './new-template'; import { TEMPLATE_POST_TYPE } from '../../utils/constants'; -export default function AddNewTemplate( { - templateType = TEMPLATE_POST_TYPE, - ...props +/** + * Internal dependencies + */ +import AddCustomTemplateModalContent from './add-custom-template-modal-content'; +import { + useExistingTemplates, + useDefaultTemplateTypes, + useTaxonomiesMenuItems, + usePostTypeMenuItems, + useAuthorMenuItem, + usePostTypeArchiveMenuItems, +} from './utils'; +import AddCustomGenericTemplateModalContent from './add-custom-generic-template-modal-content'; +import { unlock } from '../../lock-unlock'; + +const { useHistory } = unlock( routerPrivateApis ); + +const DEFAULT_TEMPLATE_SLUGS = [ + 'front-page', + 'home', + 'single', + 'page', + 'index', + 'archive', + 'author', + 'category', + 'date', + 'tag', + 'search', + '404', +]; + +const TEMPLATE_ICONS = { + 'front-page': home, + home: verse, + single: pin, + page, + archive, + search, + 404: notFound, + index: list, + category, + author: commentAuthorAvatar, + taxonomy: blockMeta, + date: calendar, + tag, + attachment: media, +}; + +function TemplateListItem( { + title, + direction, + className, + description, + icon, + onClick, + children, } ) { - const postType = useSelect( - ( select ) => select( coreStore ).getPostType( templateType ), - [ templateType ] + return ( + + ); +} + +const modalContentMap = { + templatesList: 1, + customTemplate: 2, + customGenericTemplate: 3, +}; + +function NewTemplateModal( { onClose } ) { + const [ modalContent, setModalContent ] = useState( + modalContentMap.templatesList + ); + const [ entityForSuggestions, setEntityForSuggestions ] = useState( {} ); + const [ isSubmitting, setIsSubmitting ] = useState( false ); + const missingTemplates = useMissingTemplates( setEntityForSuggestions, () => + setModalContent( modalContentMap.customTemplate ) + ); + const history = useHistory(); + const { saveEntityRecord } = useDispatch( coreStore ); + const { createErrorNotice, createSuccessNotice } = + useDispatch( noticesStore ); + + const { homeUrl } = useSelect( ( select ) => { + const { + getUnstableBase, // Site index. + } = select( coreStore ); + + return { + homeUrl: getUnstableBase()?.home, + }; + }, [] ); + + const TEMPLATE_SHORT_DESCRIPTIONS = { + 'front-page': homeUrl, + date: sprintf( + // translators: %s: The homepage url. + __( 'E.g. %s' ), + homeUrl + '/' + new Date().getFullYear() + ), + }; + + async function createTemplate( template, isWPSuggestion = true ) { + if ( isSubmitting ) { + return; + } + setIsSubmitting( true ); + try { + const { title, description, slug } = template; + const newTemplate = await saveEntityRecord( + 'postType', + TEMPLATE_POST_TYPE, + { + description, + // Slugs need to be strings, so this is for template `404` + slug: slug.toString(), + status: 'publish', + title, + // This adds a post meta field in template that is part of `is_custom` value calculation. + is_wp_suggestion: isWPSuggestion, + }, + { throwOnError: true } + ); + + // Navigate to the created template editor. + history.push( { + postId: newTemplate.id, + postType: TEMPLATE_POST_TYPE, + canvas: 'edit', + } ); + + createSuccessNotice( + sprintf( + // translators: %s: Title of the created template e.g: "Category". + __( '"%s" successfully created.' ), + decodeEntities( newTemplate.title?.rendered || title ) + ), + { + type: 'snackbar', + } + ); + } catch ( error ) { + const errorMessage = + error.message && error.code !== 'unknown_error' + ? error.message + : __( 'An error occurred while creating the template.' ); + + createErrorNotice( errorMessage, { + type: 'snackbar', + } ); + } finally { + setIsSubmitting( false ); + } + } + const onModalClose = () => { + onClose(); + setModalContent( modalContentMap.templatesList ); + }; + + let modalTitle = __( 'Add template' ); + if ( modalContent === modalContentMap.customTemplate ) { + modalTitle = sprintf( + // translators: %s: Name of the post type e.g: "Post". + __( 'Add template: %s' ), + entityForSuggestions.labels.singular_name + ); + } else if ( modalContent === modalContentMap.customGenericTemplate ) { + modalTitle = __( 'Create custom template' ); + } + + return ( + + { modalContent === modalContentMap.templatesList && ( + + + { __( + 'Select what the new template should apply to:' + ) } + + { missingTemplates.map( ( template ) => { + const { title, slug, onClick } = template; + return ( + + onClick + ? onClick( template ) + : createTemplate( template ) + } + /> + ); + } ) } + + setModalContent( + modalContentMap.customGenericTemplate + ) + } + > + + { __( + 'A custom template can be manually applied to any post or page.' + ) } + + + + ) } + { modalContent === modalContentMap.customTemplate && ( + + ) } + { modalContent === modalContentMap.customGenericTemplate && ( + + ) } + ); +} + +function NewTemplate() { + const [ showModal, setShowModal ] = useState( false ); + + const { postType } = useSelect( ( select ) => { + const { getPostType } = select( coreStore ); + + return { + postType: getPostType( TEMPLATE_POST_TYPE ), + }; + }, [] ); if ( ! postType ) { return null; } - if ( templateType === TEMPLATE_POST_TYPE ) { - return ; - } + return ( + <> + + { showModal && ( + setShowModal( false ) } /> + ) } + + ); +} - return null; +function useMissingTemplates( setEntityForSuggestions, onClick ) { + const existingTemplates = useExistingTemplates(); + const defaultTemplateTypes = useDefaultTemplateTypes(); + const existingTemplateSlugs = ( existingTemplates || [] ).map( + ( { slug } ) => slug + ); + const missingDefaultTemplates = ( defaultTemplateTypes || [] ).filter( + ( template ) => + DEFAULT_TEMPLATE_SLUGS.includes( template.slug ) && + ! existingTemplateSlugs.includes( template.slug ) + ); + const onClickMenuItem = ( _entityForSuggestions ) => { + onClick?.(); + setEntityForSuggestions( _entityForSuggestions ); + }; + // We need to replace existing default template types with + // the create specific template functionality. The original + // info (title, description, etc.) is preserved in the + // used hooks. + const enhancedMissingDefaultTemplateTypes = [ ...missingDefaultTemplates ]; + const { defaultTaxonomiesMenuItems, taxonomiesMenuItems } = + useTaxonomiesMenuItems( onClickMenuItem ); + const { defaultPostTypesMenuItems, postTypesMenuItems } = + usePostTypeMenuItems( onClickMenuItem ); + + const authorMenuItem = useAuthorMenuItem( onClickMenuItem ); + [ + ...defaultTaxonomiesMenuItems, + ...defaultPostTypesMenuItems, + authorMenuItem, + ].forEach( ( menuItem ) => { + if ( ! menuItem ) { + return; + } + const matchIndex = enhancedMissingDefaultTemplateTypes.findIndex( + ( template ) => template.slug === menuItem.slug + ); + // Some default template types might have been filtered above from + // `missingDefaultTemplates` because they only check for the general + // template. So here we either replace or append the item, augmented + // with the check if it has available specific item to create a + // template for. + if ( matchIndex > -1 ) { + enhancedMissingDefaultTemplateTypes[ matchIndex ] = menuItem; + } else { + enhancedMissingDefaultTemplateTypes.push( menuItem ); + } + } ); + // Update the sort order to match the DEFAULT_TEMPLATE_SLUGS order. + enhancedMissingDefaultTemplateTypes?.sort( ( template1, template2 ) => { + return ( + DEFAULT_TEMPLATE_SLUGS.indexOf( template1.slug ) - + DEFAULT_TEMPLATE_SLUGS.indexOf( template2.slug ) + ); + } ); + const missingTemplates = [ + ...enhancedMissingDefaultTemplateTypes, + ...usePostTypeArchiveMenuItems(), + ...postTypesMenuItems, + ...taxonomiesMenuItems, + ]; + return missingTemplates; } + +export default memo( NewTemplate ); diff --git a/packages/edit-site/src/components/add-new-template/new-template.js b/packages/edit-site/src/components/add-new-template/new-template.js deleted file mode 100644 index 95bb074c66ee6..0000000000000 --- a/packages/edit-site/src/components/add-new-template/new-template.js +++ /dev/null @@ -1,429 +0,0 @@ -/** - * External dependencies - */ -import classnames from 'classnames'; - -/** - * WordPress dependencies - */ -import { - Button, - Modal, - __experimentalGrid as Grid, - __experimentalText as Text, - __experimentalVStack as VStack, - Flex, - Icon, -} from '@wordpress/components'; -import { decodeEntities } from '@wordpress/html-entities'; -import { useState } from '@wordpress/element'; -import { useSelect, useDispatch } from '@wordpress/data'; -import { store as coreStore } from '@wordpress/core-data'; -import { - archive, - blockMeta, - calendar, - category, - commentAuthorAvatar, - edit, - home, - layout, - list, - media, - notFound, - page, - plus, - pin, - verse, - search, - tag, -} from '@wordpress/icons'; -import { __, sprintf } from '@wordpress/i18n'; -import { store as noticesStore } from '@wordpress/notices'; -import { privateApis as routerPrivateApis } from '@wordpress/router'; - -/** - * Internal dependencies - */ -import { TEMPLATE_POST_TYPE } from '../../utils/constants'; - -/** - * Internal dependencies - */ -import AddCustomTemplateModalContent from './add-custom-template-modal-content'; -import { - useExistingTemplates, - useDefaultTemplateTypes, - useTaxonomiesMenuItems, - usePostTypeMenuItems, - useAuthorMenuItem, - usePostTypeArchiveMenuItems, -} from './utils'; -import AddCustomGenericTemplateModalContent from './add-custom-generic-template-modal-content'; -import TemplateActionsLoadingScreen from './template-actions-loading-screen'; -import { unlock } from '../../lock-unlock'; - -const { useHistory } = unlock( routerPrivateApis ); - -const DEFAULT_TEMPLATE_SLUGS = [ - 'front-page', - 'home', - 'single', - 'page', - 'index', - 'archive', - 'author', - 'category', - 'date', - 'tag', - 'search', - '404', -]; - -const TEMPLATE_ICONS = { - 'front-page': home, - home: verse, - single: pin, - page, - archive, - search, - 404: notFound, - index: list, - category, - author: commentAuthorAvatar, - taxonomy: blockMeta, - date: calendar, - tag, - attachment: media, -}; - -function TemplateListItem( { - title, - direction, - className, - description, - icon, - onClick, - children, -} ) { - return ( - - ); -} - -const modalContentMap = { - templatesList: 1, - customTemplate: 2, - customGenericTemplate: 3, -}; - -export default function NewTemplate( { - postType, - toggleProps, - showIcon = true, -} ) { - const [ showModal, setShowModal ] = useState( false ); - const [ modalContent, setModalContent ] = useState( - modalContentMap.templatesList - ); - const [ entityForSuggestions, setEntityForSuggestions ] = useState( {} ); - const [ isCreatingTemplate, setIsCreatingTemplate ] = useState( false ); - - const history = useHistory(); - const { saveEntityRecord } = useDispatch( coreStore ); - const { createErrorNotice, createSuccessNotice } = - useDispatch( noticesStore ); - - const { homeUrl } = useSelect( ( select ) => { - const { - getUnstableBase, // Site index. - } = select( coreStore ); - - return { - homeUrl: getUnstableBase()?.home, - }; - }, [] ); - - const TEMPLATE_SHORT_DESCRIPTIONS = { - 'front-page': homeUrl, - date: sprintf( - // translators: %s: The homepage url. - __( 'E.g. %s' ), - homeUrl + '/' + new Date().getFullYear() - ), - }; - - async function createTemplate( template, isWPSuggestion = true ) { - if ( isCreatingTemplate ) { - return; - } - setIsCreatingTemplate( true ); - try { - const { title, description, slug } = template; - const newTemplate = await saveEntityRecord( - 'postType', - TEMPLATE_POST_TYPE, - { - description, - // Slugs need to be strings, so this is for template `404` - slug: slug.toString(), - status: 'publish', - title, - // This adds a post meta field in template that is part of `is_custom` value calculation. - is_wp_suggestion: isWPSuggestion, - }, - { throwOnError: true } - ); - - // Navigate to the created template editor. - history.push( { - postId: newTemplate.id, - postType: newTemplate.type, - canvas: 'edit', - } ); - - createSuccessNotice( - sprintf( - // translators: %s: Title of the created template e.g: "Category". - __( '"%s" successfully created.' ), - decodeEntities( newTemplate.title?.rendered || title ) - ), - { - type: 'snackbar', - } - ); - } catch ( error ) { - const errorMessage = - error.message && error.code !== 'unknown_error' - ? error.message - : __( 'An error occurred while creating the template.' ); - - createErrorNotice( errorMessage, { - type: 'snackbar', - } ); - } finally { - setIsCreatingTemplate( false ); - } - } - const onModalClose = () => { - setShowModal( false ); - setModalContent( modalContentMap.templatesList ); - }; - - const missingTemplates = useMissingTemplates( setEntityForSuggestions, () => - setModalContent( modalContentMap.customTemplate ) - ); - if ( ! missingTemplates.length ) { - return null; - } - const { as: Toggle = Button, ...restToggleProps } = toggleProps ?? {}; - - let modalTitle = __( 'Add template' ); - if ( modalContent === modalContentMap.customTemplate ) { - modalTitle = sprintf( - // translators: %s: Name of the post type e.g: "Post". - __( 'Add template: %s' ), - entityForSuggestions.labels.singular_name - ); - } else if ( modalContent === modalContentMap.customGenericTemplate ) { - modalTitle = __( 'Create custom template' ); - } - return ( - <> - { isCreatingTemplate && } - setShowModal( true ) } - icon={ showIcon ? plus : null } - label={ postType.labels.add_new_item } - > - { showIcon ? null : postType.labels.add_new_item } - - { showModal && ( - - { modalContent === modalContentMap.templatesList && ( - - - { __( - 'Select what the new template should apply to:' - ) } - - { missingTemplates.map( ( template ) => { - const { title, slug, onClick } = template; - return ( - - onClick - ? onClick( template ) - : createTemplate( template ) - } - /> - ); - } ) } - - setModalContent( - modalContentMap.customGenericTemplate - ) - } - > - - { __( - 'A custom template can be manually applied to any post or page.' - ) } - - - - ) } - { modalContent === modalContentMap.customTemplate && ( - - ) } - { modalContent === - modalContentMap.customGenericTemplate && ( - - ) } - - ) } - - ); -} - -function useMissingTemplates( setEntityForSuggestions, onClick ) { - const existingTemplates = useExistingTemplates(); - const defaultTemplateTypes = useDefaultTemplateTypes(); - const existingTemplateSlugs = ( existingTemplates || [] ).map( - ( { slug } ) => slug - ); - const missingDefaultTemplates = ( defaultTemplateTypes || [] ).filter( - ( template ) => - DEFAULT_TEMPLATE_SLUGS.includes( template.slug ) && - ! existingTemplateSlugs.includes( template.slug ) - ); - const onClickMenuItem = ( _entityForSuggestions ) => { - onClick?.(); - setEntityForSuggestions( _entityForSuggestions ); - }; - // We need to replace existing default template types with - // the create specific template functionality. The original - // info (title, description, etc.) is preserved in the - // used hooks. - const enhancedMissingDefaultTemplateTypes = [ ...missingDefaultTemplates ]; - const { defaultTaxonomiesMenuItems, taxonomiesMenuItems } = - useTaxonomiesMenuItems( onClickMenuItem ); - const { defaultPostTypesMenuItems, postTypesMenuItems } = - usePostTypeMenuItems( onClickMenuItem ); - - const authorMenuItem = useAuthorMenuItem( onClickMenuItem ); - [ - ...defaultTaxonomiesMenuItems, - ...defaultPostTypesMenuItems, - authorMenuItem, - ].forEach( ( menuItem ) => { - if ( ! menuItem ) { - return; - } - const matchIndex = enhancedMissingDefaultTemplateTypes.findIndex( - ( template ) => template.slug === menuItem.slug - ); - // Some default template types might have been filtered above from - // `missingDefaultTemplates` because they only check for the general - // template. So here we either replace or append the item, augmented - // with the check if it has available specific item to create a - // template for. - if ( matchIndex > -1 ) { - enhancedMissingDefaultTemplateTypes[ matchIndex ] = menuItem; - } else { - enhancedMissingDefaultTemplateTypes.push( menuItem ); - } - } ); - // Update the sort order to match the DEFAULT_TEMPLATE_SLUGS order. - enhancedMissingDefaultTemplateTypes?.sort( ( template1, template2 ) => { - return ( - DEFAULT_TEMPLATE_SLUGS.indexOf( template1.slug ) - - DEFAULT_TEMPLATE_SLUGS.indexOf( template2.slug ) - ); - } ); - const missingTemplates = [ - ...enhancedMissingDefaultTemplateTypes, - ...usePostTypeArchiveMenuItems(), - ...postTypesMenuItems, - ...taxonomiesMenuItems, - ]; - return missingTemplates; -} diff --git a/packages/edit-site/src/components/add-new-template/style.scss b/packages/edit-site/src/components/add-new-template/style.scss index 305119eeb14e2..a1080d4d893d8 100644 --- a/packages/edit-site/src/components/add-new-template/style.scss +++ b/packages/edit-site/src/components/add-new-template/style.scss @@ -96,28 +96,6 @@ } } -.edit-site-template-actions-loading-screen-modal { - backdrop-filter: none; - background-color: transparent; - - &.is-full-screen { - background-color: $white; - box-shadow: 0 0 0 transparent; - min-width: 100%; - min-height: 100%; - } - - &__content { - display: flex; - align-items: center; - justify-content: center; - height: 100%; - position: absolute; - left: 50%; - transform: translateX(-50%); - } -} - .edit-site-add-new-template__modal { max-width: $grid-unit-80 * 13; width: calc(100% - #{$grid-unit-80}); diff --git a/packages/edit-site/src/components/add-new-template/template-actions-loading-screen.js b/packages/edit-site/src/components/add-new-template/template-actions-loading-screen.js deleted file mode 100644 index a57880d8551f7..0000000000000 --- a/packages/edit-site/src/components/add-new-template/template-actions-loading-screen.js +++ /dev/null @@ -1,23 +0,0 @@ -/** - * WordPress dependencies - */ -import { Spinner, Modal } from '@wordpress/components'; - -export default function TemplateActionsLoadingScreen() { - const baseCssClass = 'edit-site-template-actions-loading-screen-modal'; - return ( - {} } - __experimentalHideHeader - className={ baseCssClass } - > -
- -
-
- ); -} diff --git a/packages/edit-site/src/components/add-new-template/utils.js b/packages/edit-site/src/components/add-new-template/utils.js index f94c230196053..cff0cac4b64e3 100644 --- a/packages/edit-site/src/components/add-new-template/utils.js +++ b/packages/edit-site/src/components/add-new-template/utils.js @@ -14,6 +14,8 @@ import { blockMeta, post, archive } from '@wordpress/icons'; */ import { TEMPLATE_POST_TYPE } from '../../utils/constants'; +const EMPTY_OBJECT = {}; + /** * @typedef IHasNameAndId * @property {string|number} id The entity's id. @@ -622,14 +624,14 @@ const useTemplatesToExclude = ( const useEntitiesInfo = ( entityName, templatePrefixes, - additionalQueryParameters = {} + additionalQueryParameters = EMPTY_OBJECT ) => { const recordsToExcludePerEntity = useTemplatesToExclude( entityName, templatePrefixes, additionalQueryParameters ); - const entitiesInfo = useSelect( + const entitiesHasRecords = useSelect( ( select ) => { return Object.keys( templatePrefixes || {} ).reduce( ( accumulator, slug ) => { @@ -637,26 +639,42 @@ const useEntitiesInfo = ( recordsToExcludePerEntity?.[ slug ]?.map( ( { id } ) => id ) || []; - accumulator[ slug ] = { - hasEntities: !! select( coreStore ).getEntityRecords( - entityName, - slug, - { - per_page: 1, - _fields: 'id', - context: 'view', - exclude: existingEntitiesIds, - ...additionalQueryParameters[ slug ], - } - )?.length, - existingEntitiesIds, - }; + accumulator[ slug ] = !! select( + coreStore + ).getEntityRecords( entityName, slug, { + per_page: 1, + _fields: 'id', + context: 'view', + exclude: existingEntitiesIds, + ...additionalQueryParameters[ slug ], + } )?.length; return accumulator; }, {} ); }, - [ templatePrefixes, recordsToExcludePerEntity ] + [ + templatePrefixes, + recordsToExcludePerEntity, + entityName, + additionalQueryParameters, + ] ); + const entitiesInfo = useMemo( () => { + return Object.keys( templatePrefixes || {} ).reduce( + ( accumulator, slug ) => { + const existingEntitiesIds = + recordsToExcludePerEntity?.[ slug ]?.map( + ( { id } ) => id + ) || []; + accumulator[ slug ] = { + hasEntities: entitiesHasRecords[ slug ], + existingEntitiesIds, + }; + return accumulator; + }, + {} + ); + }, [ templatePrefixes, recordsToExcludePerEntity, entitiesHasRecords ] ); return entitiesInfo; }; diff --git a/packages/edit-site/src/components/block-editor/editor-canvas.js b/packages/edit-site/src/components/block-editor/editor-canvas.js index 4ef8bbbef11a2..acb136d226f02 100644 --- a/packages/edit-site/src/components/block-editor/editor-canvas.js +++ b/packages/edit-site/src/components/block-editor/editor-canvas.js @@ -144,7 +144,7 @@ function EditorCanvas( { const scale = isZoomOutMode ? ( contentWidth ) => computeIFrameScale( - { width: 1000, scale: 0.45 }, + { width: 1000, scale: 0.55 }, { width: 400, scale: 0.9 }, contentWidth ) diff --git a/packages/edit-site/src/components/editor/index.js b/packages/edit-site/src/components/editor/index.js index ef5147c35858f..ba04333b985bd 100644 --- a/packages/edit-site/src/components/editor/index.js +++ b/packages/edit-site/src/components/editor/index.js @@ -7,8 +7,16 @@ import classnames from 'classnames'; * WordPress dependencies */ import { useDispatch, useSelect } from '@wordpress/data'; -import { Notice } from '@wordpress/components'; -import { useInstanceId, useViewportMatch } from '@wordpress/compose'; +import { + Notice, + __unstableAnimatePresence as AnimatePresence, + __unstableMotion as motion, +} from '@wordpress/components'; +import { + useInstanceId, + useViewportMatch, + useReducedMotion, +} from '@wordpress/compose'; import { store as preferencesStore } from '@wordpress/preferences'; import { BlockBreadcrumb, @@ -40,6 +48,7 @@ import { SidebarInspectorFill, } from '../sidebar-edit-mode'; import CodeEditor from '../code-editor'; +import Header from '../header-edit-mode'; import KeyboardShortcutsEditMode from '../keyboard-shortcuts/edit-mode'; import WelcomeGuide from '../welcome-guide'; import StartTemplateOptions from '../start-template-options'; @@ -70,8 +79,12 @@ const interfaceLabels = { actions: __( 'Editor publish' ), /* translators: accessibility text for the editor footer landmark region. */ footer: __( 'Editor footer' ), + /* translators: accessibility text for the editor header landmark region. */ + header: __( 'Editor top bar' ), }; +const ANIMATION_DURATION = 0.25; + export default function Editor( { isLoading, onClick } ) { const { record: editedPost, @@ -82,6 +95,7 @@ export default function Editor( { isLoading, onClick } ) { const { type: editedPostType } = editedPost; const isLargeViewport = useViewportMatch( 'medium' ); + const disableMotion = useReducedMotion(); const { context, @@ -213,6 +227,35 @@ export default function Editor( { isLoading, onClick } ) { 'show-icon-labels': showIconLabels, } ) } + header={ + + { canvasMode === 'edit' && ( + +
+ + ) } + + } notices={ } content={ <> @@ -254,9 +297,7 @@ export default function Editor( { isLoading, onClick } ) { ( shouldShowListView && ) ) } sidebar={ - ! isDistractionFree && isEditMode && - isRightSidebarOpen && ! isDistractionFree && ( ) diff --git a/packages/edit-site/src/components/global-styles/background-panel.js b/packages/edit-site/src/components/global-styles/background-panel.js index e4760a810ecbc..cae8bdba61871 100644 --- a/packages/edit-site/src/components/global-styles/background-panel.js +++ b/packages/edit-site/src/components/global-styles/background-panel.js @@ -8,6 +8,11 @@ import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor'; */ import { unlock } from '../../lock-unlock'; +// Initial control values where no block style is set. +const BACKGROUND_DEFAULT_VALUES = { + backgroundSize: 'auto', +}; + const { useGlobalStyle, useGlobalSetting, @@ -29,6 +34,7 @@ export default function BackgroundPanel() { value={ style } onChange={ setStyle } settings={ settings } + defaultValues={ BACKGROUND_DEFAULT_VALUES } /> ); } diff --git a/packages/edit-site/src/components/global-styles/block-preview-panel.js b/packages/edit-site/src/components/global-styles/block-preview-panel.js index 787d8c6d47e50..64d79e3a86436 100644 --- a/packages/edit-site/src/components/global-styles/block-preview-panel.js +++ b/packages/edit-site/src/components/global-styles/block-preview-panel.js @@ -6,6 +6,11 @@ import { getBlockType, getBlockFromExample } from '@wordpress/blocks'; import { __experimentalSpacer as Spacer } from '@wordpress/components'; import { useMemo } from '@wordpress/element'; +/** + * Internal dependencies + */ +import { getVariationClassName } from './utils'; + const BlockPreviewPanel = ( { name, variation = '' } ) => { const blockExample = getBlockType( name )?.example; const blocks = useMemo( () => { @@ -19,7 +24,7 @@ const BlockPreviewPanel = ( { name, variation = '' } ) => { ...example, attributes: { ...example.attributes, - className: 'is-style-' + variation, + className: getVariationClassName( variation ), }, }; } diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/installed-fonts.js b/packages/edit-site/src/components/global-styles/font-library-modal/installed-fonts.js index abbe8d0dd0a9f..34e757a6d0186 100644 --- a/packages/edit-site/src/components/global-styles/font-library-modal/installed-fonts.js +++ b/packages/edit-site/src/components/global-styles/font-library-modal/installed-fonts.js @@ -193,7 +193,6 @@ function InstalledFonts() { ) ) } ) } - diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/style.scss b/packages/edit-site/src/components/global-styles/font-library-modal/style.scss index 794c5dc868609..4f5c34faa06ea 100644 --- a/packages/edit-site/src/components/global-styles/font-library-modal/style.scss +++ b/packages/edit-site/src/components/global-styles/font-library-modal/style.scss @@ -1,3 +1,7 @@ +// Fixed height for the modal footer. +// Ensures that the footer is always visible when the modal content is scrollable. +$footer-height: 70px; + .font-library-modal { // @todo If a new prop is added to the Modal component that constrains // the content width, we should use that prop instead of this style. @@ -14,6 +18,7 @@ .components-modal__content { padding-top: 0; + margin-bottom: $footer-height; } .font-library-modal__subtitle { @@ -46,6 +51,7 @@ position: absolute; bottom: $grid-unit-40; width: 100%; + height: $footer-height; background-color: $white; } } diff --git a/packages/edit-site/src/components/header-edit-mode/style.scss b/packages/edit-site/src/components/header-edit-mode/style.scss index 96b26368032ea..3414a2c95ea31 100644 --- a/packages/edit-site/src/components/header-edit-mode/style.scss +++ b/packages/edit-site/src/components/header-edit-mode/style.scss @@ -21,9 +21,9 @@ height: 100%; // Allow focus ring to be fully visible on furthest right button. @include break-medium() { + padding-right: var(--wp-admin-border-width-focus); // Account for the site hub, which is 60x60px. flex-basis: calc(37.5% - 60px); - padding-right: 2px; // We need this to be overflow hidden so the block toolbar can // overflow scroll. If the overflow is visible, flexbox allows // the toolbar to grow outside of the allowed container space. diff --git a/packages/edit-site/src/components/layout/animation.js b/packages/edit-site/src/components/layout/animation.js index 6de490cf9852f..0dc6c61b165aa 100644 --- a/packages/edit-site/src/components/layout/animation.js +++ b/packages/edit-site/src/components/layout/animation.js @@ -1,7 +1,7 @@ /** * External dependencies */ -import { Controller } from '@react-spring/web'; +import { Controller, easings } from '@react-spring/web'; /** * WordPress dependencies @@ -15,7 +15,7 @@ function getAbsolutePosition( element ) { }; } -const ANIMATION_DURATION = 300; +const ANIMATION_DURATION = 400; /** * Hook used to compute the styles required to move a div into a new position. @@ -65,7 +65,10 @@ function useMovingAnimation( { triggerAnimationOnChange } ) { y: 0, width: prevRect.width, height: prevRect.height, - config: { duration: ANIMATION_DURATION }, + config: { + duration: ANIMATION_DURATION, + easing: easings.easeInOutQuint, + }, onChange( { value } ) { if ( ! ref.current ) { return; diff --git a/packages/edit-site/src/components/layout/index.js b/packages/edit-site/src/components/layout/index.js index 001a4fed66784..1ff31c973a99c 100644 --- a/packages/edit-site/src/components/layout/index.js +++ b/packages/edit-site/src/components/layout/index.js @@ -38,7 +38,6 @@ import { privateApis as coreCommandsPrivateApis } from '@wordpress/core-commands import Sidebar from '../sidebar'; import ErrorBoundary from '../error-boundary'; import { store as editSiteStore } from '../../store'; -import Header from '../header-edit-mode'; import useInitEditedEntityFromURL from '../sync-state-with-url/use-init-edited-entity-from-url'; import SiteHub from '../site-hub'; import ResizableFrame from '../resizable-frame'; @@ -217,42 +216,6 @@ export default function Layout() { isTransparent={ isResizableFrameOversized } className="edit-site-layout__hub" /> - - - { canvasMode === 'edit' && ( - -
- - ) } -
@@ -291,8 +254,6 @@ export default function Layout() { ) } - - { isMobileViewport && areas.mobile && (
{ areas.mobile } @@ -358,6 +319,8 @@ export default function Layout() {
) }
+ + ); diff --git a/packages/edit-site/src/components/layout/style.scss b/packages/edit-site/src/components/layout/style.scss index 63fc992e06c23..00b67f00f6f23 100644 --- a/packages/edit-site/src/components/layout/style.scss +++ b/packages/edit-site/src/components/layout/style.scss @@ -37,18 +37,6 @@ z-index: z-index(".edit-site-layout__header-container"); } -.edit-site-layout__header { - height: $header-height; - display: flex; - z-index: z-index(".edit-site-layout__header"); - - // This is only necessary for the exit animation - .edit-site-layout:not(.is-full-canvas) & { - position: fixed; - width: 100vw; - } -} - .edit-site-layout__content { height: 100%; flex-grow: 1; @@ -272,26 +260,14 @@ div { transform: translateX(0) translateY(0) translateZ(0) !important; } - - .edit-site-layout__header { - opacity: 1 !important; - } } } - .edit-site-site-hub, - .edit-site-layout__header { + .edit-site-site-hub { position: absolute; top: 0; - z-index: z-index(".edit-site-layout__header"); - } - .edit-site-site-hub { z-index: z-index(".edit-site-layout__hub"); } - .edit-site-layout__header { - width: 100%; - } - } .edit-site-layout__area { diff --git a/packages/edit-site/src/components/page-actions/index.js b/packages/edit-site/src/components/page-actions/index.js deleted file mode 100644 index 546b90a7dd150..0000000000000 --- a/packages/edit-site/src/components/page-actions/index.js +++ /dev/null @@ -1,40 +0,0 @@ -/** - * WordPress dependencies - */ -import { __ } from '@wordpress/i18n'; -import { DropdownMenu, MenuGroup } from '@wordpress/components'; -import { moreVertical } from '@wordpress/icons'; - -/** - * Internal dependencies - */ -import TrashPageMenuItem from './trash-page-menu-item'; -import RenamePostMenuItem from '../rename-post-menu-item'; - -export default function PageActions( { - className, - onRemove, - page, - toggleProps, -} ) { - return ( - - { () => ( - - - { !! onRemove && ( - - ) } - - ) } - - ); -} diff --git a/packages/edit-site/src/components/page-actions/trash-page-menu-item.js b/packages/edit-site/src/components/page-actions/trash-page-menu-item.js deleted file mode 100644 index 4015378c48b83..0000000000000 --- a/packages/edit-site/src/components/page-actions/trash-page-menu-item.js +++ /dev/null @@ -1,61 +0,0 @@ -/** - * WordPress dependencies - */ -import { useDispatch } from '@wordpress/data'; -import { decodeEntities } from '@wordpress/html-entities'; -import { store as coreStore } from '@wordpress/core-data'; -import { __, sprintf } from '@wordpress/i18n'; -import { MenuItem } from '@wordpress/components'; -import { store as noticesStore } from '@wordpress/notices'; - -export default function TrashPageMenuItem( { page, onRemove } ) { - const { createSuccessNotice, createErrorNotice } = - useDispatch( noticesStore ); - const { deleteEntityRecord } = useDispatch( coreStore ); - - if ( page?.status === 'trash' ) { - return; - } - - const title = decodeEntities( - typeof page.title === 'string' ? page.title : page.title.rendered - ); - - async function removePage() { - try { - await deleteEntityRecord( - 'postType', - 'page', - page.id, - {}, - { throwOnError: true } - ); - createSuccessNotice( - sprintf( - /* translators: The page's title. */ - __( '"%s" moved to the Trash.' ), - title - ), - { - type: 'snackbar', - id: 'edit-site-page-trashed', - } - ); - onRemove?.(); - } catch ( error ) { - const errorMessage = - error.message && error.code !== 'unknown_error' - ? error.message - : __( - 'An error occurred while moving the page to the trash.' - ); - - createErrorNotice( errorMessage, { type: 'snackbar' } ); - } - } - return ( - removePage() } isDestructive> - { __( 'Move to Trash' ) } - - ); -} diff --git a/packages/edit-site/src/components/page-pages/index.js b/packages/edit-site/src/components/page-pages/index.js index 0c38d369e54f7..dce6f748b651d 100644 --- a/packages/edit-site/src/components/page-pages/index.js +++ b/packages/edit-site/src/components/page-pages/index.js @@ -189,6 +189,16 @@ function FeaturedImage( { item, viewType } ) { ); } +const PAGE_ACTIONS = [ + 'edit-post', + 'view-post', + 'restore', + 'permanently-delete', + 'view-post-revisions', + 'rename-post', + 'move-to-trash', +]; + export default function PagePages() { const postType = 'page'; const [ view, setView ] = useView( postType ); @@ -353,7 +363,7 @@ export default function PagePages() { }, [ history ] ); - const actions = usePostActions( onActionPerformed ); + const actions = usePostActions( onActionPerformed, PAGE_ACTIONS ); const onChangeView = useCallback( ( newView ) => { if ( newView.type !== view.type ) { diff --git a/packages/edit-site/src/components/page-patterns/dataviews-pattern-actions.js b/packages/edit-site/src/components/page-patterns/dataviews-pattern-actions.js index 9030b3bbc73c7..50baed5658e92 100644 --- a/packages/edit-site/src/components/page-patterns/dataviews-pattern-actions.js +++ b/packages/edit-site/src/components/page-patterns/dataviews-pattern-actions.js @@ -23,6 +23,7 @@ import { useState } from '@wordpress/element'; import { store as noticesStore } from '@wordpress/notices'; import { decodeEntities } from '@wordpress/html-entities'; import { store as reusableBlocksStore } from '@wordpress/reusable-blocks'; +import { store as editorStore } from '@wordpress/editor'; import { privateApis as routerPrivateApis } from '@wordpress/router'; import { privateApis as patternsPrivateApis } from '@wordpress/patterns'; @@ -198,7 +199,7 @@ export const deleteAction = { useDispatch( reusableBlocksStore ); const { createErrorNotice, createSuccessNotice } = useDispatch( noticesStore ); - const { removeTemplates } = unlock( useDispatch( editSiteStore ) ); + const { removeTemplates } = unlock( useDispatch( editorStore ) ); const deletePattern = async () => { const promiseResult = await Promise.allSettled( diff --git a/packages/edit-site/src/components/page-patterns/index.js b/packages/edit-site/src/components/page-patterns/index.js index 7f43bb1b4c3bb..6fa84df7a0171 100644 --- a/packages/edit-site/src/components/page-patterns/index.js +++ b/packages/edit-site/src/components/page-patterns/index.js @@ -1,3 +1,8 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; + /** * WordPress dependencies */ @@ -30,6 +35,9 @@ import { lockSmall, } from '@wordpress/icons'; import { usePrevious } from '@wordpress/compose'; +import { useEntityRecords } from '@wordpress/core-data'; +import { privateApis as editorPrivateApis } from '@wordpress/editor'; +import { privateApis as routerPrivateApis } from '@wordpress/router'; /** * Internal dependencies @@ -39,6 +47,7 @@ import Page from '../page'; import { LAYOUT_GRID, LAYOUT_TABLE, + LAYOUT_LIST, PATTERN_TYPES, TEMPLATE_PART_POST_TYPE, PATTERN_SYNC_TYPES, @@ -59,10 +68,13 @@ import { unlock } from '../../lock-unlock'; import usePatterns from './use-patterns'; import PatternsHeader from './header'; import { useLink } from '../routes/link'; +import { useAddedBy } from '../page-templates-template-parts/hooks'; const { ExperimentalBlockEditorProvider, useGlobalStyle } = unlock( blockEditorPrivateApis ); +const { usePostActions } = unlock( editorPrivateApis ); +const { useHistory } = unlock( routerPrivateApis ); const templatePartIcons = { header, footer, uncategorized }; const EMPTY_ARRAY = []; @@ -142,11 +154,6 @@ function Preview( { item, categoryId, viewType } ) { ariaDescriptions.push( item.description ); } - if ( isNonUserPattern ) { - ariaDescriptions.push( - __( 'Theme & plugin patterns cannot be edited.' ) - ); - } const [ backgroundColor ] = useGlobalStyle( 'color.background' ); const { onClick } = useLink( { postType: item.type, @@ -157,47 +164,79 @@ function Preview( { item, categoryId, viewType } ) { } ); return ( - <> -
+ + `${ descriptionId }-${ index }` + ) + .join( ' ' ) + : undefined + } > - - `${ descriptionId }-${ index }` - ) - .join( ' ' ) - : undefined - } - > - { isEmpty && isTemplatePart && __( 'Empty template part' ) } - { isEmpty && ! isTemplatePart && __( 'Empty pattern' ) } - { ! isEmpty && ( - - - - ) } - -
- { ariaDescriptions.map( ( ariaDescription, index ) => ( + { isEmpty && isTemplatePart && __( 'Empty template part' ) } + { isEmpty && ! isTemplatePart && __( 'Empty pattern' ) } + { ! isEmpty && ( + + + + ) } + + { ! isNonUserPattern && + ariaDescriptions.map( ( ariaDescription, index ) => ( + + ) ) } + + ); +} + +function Author( { item, viewType } ) { + const [ isImageLoaded, setIsImageLoaded ] = useState( false ); + const { text, icon, imageUrl } = useAddedBy( item.type, item.id ); + const withIcon = viewType !== LAYOUT_LIST; + + return ( + + { withIcon && imageUrl && ( - ) ) } - + ) } + { withIcon && ! imageUrl && ( +
+ +
+ ) } + { text } +
); } @@ -289,6 +328,24 @@ export default function DataviewsPatterns() { syncStatus: viewSyncStatus, } ); + + const { records } = useEntityRecords( 'postType', TEMPLATE_PART_POST_TYPE, { + per_page: -1, + } ); + const authors = useMemo( () => { + if ( ! records ) { + return EMPTY_ARRAY; + } + const authorsSet = new Set(); + records.forEach( ( template ) => { + authorsSet.add( template.author_text ); + } ); + return Array.from( authorsSet ).map( ( author ) => ( { + value: author, + label: author, + } ) ); + }, [ records ] ); + const fields = useMemo( () => { const _fields = [ { @@ -313,6 +370,7 @@ export default function DataviewsPatterns() { enableHiding: false, }, ]; + if ( type === PATTERN_TYPES.theme ) { _fields.push( { header: __( 'Sync status' ), @@ -342,9 +400,26 @@ export default function DataviewsPatterns() { }, enableSorting: false, } ); + } else if ( type === TEMPLATE_PART_POST_TYPE ) { + _fields.push( { + header: __( 'Author' ), + id: 'author', + getValue: ( { item } ) => item.templatePart.author_text, + render: ( { item } ) => { + return ; + }, + type: ENUMERATION_TYPE, + elements: authors, + filterBy: { + isPrimary: true, + }, + width: '1%', + } ); } + return _fields; - }, [ view.type, categoryId, type ] ); + }, [ view.type, categoryId, type, authors ] ); + // Reset the page number when the category changes. useEffect( () => { if ( previousCategoryId !== categoryId ) { @@ -352,25 +427,55 @@ export default function DataviewsPatterns() { } }, [ categoryId, previousCategoryId ] ); const { data, paginationInfo } = useMemo( () => { - // Since filters are applied server-side, - // we need to remove them from the view + // Search is managed server-side as well as filters for patterns. + // However, the author filter in template parts is done client-side. const viewWithoutFilters = { ...view }; delete viewWithoutFilters.search; - viewWithoutFilters.filters = []; + if ( type !== TEMPLATE_PART_POST_TYPE ) { + viewWithoutFilters.filters = []; + } return filterSortAndPaginate( patterns, viewWithoutFilters, fields ); - }, [ patterns, view, fields ] ); + }, [ patterns, view, fields, type ] ); - const actions = useMemo( - () => [ + const history = useHistory(); + const onActionPerformed = useCallback( + ( actionId, items ) => { + if ( actionId === 'edit-post' ) { + const post = items[ 0 ]; + history.push( { + postId: post.id, + postType: post.type, + categoryId, + categoryType: type, + canvas: 'edit', + } ); + } + }, + [ history ] + ); + const [ editAction, viewRevisionsAction ] = usePostActions( + onActionPerformed, + [ 'edit-post', 'view-post-revisions' ] + ); + const actions = useMemo( () => { + if ( type === TEMPLATE_PART_POST_TYPE ) { + return [ + editAction, + renameAction, + duplicateTemplatePartAction, + viewRevisionsAction, + resetAction, + deleteAction, + ]; + } + return [ renameAction, duplicatePatternAction, - duplicateTemplatePartAction, exportJSONaction, resetAction, deleteAction, - ], - [] - ); + ]; + }, [ type, editAction, viewRevisionsAction ] ); const onChangeView = useCallback( ( newView ) => { if ( newView.type !== view.type ) { diff --git a/packages/edit-site/src/components/page-patterns/use-patterns.js b/packages/edit-site/src/components/page-patterns/use-patterns.js index b16fddbf81034..8b6c6f26ddd44 100644 --- a/packages/edit-site/src/components/page-patterns/use-patterns.js +++ b/packages/edit-site/src/components/page-patterns/use-patterns.js @@ -40,6 +40,7 @@ const templatePartToPattern = ( templatePart ) => ( { name: createTemplatePartId( templatePart.theme, templatePart.slug ), title: decodeEntities( templatePart.title.rendered ), type: templatePart.type, + _links: templatePart._links, templatePart, } ); diff --git a/packages/edit-site/src/components/page-templates-template-parts/actions.js b/packages/edit-site/src/components/page-templates-template-parts/actions.js deleted file mode 100644 index d4038b5efeb58..0000000000000 --- a/packages/edit-site/src/components/page-templates-template-parts/actions.js +++ /dev/null @@ -1,275 +0,0 @@ -/** - * WordPress dependencies - */ -import { __, sprintf, _n } from '@wordpress/i18n'; -import { useDispatch } from '@wordpress/data'; -import { useState } from '@wordpress/element'; -import { store as coreStore } from '@wordpress/core-data'; -import { store as noticesStore } from '@wordpress/notices'; -import { decodeEntities } from '@wordpress/html-entities'; -import { - Button, - TextControl, - __experimentalText as Text, - __experimentalHStack as HStack, - __experimentalVStack as VStack, -} from '@wordpress/components'; - -/** - * Internal dependencies - */ -import { unlock } from '../../lock-unlock'; -import { store as editSiteStore } from '../../store'; -import isTemplateRevertable from '../../utils/is-template-revertable'; -import isTemplateRemovable from '../../utils/is-template-removable'; -import { TEMPLATE_POST_TYPE } from '../../utils/constants'; - -export const resetTemplateAction = { - id: 'reset-template', - label: __( 'Reset' ), - isEligible: isTemplateRevertable, - supportsBulk: true, - hideModalHeader: true, - RenderModal: ( { items, closeModal, onPerform } ) => { - const { revertTemplate } = useDispatch( editSiteStore ); - const { saveEditedEntityRecord } = useDispatch( coreStore ); - const { createSuccessNotice, createErrorNotice } = - useDispatch( noticesStore ); - const onConfirm = async () => { - try { - for ( const template of items ) { - await revertTemplate( template, { - allowUndo: false, - } ); - await saveEditedEntityRecord( - 'postType', - template.type, - template.id - ); - } - - createSuccessNotice( - items.length > 1 - ? sprintf( - /* translators: The number of items. */ - __( '%s items reset.' ), - items.length - ) - : sprintf( - /* translators: The template/part's name. */ - __( '"%s" reset.' ), - decodeEntities( items[ 0 ].title.rendered ) - ), - { - type: 'snackbar', - id: 'edit-site-template-reverted', - } - ); - } catch ( error ) { - let fallbackErrorMessage; - if ( items[ 0 ].type === TEMPLATE_POST_TYPE ) { - fallbackErrorMessage = - items.length === 1 - ? __( - 'An error occurred while reverting the template.' - ) - : __( - 'An error occurred while reverting the templates.' - ); - } else { - fallbackErrorMessage = - items.length === 1 - ? __( - 'An error occurred while reverting the template part.' - ) - : __( - 'An error occurred while reverting the template parts.' - ); - } - const errorMessage = - error.message && error.code !== 'unknown_error' - ? error.message - : fallbackErrorMessage; - - createErrorNotice( errorMessage, { type: 'snackbar' } ); - } - }; - return ( - - - { __( 'Reset to default and clear all customizations?' ) } - - - - - - - ); - }, -}; - -export const deleteTemplateAction = { - id: 'delete-template', - label: __( 'Delete' ), - isEligible: isTemplateRemovable, - supportsBulk: true, - hideModalHeader: true, - RenderModal: ( { items: templates, closeModal, onPerform } ) => { - const { removeTemplates } = unlock( useDispatch( editSiteStore ) ); - return ( - - - { templates.length > 1 - ? sprintf( - // translators: %d: number of items to delete. - _n( - 'Delete %d item?', - 'Delete %d items?', - templates.length - ), - templates.length - ) - : sprintf( - // translators: %s: The template or template part's titles - __( 'Delete "%s"?' ), - decodeEntities( - templates?.[ 0 ]?.title?.rendered - ) - ) } - - - - - - - ); - }, -}; - -export const renameTemplateAction = { - id: 'rename-template', - label: __( 'Rename' ), - isEligible: ( template ) => { - // We can only remove templates or template parts that can be removed. - // Additionally in the case of templates, we can only remove custom templates. - if ( - ! isTemplateRemovable( template ) || - ( template.type === TEMPLATE_POST_TYPE && ! template.is_custom ) - ) { - return false; - } - return true; - }, - RenderModal: ( { items: templates, closeModal } ) => { - const template = templates[ 0 ]; - const title = decodeEntities( template.title.rendered ); - const [ editedTitle, setEditedTitle ] = useState( title ); - const { - editEntityRecord, - __experimentalSaveSpecifiedEntityEdits: saveSpecifiedEntityEdits, - } = useDispatch( coreStore ); - const { createSuccessNotice, createErrorNotice } = - useDispatch( noticesStore ); - async function onTemplateRename( event ) { - event.preventDefault(); - try { - await editEntityRecord( - 'postType', - template.type, - template.id, - { - title: editedTitle, - } - ); - // Update state before saving rerenders the list. - setEditedTitle( '' ); - closeModal(); - // Persist edited entity. - await saveSpecifiedEntityEdits( - 'postType', - template.type, - template.id, - [ 'title' ], // Only save title to avoid persisting other edits. - { - throwOnError: true, - } - ); - createSuccessNotice( - template.type === TEMPLATE_POST_TYPE - ? __( 'Template renamed.' ) - : __( 'Template part renamed.' ), - { - type: 'snackbar', - } - ); - } catch ( error ) { - const fallbackErrorMessage = - template.type === TEMPLATE_POST_TYPE - ? __( 'An error occurred while renaming the template.' ) - : __( - 'An error occurred while renaming the template part.' - ); - const errorMessage = - error.message && error.code !== 'unknown_error' - ? error.message - : fallbackErrorMessage; - - createErrorNotice( errorMessage, { type: 'snackbar' } ); - } - } - return ( -
- - - - - - - -
- ); - }, -}; diff --git a/packages/edit-site/src/components/page-templates-template-parts/add-new-template-part.js b/packages/edit-site/src/components/page-templates-template-parts/add-new-template-part.js index 8d46010587242..2a7bbda971013 100644 --- a/packages/edit-site/src/components/page-templates-template-parts/add-new-template-part.js +++ b/packages/edit-site/src/components/page-templates-template-parts/add-new-template-part.js @@ -4,7 +4,7 @@ import { privateApis as routerPrivateApis } from '@wordpress/router'; import { useSelect } from '@wordpress/data'; import { store as coreStore } from '@wordpress/core-data'; -import { useState } from '@wordpress/element'; +import { useState, memo } from '@wordpress/element'; import { Button } from '@wordpress/components'; /** @@ -17,7 +17,7 @@ import { TEMPLATE_PART_POST_TYPE } from '../../utils/constants'; const { useHistory } = unlock( routerPrivateApis ); -export default function AddNewTemplatePart() { +function AddNewTemplatePart() { const { canCreate, postType } = useSelect( ( select ) => { const { supportsTemplatePartsMode } = select( editSiteStore ).getSettings(); @@ -58,3 +58,5 @@ export default function AddNewTemplatePart() { ); } + +export default memo( AddNewTemplatePart ); diff --git a/packages/edit-site/src/components/page-templates-template-parts/index.js b/packages/edit-site/src/components/page-templates-template-parts/index.js index aaf424123c3de..598637f98b811 100644 --- a/packages/edit-site/src/components/page-templates-template-parts/index.js +++ b/packages/edit-site/src/components/page-templates-template-parts/index.js @@ -42,11 +42,7 @@ import { LAYOUT_TABLE, LAYOUT_LIST, } from '../../utils/constants'; -import { - resetTemplateAction, - deleteTemplateAction, - renameTemplateAction, -} from './actions'; + import usePatternSettings from '../page-patterns/use-pattern-settings'; import { unlock } from '../../lock-unlock'; import AddNewTemplatePart from './add-new-template-part'; @@ -201,6 +197,14 @@ function Preview( { item, viewType } ) { ); } +const TEMPLATE_ACTIONS = [ + 'edit-post', + 'reset-template', + 'rename-template', + 'view-post-revisions', + 'delete-template', +]; + export default function PageTemplatesTemplateParts( { postType } ) { const { params } = useLocation(); const { activeView = 'all', layout } = params; @@ -361,20 +365,8 @@ export default function PageTemplatesTemplateParts( { postType } ) { }, [ history ] ); - const [ editAction, viewRevisionsAction ] = usePostActions( - onActionPerformed, - [ 'edit-post', 'view-post-revisions' ] - ); - const actions = useMemo( - () => [ - editAction, - resetTemplateAction, - renameTemplateAction, - viewRevisionsAction, - deleteTemplateAction, - ], - [ editAction, viewRevisionsAction ] - ); + + const actions = usePostActions( onActionPerformed, TEMPLATE_ACTIONS ); const onChangeView = useCallback( ( newView ) => { @@ -407,11 +399,7 @@ export default function PageTemplatesTemplateParts( { postType } ) { } actions={ postType === TEMPLATE_POST_TYPE ? ( - + ) : ( ) diff --git a/packages/edit-site/src/components/sidebar-edit-mode/index.js b/packages/edit-site/src/components/sidebar-edit-mode/index.js index f3cb7cc9dae0e..38b2ca0665cc4 100644 --- a/packages/edit-site/src/components/sidebar-edit-mode/index.js +++ b/packages/edit-site/src/components/sidebar-edit-mode/index.js @@ -33,11 +33,7 @@ const { Slot: InspectorSlot, Fill: InspectorFill } = createSlotFill( ); export const SidebarInspectorFill = InspectorFill; -const FillContents = ( { - sidebarName, - isEditingPage, - supportsGlobalStyles, -} ) => { +const FillContents = ( { tabName, isEditingPage, supportsGlobalStyles } ) => { const tabListRef = useRef( null ); // Because `DefaultSidebar` renders a `ComplementaryArea`, we // need to forward the `Tabs` context so it can be passed through the @@ -56,7 +52,7 @@ const FillContents = ( { // We are purposefully using a custom `data-tab-id` attribute here // because we don't want rely on any assumptions about `Tabs` // component internals. - ( element ) => element.getAttribute( 'data-tab-id' ) === sidebarName + ( element ) => element.getAttribute( 'data-tab-id' ) === tabName ); const activeElement = selectedTabElement?.ownerDocument.activeElement; const tabsHasFocus = tabsElements.some( ( element ) => { @@ -69,12 +65,12 @@ const FillContents = ( { ) { selectedTabElement?.focus(); } - }, [ sidebarName ] ); + }, [ tabName ] ); return ( <> { - const _sidebar = + const sidebar = select( interfaceStore ).getActiveComplementaryArea( STORE_NAME ); const _isEditorSidebarOpened = [ SIDEBAR_BLOCK, SIDEBAR_TEMPLATE, - ].includes( _sidebar ); - const { getCanvasMode } = unlock( select( editSiteStore ) ); + ].includes( sidebar ); + let _tabName = sidebar; + if ( ! _isEditorSidebarOpened ) { + _tabName = !! select( blockEditorStore ).getBlockSelectionStart() + ? SIDEBAR_BLOCK + : SIDEBAR_TEMPLATE; + } return { - sidebar: _sidebar, + tabName: _tabName, isEditorSidebarOpened: _isEditorSidebarOpened, hasBlockSelection: !! select( blockEditorStore ).getBlockSelectionStart(), supportsGlobalStyles: select( coreStore ).getCurrentTheme()?.is_block_theme, isEditingPage: select( editSiteStore ).isPage(), - isEditorOpen: getCanvasMode() === 'edit', }; }, [] ); const { enableComplementaryArea } = useDispatch( interfaceStore ); @@ -157,11 +156,6 @@ export function SidebarComplementaryAreaFills() { enableComplementaryArea, ] ); - let sidebarName = sidebar; - if ( ! isEditorSidebarOpened ) { - sidebarName = hasBlockSelection ? SIDEBAR_BLOCK : SIDEBAR_TEMPLATE; - } - // `newSelectedTabId` could technically be falsey if no tab is selected (i.e. // the initial render) or when we don't want a tab displayed (i.e. the // sidebar is closed). These cases should both be covered by the `!!` check @@ -177,19 +171,12 @@ export function SidebarComplementaryAreaFills() { return ( diff --git a/packages/edit-site/src/components/sidebar-edit-mode/page-panels/index.js b/packages/edit-site/src/components/sidebar-edit-mode/page-panels/index.js index 0a004cfc7a792..021050d2c18f5 100644 --- a/packages/edit-site/src/components/sidebar-edit-mode/page-panels/index.js +++ b/packages/edit-site/src/components/sidebar-edit-mode/page-panels/index.js @@ -16,40 +16,58 @@ import { privateApis as editorPrivateApis, } from '@wordpress/editor'; import { privateApis as routerPrivateApis } from '@wordpress/router'; +import { useCallback } from '@wordpress/element'; /** * Internal dependencies */ import { store as editSiteStore } from '../../../store'; -import PageActions from '../../page-actions'; import PageContent from './page-content'; import PageSummary from './page-summary'; import { unlock } from '../../../lock-unlock'; -const { PostCardPanel } = unlock( editorPrivateApis ); +const { PostCardPanel, PostActions } = unlock( editorPrivateApis ); const { useHistory } = unlock( routerPrivateApis ); export default function PagePanels() { - const { hasResolved, page, renderingMode } = useSelect( ( select ) => { - const { getEditedPostContext } = select( editSiteStore ); - const { getEditedEntityRecord, hasFinishedResolution } = - select( coreStore ); - const { getRenderingMode } = select( editorStore ); - const context = getEditedPostContext(); - const queryArgs = [ 'postType', context.postType, context.postId ]; - return { - hasResolved: hasFinishedResolution( - 'getEditedEntityRecord', - queryArgs - ), - page: getEditedEntityRecord( ...queryArgs ), - renderingMode: getRenderingMode(), - }; - }, [] ); - const history = useHistory(); + const { id, type, hasResolved, status, date, password, renderingMode } = + useSelect( ( select ) => { + const { getEditedPostContext } = select( editSiteStore ); + const { getEditedEntityRecord, hasFinishedResolution } = + select( coreStore ); + const { getRenderingMode } = select( editorStore ); + const context = getEditedPostContext(); + const queryArgs = [ 'postType', context.postType, context.postId ]; + const page = getEditedEntityRecord( ...queryArgs ); + return { + hasResolved: hasFinishedResolution( + 'getEditedEntityRecord', + queryArgs + ), + id: page?.id, + type: page?.type, + status: page?.status, + date: page?.date, + password: page?.password, + renderingMode: getRenderingMode(), + }; + }, [] ); - const { id, type, status, date, password } = page; + const history = useHistory(); + const onActionPerformed = useCallback( + ( actionId, items ) => { + if ( actionId === 'move-to-trash' ) { + history.push( { + path: '/' + items[ 0 ].type, + postId: undefined, + postType: undefined, + canvas: 'view', + } ); + } + }, + [ history ] + ); if ( ! hasResolved ) { return null; @@ -59,16 +77,7 @@ export default function PagePanels() { <> { - history.push( { - path: '/page', - } ); - } } - /> + } /> diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-page/index.js b/packages/edit-site/src/components/sidebar-navigation-screen-page/index.js index 10ceccd994966..3ac1e00127c0f 100644 --- a/packages/edit-site/src/components/sidebar-navigation-screen-page/index.js +++ b/packages/edit-site/src/components/sidebar-navigation-screen-page/index.js @@ -15,8 +15,9 @@ import { pencil } from '@wordpress/icons'; import { __unstableStripHTML as stripHTML } from '@wordpress/dom'; import { escapeAttribute } from '@wordpress/escape-html'; import { safeDecodeURIComponent, filterURLForDisplay } from '@wordpress/url'; -import { useEffect } from '@wordpress/element'; +import { useEffect, useCallback } from '@wordpress/element'; import { privateApis as routerPrivateApis } from '@wordpress/router'; +import { privateApis as editorPrivateApis } from '@wordpress/editor'; /** * Internal dependencies @@ -26,17 +27,16 @@ import { unlock } from '../../lock-unlock'; import { store as editSiteStore } from '../../store'; import SidebarButton from '../sidebar-button'; import PageDetails from './page-details'; -import PageActions from '../page-actions'; import SidebarNavigationScreenDetailsFooter from '../sidebar-navigation-screen-details-footer'; const { useHistory } = unlock( routerPrivateApis ); +const { PostActions } = unlock( editorPrivateApis ); export default function SidebarNavigationScreenPage( { backPath } ) { const { setCanvasMode } = unlock( useDispatch( editSiteStore ) ); const history = useHistory(); const { params: { postId }, - goTo, } = useNavigator(); const { record, hasResolved } = useEntityRecord( 'postType', @@ -82,6 +82,20 @@ export default function SidebarNavigationScreenPage( { backPath } ) { } }, [ hasResolved, history ] ); + const onActionPerformed = useCallback( + ( actionId, items ) => { + if ( actionId === 'move-to-trash' ) { + history.push( { + path: '/' + items[ 0 ].type, + postId: undefined, + postType: undefined, + canvas: 'view', + } ); + } + }, + [ history ] + ); + const featureImageAltText = featuredMediaAltText ? decodeEntities( featuredMediaAltText ) : decodeEntities( record?.title?.rendered || __( 'Featured image' ) ); @@ -94,13 +108,7 @@ export default function SidebarNavigationScreenPage( { backPath } ) { ) } actions={ <> - { - goTo( '/page' ); - } } - /> + setCanvasMode( 'edit' ) } label={ __( 'Edit' ) } diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-page/style.scss b/packages/edit-site/src/components/sidebar-navigation-screen-page/style.scss index 66efe31726e8b..ad8677886f7c6 100644 --- a/packages/edit-site/src/components/sidebar-navigation-screen-page/style.scss +++ b/packages/edit-site/src/components/sidebar-navigation-screen-page/style.scss @@ -61,3 +61,10 @@ fill: $alert-green; } } + +.edit-site-sidebar-navigation-screen__actions .editor-all-actions-button { + color: inherit; + &:active { + color: inherit; + } +} diff --git a/packages/edit-site/src/components/sidebar/style.scss b/packages/edit-site/src/components/sidebar/style.scss index ef24b0d4b8cf6..9bbe11f44ee7b 100644 --- a/packages/edit-site/src/components/sidebar/style.scss +++ b/packages/edit-site/src/components/sidebar/style.scss @@ -13,10 +13,3 @@ // This matches the logo padding padding: 0 $grid-unit-15; } - -.edit-site-sidebar__footer { - border-top: 1px solid $gray-800; - flex-shrink: 0; - margin: 0 $canvas-padding; - padding: $canvas-padding 0; -} diff --git a/packages/edit-site/src/components/sync-state-with-url/use-init-edited-entity-from-url.js b/packages/edit-site/src/components/sync-state-with-url/use-init-edited-entity-from-url.js index 51dfff47b7967..8cfb0bca716f2 100644 --- a/packages/edit-site/src/components/sync-state-with-url/use-init-edited-entity-from-url.js +++ b/packages/edit-site/src/components/sync-state-with-url/use-init-edited-entity-from-url.js @@ -28,42 +28,48 @@ const postTypesWithoutParentTemplate = [ ]; function useResolveEditedEntityAndContext( { path, postId, postType } ) { - const { hasLoadedAllDependencies, homepageId, url, frontPageTemplateId } = - useSelect( ( select ) => { - const { getSite, getUnstableBase, getEntityRecords } = - select( coreDataStore ); - const siteData = getSite(); - const base = getUnstableBase(); - const templates = getEntityRecords( - 'postType', - TEMPLATE_POST_TYPE, - { - per_page: -1, - } + const { + hasLoadedAllDependencies, + homepageId, + postsPageId, + url, + frontPageTemplateId, + } = useSelect( ( select ) => { + const { getSite, getUnstableBase, getEntityRecords } = + select( coreDataStore ); + const siteData = getSite(); + const base = getUnstableBase(); + const templates = getEntityRecords( 'postType', TEMPLATE_POST_TYPE, { + per_page: -1, + } ); + const _homepageId = + siteData?.show_on_front === 'page' && + [ 'number', 'string' ].includes( typeof siteData.page_on_front ) && + !! +siteData.page_on_front // We also need to check if it's not zero(`0`). + ? siteData.page_on_front.toString() + : null; + const _postsPageId = + siteData?.show_on_front === 'page' && + [ 'number', 'string' ].includes( typeof siteData.page_for_posts ) + ? siteData.page_for_posts.toString() + : null; + let _frontPageTemplateId; + if ( templates ) { + const frontPageTemplate = templates.find( + ( t ) => t.slug === 'front-page' ); - let _frontPateTemplateId; - if ( templates ) { - const frontPageTemplate = templates.find( - ( t ) => t.slug === 'front-page' - ); - _frontPateTemplateId = frontPageTemplate - ? frontPageTemplate.id - : false; - } - - return { - hasLoadedAllDependencies: !! base && !! siteData, - homepageId: - siteData?.show_on_front === 'page' && - [ 'number', 'string' ].includes( - typeof siteData.page_on_front - ) - ? siteData.page_on_front.toString() - : null, - url: base?.home, - frontPageTemplateId: _frontPateTemplateId, - }; - }, [] ); + _frontPageTemplateId = frontPageTemplate + ? frontPageTemplate.id + : false; + } + return { + hasLoadedAllDependencies: !! base && !! siteData, + homepageId: _homepageId, + postsPageId: _postsPageId, + url: base?.home, + frontPageTemplateId: _frontPageTemplateId, + }; + }, [] ); /** * This is a hook that recreates the logic to resolve a template for a given WordPress postID postTypeId @@ -114,6 +120,14 @@ function useResolveEditedEntityAndContext( { path, postId, postType } ) { if ( ! editedEntity ) { return undefined; } + // Check if the current page is the posts page. + if ( + postTypeToResolve === 'page' && + postsPageId === postIdToResolve + ) { + return __experimentalGetTemplateForLink( editedEntity.link ) + ?.id; + } // First see if the post/page has an assigned template and fetch it. const currentTemplateSlug = editedEntity.template; if ( currentTemplateSlug ) { @@ -177,6 +191,7 @@ function useResolveEditedEntityAndContext( { path, postId, postType } ) { }, [ homepageId, + postsPageId, hasLoadedAllDependencies, url, postId, diff --git a/packages/edit-site/src/components/welcome-guide/style.scss b/packages/edit-site/src/components/welcome-guide/style.scss index e9e8b46a8aff2..4ac45ed199386 100644 --- a/packages/edit-site/src/components/welcome-guide/style.scss +++ b/packages/edit-site/src/components/welcome-guide/style.scss @@ -1,7 +1,7 @@ .edit-site-welcome-guide { width: 312px; - &.guide-editor .edit-site-welcome-guide__image + &.guide-editor .edit-site-welcome-guide__image, &.guide-styles .edit-site-welcome-guide__image { background: #00a0d2; } diff --git a/packages/edit-site/src/store/actions.js b/packages/edit-site/src/store/actions.js index dfe8f81ca21cc..2806568d627c8 100644 --- a/packages/edit-site/src/store/actions.js +++ b/packages/edit-site/src/store/actions.js @@ -1,12 +1,8 @@ /** * WordPress dependencies */ -import apiFetch from '@wordpress/api-fetch'; -import { parse, __unstableSerializeAndClean } from '@wordpress/blocks'; +import { parse } from '@wordpress/blocks'; import deprecated from '@wordpress/deprecated'; -import { addQueryArgs } from '@wordpress/url'; -import { __ } from '@wordpress/i18n'; -import { store as noticesStore } from '@wordpress/notices'; import { store as coreStore } from '@wordpress/core-data'; import { store as interfaceStore } from '@wordpress/interface'; import { store as blockEditorStore } from '@wordpress/block-editor'; @@ -17,13 +13,12 @@ import { store as preferencesStore } from '@wordpress/preferences'; * Internal dependencies */ import { STORE_NAME as editSiteStoreName } from './constants'; -import isTemplateRevertable from '../utils/is-template-revertable'; import { TEMPLATE_POST_TYPE, TEMPLATE_PART_POST_TYPE, NAVIGATION_POST_TYPE, } from '../utils/constants'; -import { removeTemplates } from './private-actions'; +import { unlock } from '../lock-unlock'; /** * Dispatches an action that toggles a feature flag. @@ -133,9 +128,13 @@ export const addTemplate = * * @param {Object} template The template object. */ -export const removeTemplate = ( template ) => { - return removeTemplates( [ template ] ); -}; +export const removeTemplate = + ( template ) => + ( { registry } ) => { + return unlock( registry.dispatch( editorStore ) ).removeTemplates( [ + template, + ] ); + }; /** * Action that sets a template part. @@ -345,130 +344,14 @@ export function setIsSaveViewOpened( isOpen ) { * reverting the template. Default true. */ export const revertTemplate = - ( template, { allowUndo = true } = {} ) => - async ( { registry } ) => { - const noticeId = 'edit-site-template-reverted'; - registry.dispatch( noticesStore ).removeNotice( noticeId ); - if ( ! isTemplateRevertable( template ) ) { - registry - .dispatch( noticesStore ) - .createErrorNotice( __( 'This template is not revertable.' ), { - type: 'snackbar', - } ); - return; - } - - try { - const templateEntityConfig = registry - .select( coreStore ) - .getEntityConfig( 'postType', template.type ); - - if ( ! templateEntityConfig ) { - registry - .dispatch( noticesStore ) - .createErrorNotice( - __( - 'The editor has encountered an unexpected error. Please reload.' - ), - { type: 'snackbar' } - ); - return; - } - - const fileTemplatePath = addQueryArgs( - `${ templateEntityConfig.baseURL }/${ template.id }`, - { context: 'edit', source: 'theme' } - ); - - const fileTemplate = await apiFetch( { path: fileTemplatePath } ); - if ( ! fileTemplate ) { - registry - .dispatch( noticesStore ) - .createErrorNotice( - __( - 'The editor has encountered an unexpected error. Please reload.' - ), - { type: 'snackbar' } - ); - return; - } - - const serializeBlocks = ( { - blocks: blocksForSerialization = [], - } ) => __unstableSerializeAndClean( blocksForSerialization ); - - const edited = registry - .select( coreStore ) - .getEditedEntityRecord( - 'postType', - template.type, - template.id - ); - - // We are fixing up the undo level here to make sure we can undo - // the revert in the header toolbar correctly. - registry.dispatch( coreStore ).editEntityRecord( - 'postType', - template.type, - template.id, - { - content: serializeBlocks, // Required to make the `undo` behave correctly. - blocks: edited.blocks, // Required to revert the blocks in the editor. - source: 'custom', // required to avoid turning the editor into a dirty state - }, - { - undoIgnore: true, // Required to merge this edit with the last undo level. - } - ); - - const blocks = parse( fileTemplate?.content?.raw ); - registry - .dispatch( coreStore ) - .editEntityRecord( 'postType', template.type, fileTemplate.id, { - content: serializeBlocks, - blocks, - source: 'theme', - } ); - - if ( allowUndo ) { - const undoRevert = () => { - registry - .dispatch( coreStore ) - .editEntityRecord( - 'postType', - template.type, - edited.id, - { - content: serializeBlocks, - blocks: edited.blocks, - source: 'custom', - } - ); - }; - - registry - .dispatch( noticesStore ) - .createSuccessNotice( __( 'Template reset.' ), { - type: 'snackbar', - id: noticeId, - actions: [ - { - label: __( 'Undo' ), - onClick: undoRevert, - }, - ], - } ); - } - } catch ( error ) { - const errorMessage = - error.message && error.code !== 'unknown_error' - ? error.message - : __( 'Template revert failed. Please reload.' ); - registry - .dispatch( noticesStore ) - .createErrorNotice( errorMessage, { type: 'snackbar' } ); - } + ( template, options ) => + ( { registry } ) => { + return unlock( registry.dispatch( editorStore ) ).revertTemplate( + template, + options + ); }; + /** * Action that opens an editor sidebar. * diff --git a/packages/edit-site/src/store/private-actions.js b/packages/edit-site/src/store/private-actions.js index fd23903a6a05e..0aaee3def948c 100644 --- a/packages/edit-site/src/store/private-actions.js +++ b/packages/edit-site/src/store/private-actions.js @@ -4,15 +4,6 @@ import { store as blockEditorStore } from '@wordpress/block-editor'; import { store as preferencesStore } from '@wordpress/preferences'; import { store as editorStore } from '@wordpress/editor'; -import { store as coreStore } from '@wordpress/core-data'; -import { store as noticesStore } from '@wordpress/notices'; -import { __, sprintf } from '@wordpress/i18n'; -import { decodeEntities } from '@wordpress/html-entities'; - -/** - * Internal dependencies - */ -import { TEMPLATE_POST_TYPE } from '../utils/constants'; /** * Action that switches the canvas mode. @@ -62,127 +53,3 @@ export const setEditorCanvasContainerView = view, } ); }; - -/** - * Action that removes an array of templates. - * - * @param {Array} items An array of template or template part objects to remove. - */ -export const removeTemplates = - ( items ) => - async ( { registry } ) => { - const isTemplate = items[ 0 ].type === TEMPLATE_POST_TYPE; - const promiseResult = await Promise.allSettled( - items.map( ( item ) => { - return registry - .dispatch( coreStore ) - .deleteEntityRecord( - 'postType', - item.type, - item.id, - { force: true }, - { throwOnError: true } - ); - } ) - ); - - // If all the promises were fulfilled with sucess. - if ( promiseResult.every( ( { status } ) => status === 'fulfilled' ) ) { - let successMessage; - - if ( items.length === 1 ) { - // Depending on how the entity was retrieved its title might be - // an object or simple string. - const title = - typeof items[ 0 ].title === 'string' - ? items[ 0 ].title - : items[ 0 ].title?.rendered; - successMessage = sprintf( - /* translators: The template/part's name. */ - __( '"%s" deleted.' ), - decodeEntities( title ) - ); - } else { - successMessage = isTemplate - ? __( 'Templates deleted.' ) - : __( 'Template parts deleted.' ); - } - - registry - .dispatch( noticesStore ) - .createSuccessNotice( successMessage, { - type: 'snackbar', - id: 'site-editor-template-deleted-success', - } ); - } else { - // If there was at lease one failure. - let errorMessage; - // If we were trying to delete a single template. - if ( promiseResult.length === 1 ) { - if ( promiseResult[ 0 ].reason?.message ) { - errorMessage = promiseResult[ 0 ].reason.message; - } else { - errorMessage = isTemplate - ? __( 'An error occurred while deleting the template.' ) - : __( - 'An error occurred while deleting the template part.' - ); - } - // If we were trying to delete a multiple templates - } else { - const errorMessages = new Set(); - const failedPromises = promiseResult.filter( - ( { status } ) => status === 'rejected' - ); - for ( const failedPromise of failedPromises ) { - if ( failedPromise.reason?.message ) { - errorMessages.add( failedPromise.reason.message ); - } - } - if ( errorMessages.size === 0 ) { - errorMessage = isTemplate - ? __( - 'An error occurred while deleting the templates.' - ) - : __( - 'An error occurred while deleting the template parts.' - ); - } else if ( errorMessages.size === 1 ) { - errorMessage = isTemplate - ? sprintf( - /* translators: %s: an error message */ - __( - 'An error occurred while deleting the templates: %s' - ), - [ ...errorMessages ][ 0 ] - ) - : sprintf( - /* translators: %s: an error message */ - __( - 'An error occurred while deleting the template parts: %s' - ), - [ ...errorMessages ][ 0 ] - ); - } else { - errorMessage = isTemplate - ? sprintf( - /* translators: %s: a list of comma separated error messages */ - __( - 'Some errors occurred while deleting the templates: %s' - ), - [ ...errorMessages ].join( ',' ) - ) - : sprintf( - /* translators: %s: a list of comma separated error messages */ - __( - 'Some errors occurred while deleting the template parts: %s' - ), - [ ...errorMessages ].join( ',' ) - ); - } - } - registry - .dispatch( noticesStore ) - .createErrorNotice( errorMessage, { type: 'snackbar' } ); - } - }; diff --git a/packages/edit-widgets/src/components/layout/interface.js b/packages/edit-widgets/src/components/layout/interface.js index 987e3868de133..ee46251eca224 100644 --- a/packages/edit-widgets/src/components/layout/interface.js +++ b/packages/edit-widgets/src/components/layout/interface.js @@ -96,11 +96,7 @@ function Interface( { blockEditorSettings } ) { } } header={
} secondarySidebar={ hasSecondarySidebar && } - sidebar={ - hasSidebarEnabled && ( - - ) - } + sidebar={ } content={ <> { + const { revertTemplate } = unlock( useDispatch( editorStore ) ); + const { saveEditedEntityRecord } = useDispatch( coreStore ); + const { createSuccessNotice, createErrorNotice } = + useDispatch( noticesStore ); + const onConfirm = async () => { + try { + for ( const template of items ) { + await revertTemplate( template, { + allowUndo: false, + } ); + await saveEditedEntityRecord( + 'postType', + template.type, + template.id + ); + } + + createSuccessNotice( + items.length > 1 + ? sprintf( + /* translators: The number of items. */ + __( '%s items reset.' ), + items.length + ) + : sprintf( + /* translators: The template/part's name. */ + __( '"%s" reset.' ), + decodeEntities( items[ 0 ].title.rendered ) + ), + { + type: 'snackbar', + id: 'edit-site-template-reverted', + } + ); + } catch ( error ) { + let fallbackErrorMessage; + if ( items[ 0 ].type === TEMPLATE_POST_TYPE ) { + fallbackErrorMessage = + items.length === 1 + ? __( + 'An error occurred while reverting the template.' + ) + : __( + 'An error occurred while reverting the templates.' + ); + } else { + fallbackErrorMessage = + items.length === 1 + ? __( + 'An error occurred while reverting the template part.' + ) + : __( + 'An error occurred while reverting the template parts.' + ); + } + const errorMessage = + error.message && error.code !== 'unknown_error' + ? error.message + : fallbackErrorMessage; + + createErrorNotice( errorMessage, { type: 'snackbar' } ); + } + }; + return ( + + + { __( 'Reset to default and clear all customizations?' ) } + + + + + + + ); + }, +}; + +/** + * Check if a template is removable. + * Copy from packages/edit-site/src/utils/is-template-removable.js. + * + * @param {Object} template The template entity to check. + * @return {boolean} Whether the template is revertable. + */ +function isTemplateRemovable( template ) { + if ( ! template ) { + return false; + } + + return ( + template.source === TEMPLATE_ORIGINS.custom && ! template.has_theme_file + ); +} + +const deleteTemplateAction = { + id: 'delete-template', + label: __( 'Delete' ), + isEligible: isTemplateRemovable, + supportsBulk: true, + hideModalHeader: true, + RenderModal: ( { items: templates, closeModal, onActionPerformed } ) => { + const { removeTemplates } = unlock( useDispatch( editorStore ) ); + return ( + + + { templates.length > 1 + ? sprintf( + // translators: %d: number of items to delete. + _n( + 'Delete %d item?', + 'Delete %d items?', + templates.length + ), + templates.length + ) + : sprintf( + // translators: %s: The template or template part's titles + __( 'Delete "%s"?' ), + decodeEntities( + templates?.[ 0 ]?.title?.rendered + ) + ) } + + + + + + + ); + }, +}; + +const renameTemplateAction = { + id: 'rename-template', + label: __( 'Rename' ), + isEligible: ( template ) => { + // We can only remove templates or template parts that can be removed. + // Additionally in the case of templates, we can only remove custom templates. + if ( + ! isTemplateRemovable( template ) || + ( template.type === TEMPLATE_POST_TYPE && ! template.is_custom ) + ) { + return false; + } + return true; + }, + RenderModal: ( { items: templates, closeModal } ) => { + const template = templates[ 0 ]; + const title = decodeEntities( template.title.rendered ); + const [ editedTitle, setEditedTitle ] = useState( title ); + const { + editEntityRecord, + __experimentalSaveSpecifiedEntityEdits: saveSpecifiedEntityEdits, + } = useDispatch( coreStore ); + const { createSuccessNotice, createErrorNotice } = + useDispatch( noticesStore ); + async function onTemplateRename( event ) { + event.preventDefault(); + try { + await editEntityRecord( + 'postType', + template.type, + template.id, + { + title: editedTitle, + } + ); + // Update state before saving rerenders the list. + setEditedTitle( '' ); + closeModal(); + // Persist edited entity. + await saveSpecifiedEntityEdits( + 'postType', + template.type, + template.id, + [ 'title' ], // Only save title to avoid persisting other edits. + { + throwOnError: true, + } + ); + createSuccessNotice( + template.type === TEMPLATE_POST_TYPE + ? __( 'Template renamed.' ) + : __( 'Template part renamed.' ), + { + type: 'snackbar', + } + ); + } catch ( error ) { + const fallbackErrorMessage = + template.type === TEMPLATE_POST_TYPE + ? __( 'An error occurred while renaming the template.' ) + : __( + 'An error occurred while renaming the template part.' + ); + const errorMessage = + error.message && error.code !== 'unknown_error' + ? error.message + : fallbackErrorMessage; + + createErrorNotice( errorMessage, { type: 'snackbar' } ); + } + } + return ( +
+ + + + + + + +
+ ); + }, +}; + export function usePostActions( onActionPerformed, actionIds = null ) { const permanentlyDeletePostAction = usePermanentlyDeletePostAction(); const restorePostAction = useRestorePostAction(); @@ -499,11 +774,14 @@ export function usePostActions( onActionPerformed, actionIds = null ) { // By default, return all actions... const defaultActions = [ editPostAction, + resetTemplateAction, viewPostAction, restorePostAction, + deleteTemplateAction, permanentlyDeletePostAction, postRevisionsAction, renamePostAction, + renameTemplateAction, trashPostAction, ]; diff --git a/packages/editor/src/components/post-actions/index.js b/packages/editor/src/components/post-actions/index.js new file mode 100644 index 0000000000000..b3a6cee47d31f --- /dev/null +++ b/packages/editor/src/components/post-actions/index.js @@ -0,0 +1,207 @@ +/** + * WordPress dependencies + */ +import { useSelect } from '@wordpress/data'; +import { store as coreStore } from '@wordpress/core-data'; +import { useMemo, useState, Fragment, Children } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; +import { + privateApis as componentsPrivateApis, + Button, + Modal, +} from '@wordpress/components'; +import { moreVertical } from '@wordpress/icons'; + +/** + * Internal dependencies + */ +import { unlock } from '../../lock-unlock'; +import { usePostActions } from './actions'; +import { store as editorStore } from '../../store'; +import { + TEMPLATE_POST_TYPE, + TEMPLATE_PART_POST_TYPE, + PATTERN_POST_TYPE, +} from '../../store/constants'; + +const { + DropdownMenuV2: DropdownMenu, + DropdownMenuGroupV2: DropdownMenuGroup, + DropdownMenuItemV2: DropdownMenuItem, + DropdownMenuItemLabelV2: DropdownMenuItemLabel, + DropdownMenuSeparatorV2: DropdownMenuSeparator, + kebabCase, +} = unlock( componentsPrivateApis ); + +const POST_ACTIONS_WHILE_EDITING = [ + 'view-post', + 'view-post-revisions', + 'rename-post', + 'move-to-trash', +]; + +export default function PostActions( { onActionPerformed } ) { + const { postType, postId } = useSelect( ( select ) => { + const { getCurrentPostType, getCurrentPostId } = select( editorStore ); + return { + postType: getCurrentPostType(), + postId: getCurrentPostId(), + }; + } ); + const actions = usePostActions( + onActionPerformed, + POST_ACTIONS_WHILE_EDITING + ); + const item = useSelect( + ( select ) => { + const { getEditedEntityRecord } = select( coreStore ); + return getEditedEntityRecord( 'postType', postType, postId ); + }, + [ postType, postId ] + ); + + const { primaryActions, secondaryActions } = useMemo( () => { + return actions.reduce( + ( accumulator, action ) => { + if ( action.isEligible && ! action.isEligible( item ) ) { + return accumulator; + } + if ( action.isPrimary ) { + accumulator.primaryActions.push( action ); + } else { + accumulator.secondaryActions.push( action ); + } + return accumulator; + }, + { primaryActions: [], secondaryActions: [] } + ); + }, [ actions, item ] ); + if ( + [ + TEMPLATE_POST_TYPE, + TEMPLATE_PART_POST_TYPE, + PATTERN_POST_TYPE, + ].includes( postType ) + ) { + return null; + } + return ( + + } + placement="bottom-end" + > + + { !! primaryActions.length && ( + + ) } + { !! secondaryActions.length && ( + + ) } + + + ); +} + +// From now on all the functions on this file are copied as from the dataviews packages, +// The editor packages should not be using the dataviews packages directly, +// and the dataviews package should not be using the editor packages directly, +// so duplicating the code here seems like the least bad option. + +// Copied as is from packages/dataviews/src/item-actions.js +function DropdownMenuItemTrigger( { action, onClick } ) { + return ( + + { action.label } + + ); +} + +// Copied as is from packages/dataviews/src/item-actions.js +function ActionWithModal( { action, item, ActionTrigger } ) { + const [ isModalOpen, setIsModalOpen ] = useState( false ); + const actionTriggerProps = { + action, + onClick: () => setIsModalOpen( true ), + }; + const { RenderModal, hideModalHeader } = action; + return ( + <> + + { isModalOpen && ( + { + setIsModalOpen( false ); + } } + overlayClassName={ `editor-action-modal editor-action-modal__${ kebabCase( + action.id + ) }` } + > + setIsModalOpen( false ) } + /> + + ) } + + ); +} + +// Copied as is from packages/dataviews/src/view-table.js +function WithDropDownMenuSeparators( { children } ) { + return Children.toArray( children ) + .filter( Boolean ) + .map( ( child, i ) => ( + + { i > 0 && } + { child } + + ) ); +} + +// Copied as is from packages/dataviews/src/item-actions.js +function ActionsDropdownMenuGroup( { actions, item } ) { + return ( + + { actions.map( ( action ) => { + if ( action.RenderModal ) { + return ( + + ); + } + return ( + action.callback( [ item ] ) } + /> + ); + } ) } + + ); +} diff --git a/packages/editor/src/components/post-actions/style.scss b/packages/editor/src/components/post-actions/style.scss new file mode 100644 index 0000000000000..2e33975685a8b --- /dev/null +++ b/packages/editor/src/components/post-actions/style.scss @@ -0,0 +1,3 @@ +.editor-action-modal { + z-index: z-index(".editor-action-modal"); +} diff --git a/packages/editor/src/components/post-url/index.js b/packages/editor/src/components/post-url/index.js index 6c58a707ca874..2c10206e9b20e 100644 --- a/packages/editor/src/components/post-url/index.js +++ b/packages/editor/src/components/post-url/index.js @@ -6,122 +6,152 @@ import { safeDecodeURIComponent, cleanForSlug } from '@wordpress/url'; import { useState } from '@wordpress/element'; import { __experimentalInspectorPopoverHeader as InspectorPopoverHeader } from '@wordpress/block-editor'; import { __ } from '@wordpress/i18n'; -import { TextControl, ExternalLink } from '@wordpress/components'; +import { + ExternalLink, + Button, + __experimentalInputControl as InputControl, + __experimentalInputControlPrefixWrapper as InputControlPrefixWrapper, + __experimentalVStack as VStack, +} from '@wordpress/components'; +import { store as noticesStore } from '@wordpress/notices'; +import { copySmall } from '@wordpress/icons'; import { store as coreStore } from '@wordpress/core-data'; +import { useCopyToClipboard } from '@wordpress/compose'; /** * Internal dependencies */ +import { usePostURLLabel } from './label'; import { store as editorStore } from '../../store'; export default function PostURL( { onClose } ) { - const { - isEditable, - postSlug, - viewPostLabel, - postLink, - permalinkPrefix, - permalinkSuffix, - } = useSelect( ( select ) => { - const post = select( editorStore ).getCurrentPost(); - const postTypeSlug = select( editorStore ).getCurrentPostType(); - const postType = select( coreStore ).getPostType( postTypeSlug ); - const permalinkParts = select( editorStore ).getPermalinkParts(); - const hasPublishAction = post?._links?.[ 'wp:action-publish' ] ?? false; - - return { - isEditable: - select( editorStore ).isPermalinkEditable() && hasPublishAction, - postSlug: safeDecodeURIComponent( - select( editorStore ).getEditedPostSlug() - ), - viewPostLabel: postType?.labels.view_item, - postLink: post.link, - permalinkPrefix: permalinkParts?.prefix, - permalinkSuffix: permalinkParts?.suffix, - }; - }, [] ); + const { isEditable, postSlug, postLink, permalinkPrefix, permalinkSuffix } = + useSelect( ( select ) => { + const post = select( editorStore ).getCurrentPost(); + const postTypeSlug = select( editorStore ).getCurrentPostType(); + const postType = select( coreStore ).getPostType( postTypeSlug ); + const permalinkParts = select( editorStore ).getPermalinkParts(); + const hasPublishAction = + post?._links?.[ 'wp:action-publish' ] ?? false; + return { + isEditable: + select( editorStore ).isPermalinkEditable() && + hasPublishAction, + postSlug: safeDecodeURIComponent( + select( editorStore ).getEditedPostSlug() + ), + viewPostLabel: postType?.labels.view_item, + postLink: post.link, + permalinkPrefix: permalinkParts?.prefix, + permalinkSuffix: permalinkParts?.suffix, + }; + }, [] ); const { editPost } = useDispatch( editorStore ); - + const { createNotice } = useDispatch( noticesStore ); const [ forceEmptyField, setForceEmptyField ] = useState( false ); - + const postUrlLabel = usePostURLLabel(); + const copyButtonRef = useCopyToClipboard( postUrlLabel, () => { + createNotice( 'info', __( 'Copied URL to clipboard.' ), { + isDismissible: true, + type: 'snackbar', + } ); + } ); return (
- - { isEditable && ( - - { __( 'The last part of the URL.' ) }{ ' ' } - - { __( 'Learn more.' ) } - - - } - onChange={ ( newValue ) => { - editPost( { slug: newValue } ); - // When we delete the field the permalink gets - // reverted to the original value. - // The forceEmptyField logic allows the user to have - // the field temporarily empty while typing. - if ( ! newValue ) { - if ( ! forceEmptyField ) { - setForceEmptyField( true ); + + + { isEditable && ( +
+ { __( 'Customize the last part of the URL. ' ) } + + { __( 'Learn more.' ) } + +
+ ) } +
+ { isEditable && ( + + / + } - return; - } - if ( forceEmptyField ) { - setForceEmptyField( false ); - } - } } - onBlur={ ( event ) => { - editPost( { - slug: cleanForSlug( event.target.value ), - } ); - if ( forceEmptyField ) { - setForceEmptyField( false ); - } - } } - /> - ) } - { isEditable && ( -

- { viewPostLabel ?? __( 'View post' ) } -

- ) } -

- - { isEditable ? ( - <> - - { permalinkPrefix } - - - { postSlug } - - - { permalinkSuffix } - - - ) : ( - postLink + suffix={ +

+
); } diff --git a/packages/editor/src/components/post-url/panel.js b/packages/editor/src/components/post-url/panel.js index 1a64ae7096df7..3e5ea6cd8ba47 100644 --- a/packages/editor/src/components/post-url/panel.js +++ b/packages/editor/src/components/post-url/panel.js @@ -2,16 +2,18 @@ * WordPress dependencies */ import { useMemo, useState } from '@wordpress/element'; +import { useSelect } from '@wordpress/data'; import { Dropdown, Button } from '@wordpress/components'; import { __, sprintf } from '@wordpress/i18n'; +import { safeDecodeURIComponent } from '@wordpress/url'; /** * Internal dependencies */ import PostURLCheck from './check'; import PostURL from './index'; -import { usePostURLLabel } from './label'; import PostPanelRow from '../post-panel-row'; +import { store as editorStore } from '../../store'; export default function PostURLPanel() { // Use internal state instead of a ref to make sure that the component @@ -19,13 +21,20 @@ export default function PostURLPanel() { const [ popoverAnchor, setPopoverAnchor ] = useState( null ); // Memoize popoverProps to avoid returning a new object every time. const popoverProps = useMemo( - () => ( { anchor: popoverAnchor, placement: 'bottom-end' } ), + () => ( { + // Anchor the popover to the middle of the entire row so that it doesn't + // move around when the label changes. + anchor: popoverAnchor, + placement: 'left-start', + offset: 36, + shift: true, + } ), [ popoverAnchor ] ); return ( - + select( editorStore ).getEditedPostSlug(), + [] + ); + const decodedSlug = safeDecodeURIComponent( slug ); return ( ); } diff --git a/packages/editor/src/components/post-url/style.scss b/packages/editor/src/components/post-url/style.scss index 4a3e8e1b39c9f..c622cfce33f90 100644 --- a/packages/editor/src/components/post-url/style.scss +++ b/packages/editor/src/components/post-url/style.scss @@ -17,19 +17,21 @@ margin: $grid-unit-10; } -.editor-post-url__link-label { - font-size: $default-font-size; - font-weight: 400; - margin: 0; -} - /* rtl:begin:ignore */ .editor-post-url__link { direction: ltr; word-break: break-word; + margin-top: $grid-unit-05; + color: $gray-700; } /* rtl:end:ignore */ .editor-post-url__link-slug { font-weight: 600; } + +// TODO: This might indicate a need to update the InputControl itself, as +// there is no way currently to control the padding when adding prefix/suffix. +.editor-post-url__input input.components-input-control__input { + padding-inline-start: 0 !important; +} diff --git a/packages/editor/src/components/provider/index.js b/packages/editor/src/components/provider/index.js index 8a65a4ff2cb08..5d4b2c0079a74 100644 --- a/packages/editor/src/components/provider/index.js +++ b/packages/editor/src/components/provider/index.js @@ -167,7 +167,8 @@ export const ExperimentalEditorProvider = withRegistryProvider( const blockEditorSettings = useBlockEditorSettings( editorSettings, type, - id + id, + mode ); const [ blocks, onInput, onChange ] = useBlockEditorProps( post, diff --git a/packages/editor/src/components/provider/use-block-editor-settings.js b/packages/editor/src/components/provider/use-block-editor-settings.js index 2f8bf920a4829..bcd0885614183 100644 --- a/packages/editor/src/components/provider/use-block-editor-settings.js +++ b/packages/editor/src/components/provider/use-block-editor-settings.js @@ -12,7 +12,10 @@ import { __ } from '@wordpress/i18n'; import { store as preferencesStore } from '@wordpress/preferences'; import { useViewportMatch } from '@wordpress/compose'; import { store as blocksStore } from '@wordpress/blocks'; -import { privateApis } from '@wordpress/block-editor'; +import { + privateApis, + store as blockEditorStore, +} from '@wordpress/block-editor'; /** * Internal dependencies @@ -20,7 +23,7 @@ import { privateApis } from '@wordpress/block-editor'; import inserterMediaCategories from '../media-categories'; import { mediaUpload } from '../../utils'; import { store as editorStore } from '../../store'; -import { unlock } from '../../lock-unlock'; +import { lock, unlock } from '../../lock-unlock'; const EMPTY_BLOCKS_LIST = []; @@ -70,6 +73,7 @@ const BLOCK_EDITOR_SETTINGS = [ 'postContentAttributes', 'postsPerPage', 'readOnly', + 'sectionRootClientId', 'styles', 'titlePlaceholder', 'supportsLayout', @@ -85,13 +89,14 @@ const BLOCK_EDITOR_SETTINGS = [ /** * React hook used to compute the block editor settings to use for the post editor. * - * @param {Object} settings EditorProvider settings prop. - * @param {string} postType Editor root level post type. - * @param {string} postId Editor root level post ID. + * @param {Object} settings EditorProvider settings prop. + * @param {string} postType Editor root level post type. + * @param {string} postId Editor root level post ID. + * @param {string} renderingMode Editor rendering mode. * * @return {Object} Block Editor Settings. */ -function useBlockEditorSettings( settings, postType, postId ) { +function useBlockEditorSettings( settings, postType, postId, renderingMode ) { const isLargeViewport = useViewportMatch( 'medium' ); const { allowRightClickOverrides, @@ -108,6 +113,7 @@ function useBlockEditorSettings( settings, postType, postId ) { pageForPosts, userPatternCategories, restBlockPatternCategories, + sectionRootClientId, } = useSelect( ( select ) => { const { @@ -119,10 +125,25 @@ function useBlockEditorSettings( settings, postType, postId ) { } = select( coreStore ); const { get } = select( preferencesStore ); const { getBlockTypes } = select( blocksStore ); + const { getBlocksByName, getBlockAttributes } = + select( blockEditorStore ); const siteSettings = canUser( 'read', 'settings' ) ? getEntityRecord( 'root', 'site' ) : undefined; + function getSectionRootBlock() { + if ( renderingMode === 'template-locked' ) { + return getBlocksByName( 'core/post-content' )?.[ 0 ] ?? ''; + } + + return ( + getBlocksByName( 'core/group' ).find( + ( clientId ) => + getBlockAttributes( clientId )?.tagName === 'main' + ) ?? '' + ); + } + return { allowRightClickOverrides: get( 'core', @@ -146,9 +167,10 @@ function useBlockEditorSettings( settings, postType, postId ) { pageForPosts: siteSettings?.page_for_posts, userPatternCategories: getUserPatternCategories(), restBlockPatternCategories: getBlockPatternCategories(), + sectionRootClientId: getSectionRootBlock(), }; }, - [ postType, postId, isLargeViewport ] + [ postType, postId, isLargeViewport, renderingMode ] ); const settingsBlockPatterns = @@ -230,8 +252,8 @@ function useBlockEditorSettings( settings, postType, postId ) { const forceDisableFocusMode = settings.focusMode === false; - return useMemo( - () => ( { + return useMemo( () => { + const blockEditorSettings = { ...Object.fromEntries( Object.entries( settings ).filter( ( [ key ] ) => BLOCK_EDITOR_SETTINGS.includes( key ) @@ -278,30 +300,34 @@ function useBlockEditorSettings( settings, postType, postId ) { ? [ [ 'core/navigation', {}, [] ] ] : settings.template, __experimentalSetIsInserterOpened: setIsInserterOpened, - } ), - [ - allowedBlockTypes, - allowRightClickOverrides, - focusMode, - forceDisableFocusMode, - hasFixedToolbar, - isDistractionFree, - keepCaretInsideBlock, - settings, - hasUploadPermissions, - userPatternCategories, - blockPatterns, - blockPatternCategories, - canUseUnfilteredHTML, - undo, - createPageEntity, - userCanCreatePages, - pageOnFront, - pageForPosts, - postType, - setIsInserterOpened, - ] - ); + }; + lock( blockEditorSettings, { + sectionRootClientId, + } ); + return blockEditorSettings; + }, [ + allowedBlockTypes, + allowRightClickOverrides, + focusMode, + forceDisableFocusMode, + hasFixedToolbar, + isDistractionFree, + keepCaretInsideBlock, + settings, + hasUploadPermissions, + userPatternCategories, + blockPatterns, + blockPatternCategories, + canUseUnfilteredHTML, + undo, + createPageEntity, + userCanCreatePages, + pageOnFront, + pageForPosts, + postType, + setIsInserterOpened, + sectionRootClientId, + ] ); } export default useBlockEditorSettings; diff --git a/packages/editor/src/private-apis.js b/packages/editor/src/private-apis.js index ea42d6ad5fde5..4bdcbb1042b22 100644 --- a/packages/editor/src/private-apis.js +++ b/packages/editor/src/private-apis.js @@ -16,6 +16,7 @@ import PostPanelRow from './components/post-panel-row'; import PostViewLink from './components/post-view-link'; import PreviewDropdown from './components/preview-dropdown'; import PreferencesModal from './components/preferences-modal'; +import PostActions from './components/post-actions'; import { usePostActions } from './components/post-actions/actions'; import PostCardPanel from './components/post-card-panel'; @@ -30,6 +31,7 @@ lock( privateApis, { ModeSwitcher, PatternOverridesPanel, PluginPostExcerpt, + PostActions, PostPanelRow, PostViewLink, PreviewDropdown, diff --git a/packages/editor/src/store/constants.js b/packages/editor/src/store/constants.js index 7b5445e1e328f..73ce13066a6df 100644 --- a/packages/editor/src/store/constants.js +++ b/packages/editor/src/store/constants.js @@ -20,3 +20,9 @@ export const ONE_MINUTE_IN_MS = 60 * 1000; export const AUTOSAVE_PROPERTIES = [ 'title', 'excerpt', 'content' ]; export const TEMPLATE_POST_TYPE = 'wp_template'; export const TEMPLATE_PART_POST_TYPE = 'wp_template_part'; +export const PATTERN_POST_TYPE = 'wp_block'; +export const TEMPLATE_ORIGINS = { + custom: 'custom', + theme: 'theme', + plugin: 'plugin', +}; diff --git a/packages/editor/src/store/private-actions.js b/packages/editor/src/store/private-actions.js index 3e87df4812345..0c76ffb6960ec 100644 --- a/packages/editor/src/store/private-actions.js +++ b/packages/editor/src/store/private-actions.js @@ -2,10 +2,20 @@ * WordPress dependencies */ import { store as coreStore } from '@wordpress/core-data'; -import { __ } from '@wordpress/i18n'; +import { __, sprintf } from '@wordpress/i18n'; import { store as noticesStore } from '@wordpress/notices'; import { store as blockEditorStore } from '@wordpress/block-editor'; import { store as preferencesStore } from '@wordpress/preferences'; +import { addQueryArgs } from '@wordpress/url'; +import apiFetch from '@wordpress/api-fetch'; +import { parse, __unstableSerializeAndClean } from '@wordpress/blocks'; +import { decodeEntities } from '@wordpress/html-entities'; + +/** + * Internal dependencies + */ +import isTemplateRevertable from './utils/is-template-revertable'; +import { TEMPLATE_POST_TYPE } from './constants'; /** * Returns an action object used to set which template is currently being used/edited. @@ -220,3 +230,261 @@ export const saveDirtyEntities = ) ); }; + +/** + * Reverts a template to its original theme-provided file. + * + * @param {Object} template The template to revert. + * @param {Object} [options] + * @param {boolean} [options.allowUndo] Whether to allow the user to undo + * reverting the template. Default true. + */ +export const revertTemplate = + ( template, { allowUndo = true } = {} ) => + async ( { registry } ) => { + const noticeId = 'edit-site-template-reverted'; + registry.dispatch( noticesStore ).removeNotice( noticeId ); + if ( ! isTemplateRevertable( template ) ) { + registry + .dispatch( noticesStore ) + .createErrorNotice( __( 'This template is not revertable.' ), { + type: 'snackbar', + } ); + return; + } + + try { + const templateEntityConfig = registry + .select( coreStore ) + .getEntityConfig( 'postType', template.type ); + + if ( ! templateEntityConfig ) { + registry + .dispatch( noticesStore ) + .createErrorNotice( + __( + 'The editor has encountered an unexpected error. Please reload.' + ), + { type: 'snackbar' } + ); + return; + } + + const fileTemplatePath = addQueryArgs( + `${ templateEntityConfig.baseURL }/${ template.id }`, + { context: 'edit', source: 'theme' } + ); + + const fileTemplate = await apiFetch( { path: fileTemplatePath } ); + if ( ! fileTemplate ) { + registry + .dispatch( noticesStore ) + .createErrorNotice( + __( + 'The editor has encountered an unexpected error. Please reload.' + ), + { type: 'snackbar' } + ); + return; + } + + const serializeBlocks = ( { + blocks: blocksForSerialization = [], + } ) => __unstableSerializeAndClean( blocksForSerialization ); + + const edited = registry + .select( coreStore ) + .getEditedEntityRecord( + 'postType', + template.type, + template.id + ); + + // We are fixing up the undo level here to make sure we can undo + // the revert in the header toolbar correctly. + registry.dispatch( coreStore ).editEntityRecord( + 'postType', + template.type, + template.id, + { + content: serializeBlocks, // Required to make the `undo` behave correctly. + blocks: edited.blocks, // Required to revert the blocks in the editor. + source: 'custom', // required to avoid turning the editor into a dirty state + }, + { + undoIgnore: true, // Required to merge this edit with the last undo level. + } + ); + + const blocks = parse( fileTemplate?.content?.raw ); + registry + .dispatch( coreStore ) + .editEntityRecord( 'postType', template.type, fileTemplate.id, { + content: serializeBlocks, + blocks, + source: 'theme', + } ); + + if ( allowUndo ) { + const undoRevert = () => { + registry + .dispatch( coreStore ) + .editEntityRecord( + 'postType', + template.type, + edited.id, + { + content: serializeBlocks, + blocks: edited.blocks, + source: 'custom', + } + ); + }; + + registry + .dispatch( noticesStore ) + .createSuccessNotice( __( 'Template reset.' ), { + type: 'snackbar', + id: noticeId, + actions: [ + { + label: __( 'Undo' ), + onClick: undoRevert, + }, + ], + } ); + } + } catch ( error ) { + const errorMessage = + error.message && error.code !== 'unknown_error' + ? error.message + : __( 'Template revert failed. Please reload.' ); + registry + .dispatch( noticesStore ) + .createErrorNotice( errorMessage, { type: 'snackbar' } ); + } + }; + +/** + * Action that removes an array of templates. + * + * @param {Array} items An array of template or template part objects to remove. + */ +export const removeTemplates = + ( items ) => + async ( { registry } ) => { + const isTemplate = items[ 0 ].type === TEMPLATE_POST_TYPE; + const promiseResult = await Promise.allSettled( + items.map( ( item ) => { + return registry + .dispatch( coreStore ) + .deleteEntityRecord( + 'postType', + item.type, + item.id, + { force: true }, + { throwOnError: true } + ); + } ) + ); + + // If all the promises were fulfilled with sucess. + if ( promiseResult.every( ( { status } ) => status === 'fulfilled' ) ) { + let successMessage; + + if ( items.length === 1 ) { + // Depending on how the entity was retrieved its title might be + // an object or simple string. + const title = + typeof items[ 0 ].title === 'string' + ? items[ 0 ].title + : items[ 0 ].title?.rendered; + successMessage = sprintf( + /* translators: The template/part's name. */ + __( '"%s" deleted.' ), + decodeEntities( title ) + ); + } else { + successMessage = isTemplate + ? __( 'Templates deleted.' ) + : __( 'Template parts deleted.' ); + } + + registry + .dispatch( noticesStore ) + .createSuccessNotice( successMessage, { + type: 'snackbar', + id: 'site-editor-template-deleted-success', + } ); + } else { + // If there was at lease one failure. + let errorMessage; + // If we were trying to delete a single template. + if ( promiseResult.length === 1 ) { + if ( promiseResult[ 0 ].reason?.message ) { + errorMessage = promiseResult[ 0 ].reason.message; + } else { + errorMessage = isTemplate + ? __( 'An error occurred while deleting the template.' ) + : __( + 'An error occurred while deleting the template part.' + ); + } + // If we were trying to delete a multiple templates + } else { + const errorMessages = new Set(); + const failedPromises = promiseResult.filter( + ( { status } ) => status === 'rejected' + ); + for ( const failedPromise of failedPromises ) { + if ( failedPromise.reason?.message ) { + errorMessages.add( failedPromise.reason.message ); + } + } + if ( errorMessages.size === 0 ) { + errorMessage = isTemplate + ? __( + 'An error occurred while deleting the templates.' + ) + : __( + 'An error occurred while deleting the template parts.' + ); + } else if ( errorMessages.size === 1 ) { + errorMessage = isTemplate + ? sprintf( + /* translators: %s: an error message */ + __( + 'An error occurred while deleting the templates: %s' + ), + [ ...errorMessages ][ 0 ] + ) + : sprintf( + /* translators: %s: an error message */ + __( + 'An error occurred while deleting the template parts: %s' + ), + [ ...errorMessages ][ 0 ] + ); + } else { + errorMessage = isTemplate + ? sprintf( + /* translators: %s: a list of comma separated error messages */ + __( + 'Some errors occurred while deleting the templates: %s' + ), + [ ...errorMessages ].join( ',' ) + ) + : sprintf( + /* translators: %s: a list of comma separated error messages */ + __( + 'Some errors occurred while deleting the template parts: %s' + ), + [ ...errorMessages ].join( ',' ) + ); + } + } + registry + .dispatch( noticesStore ) + .createErrorNotice( errorMessage, { type: 'snackbar' } ); + } + }; diff --git a/packages/editor/src/store/utils/is-template-revertable.js b/packages/editor/src/store/utils/is-template-revertable.js new file mode 100644 index 0000000000000..efe4647f21280 --- /dev/null +++ b/packages/editor/src/store/utils/is-template-revertable.js @@ -0,0 +1,23 @@ +/** + * Internal dependencies + */ +import { TEMPLATE_ORIGINS } from '../constants'; + +// Copy of the function from packages/edit-site/src/utils/is-template-revertable.js + +/** + * Check if a template is revertable to its original theme-provided template file. + * + * @param {Object} template The template entity to check. + * @return {boolean} Whether the template is revertable. + */ +export default function isTemplateRevertable( template ) { + if ( ! template ) { + return false; + } + /* eslint-disable camelcase */ + return ( + template?.source === TEMPLATE_ORIGINS.custom && template?.has_theme_file + ); + /* eslint-enable camelcase */ +} diff --git a/packages/editor/src/style.scss b/packages/editor/src/style.scss index fd8f5821392c6..865635f228891 100644 --- a/packages/editor/src/style.scss +++ b/packages/editor/src/style.scss @@ -9,6 +9,7 @@ @import "./components/inserter-sidebar/style.scss"; @import "./components/list-view-sidebar/style.scss"; @import "./components/post-author/style.scss"; +@import "./components/post-actions/style.scss"; @import "./components/post-card-panel/style.scss"; @import "./components/post-excerpt/style.scss"; @import "./components/post-featured-image/style.scss"; diff --git a/packages/format-library/src/link/index.js b/packages/format-library/src/link/index.js index bbed362ccd9a7..ce1af0a36268e 100644 --- a/packages/format-library/src/link/index.js +++ b/packages/format-library/src/link/index.js @@ -38,36 +38,17 @@ function Edit( { onFocus, contentRef, } ) { - const [ editingLink, setEditingLink ] = useState( false ); - const [ creatingLink, setCreatingLink ] = useState( false ); + const [ addingLink, setAddingLink ] = useState( false ); // We only need to store the button element that opened the popover. We can ignore the other states, as they will be handled by the onFocus prop to return to the rich text field. const [ openedBy, setOpenedBy ] = useState( null ); - // Manages whether the Link UI popover should autofocus when shown. - const [ shouldAutoFocus, setShouldAutoFocus ] = useState( true ); - - function setIsEditingLink( isEditing, { autoFocus = true } = {} ) { - setEditingLink( isEditing ); - setShouldAutoFocus( autoFocus ); - } - - function setIsCreatingLink( isCreating ) { - // Don't add a new link if there is already an active link. - // The two states are mutually exclusive. - if ( isCreating === true && isActive ) { - return; - } - setCreatingLink( isCreating ); - } - useEffect( () => { // When the link becomes inactive (i.e. isActive is false), reset the editingLink state // and the creatingLink state. This means that if the Link UI is displayed and the link // becomes inactive (e.g. used arrow keys to move cursor outside of link bounds), the UI will close. if ( ! isActive ) { - setEditingLink( false ); - setCreatingLink( false ); + setAddingLink( false ); } }, [ isActive ] ); @@ -84,15 +65,19 @@ function Edit( { // This causes the `editingLink` state to be set to `true` and the link UI // to be rendered in "creating" mode. We need to check isActive to see if // we have an active link format. + const link = event.target.closest( '[contenteditable] a' ); if ( - ! event.target.closest( '[contenteditable] a' ) || // other formats (e.g. bold) may be nested within the link. + ! link || // other formats (e.g. bold) may be nested within the link. ! isActive ) { - setIsEditingLink( false ); return; } - setIsEditingLink( true, { autoFocus: false } ); + setAddingLink( true ); + setOpenedBy( { + el: link, + action: 'click', + } ); } editableContentElement.addEventListener( 'click', handleClick ); @@ -103,7 +88,6 @@ function Edit( { }, [ contentRef, isActive ] ); function addLink( target ) { - setShouldAutoFocus( true ); const text = getTextContent( slice( value ) ); if ( ! isActive && text && isURL( text ) && isValidHref( text ) ) { @@ -122,13 +106,12 @@ function Edit( { ); } else { if ( target ) { - setOpenedBy( target ); - } - if ( ! isActive ) { - setIsCreatingLink( true ); - } else { - setIsEditingLink( true ); + setOpenedBy( { + el: target, + action: null, // We don't need to distinguish between click or keyboard here + } ); } + setAddingLink( true ); } } @@ -147,12 +130,11 @@ function Edit( { // Otherwise, we rely on the passed in onFocus to return focus to the rich text field. // Close the popover - setIsEditingLink( false ); - setIsCreatingLink( false ); + setAddingLink( false ); // Return focus to the toolbar button or the rich text field - if ( openedBy?.tagName === 'BUTTON' ) { - openedBy.focus(); + if ( openedBy?.el?.tagName === 'BUTTON' ) { + openedBy.el.focus(); } else { onFocus(); } @@ -167,8 +149,7 @@ function Edit( { // 4. Press Escape // 5. Focus should be on the Options button function onFocusOutside() { - setIsEditingLink( false ); - setIsCreatingLink( false ); + setAddingLink( false ); setOpenedBy( null ); } @@ -177,7 +158,10 @@ function Edit( { speak( __( 'Link removed.' ), 'assertive' ); } - const isEditingActiveLink = editingLink && isActive; + // Only autofocus if we have clicked a link within the editor + const shouldAutoFocus = ! ( + openedBy?.el?.tagName === 'A' && openedBy?.action === 'click' + ); return ( <> @@ -194,13 +178,13 @@ function Edit( { onClick={ ( event ) => { addLink( event.currentTarget ); } } - isActive={ isActive || editingLink } + isActive={ isActive || addingLink } shortcutType="primary" shortcutCharacter="k" aria-haspopup="true" - aria-expanded={ editingLink } + aria-expanded={ addingLink } /> - { ( isEditingActiveLink || creatingLink ) && ( + { addingLink && ( { - return new Promise( ( resolve ) => { - const done = () => { - clearTimeout( timeout ); - window.cancelAnimationFrame( raf ); - setTimeout( () => { - callback(); - resolve(); - } ); - }; - const timeout = setTimeout( done, 100 ); - const raf = window.requestAnimationFrame( done ); - } ); -}; - -// Using the mangled properties: -// this.c: this._callback -// this.x: this._compute -// https://github.com/preactjs/signals/blob/main/mangle.json -function createFlusher( compute, notify ) { - let flush; - const dispose = effect( function () { - flush = this.c.bind( this ); - this.x = compute; - this.c = notify; - return compute(); - } ); - return { flush, dispose }; -} - -// Version of `useSignalEffect` with a `useEffect`-like execution. This hook -// implementation comes from this PR, but we added short-cirtuiting to avoid -// infinite loops: https://github.com/preactjs/signals/pull/290 -export function useSignalEffect( callback ) { - _useEffect( () => { - let eff = null; - let isExecuting = false; - const notify = async () => { - if ( eff && ! isExecuting ) { - isExecuting = true; - await afterNextFrame( eff.flush ); - isExecuting = false; - } - }; - eff = createFlusher( callback, notify ); - return eff.dispose; - }, [] ); -} - -/** - * Returns the passed function wrapped with the current scope so it is - * accessible whenever the function runs. This is primarily to make the scope - * available inside hook callbacks. - * - * @param {Function} func The passed function. - * @return {Function} The wrapped function. - */ -export const withScope = ( func ) => { - const scope = getScope(); - const ns = getNamespace(); - if ( func?.constructor?.name === 'GeneratorFunction' ) { - return async ( ...args ) => { - const gen = func( ...args ); - let value; - let it; - while ( true ) { - setNamespace( ns ); - setScope( scope ); - try { - it = gen.next( value ); - } finally { - resetNamespace(); - resetScope(); - } - try { - value = await it.value; - } catch ( e ) { - gen.throw( e ); - } - if ( it.done ) break; - } - return value; - }; - } - return ( ...args ) => { - setNamespace( ns ); - setScope( scope ); - try { - return func( ...args ); - } finally { - resetNamespace(); - resetScope(); - } - }; -}; - -/** - * Accepts a function that contains imperative code which runs whenever any of - * the accessed _reactive_ properties (e.g., values from the global state or the - * context) is modified. - * - * This hook makes the element's scope available so functions like - * `getElement()` and `getContext()` can be used inside the passed callback. - * - * @param {Function} callback The hook callback. - */ -export function useWatch( callback ) { - useSignalEffect( withScope( callback ) ); -} - -/** - * Accepts a function that contains imperative code which runs only after the - * element's first render, mainly useful for intialization logic. - * - * This hook makes the element's scope available so functions like - * `getElement()` and `getContext()` can be used inside the passed callback. - * - * @param {Function} callback The hook callback. - */ -export function useInit( callback ) { - _useEffect( withScope( callback ), [] ); -} - -/** - * Accepts a function that contains imperative, possibly effectful code. The - * effects run after browser paint, without blocking it. - * - * This hook is equivalent to Preact's `useEffect` and makes the element's scope - * available so functions like `getElement()` and `getContext()` can be used - * inside the passed callback. - * - * @param {Function} callback Imperative function that can return a cleanup - * function. - * @param {any[]} inputs If present, effect will only activate if the - * values in the list change (using `===`). - */ -export function useEffect( callback, inputs ) { - _useEffect( withScope( callback ), inputs ); -} - -/** - * Accepts a function that contains imperative, possibly effectful code. Use - * this to read layout from the DOM and synchronously re-render. - * - * This hook is equivalent to Preact's `useLayoutEffect` and makes the element's - * scope available so functions like `getElement()` and `getContext()` can be - * used inside the passed callback. - * - * @param {Function} callback Imperative function that can return a cleanup - * function. - * @param {any[]} inputs If present, effect will only activate if the - * values in the list change (using `===`). - */ -export function useLayoutEffect( callback, inputs ) { - _useLayoutEffect( withScope( callback ), inputs ); -} - -/** - * Returns a memoized version of the callback that only changes if one of the - * inputs has changed (using `===`). - * - * This hook is equivalent to Preact's `useCallback` and makes the element's - * scope available so functions like `getElement()` and `getContext()` can be - * used inside the passed callback. - * - * @template {Function} T The callback function type. - * - * @param {T} callback Callback function. - * @param {ReadonlyArray} inputs If present, the callback will only be updated if the - * values in the list change (using `===`). - * - * @return {T} The callback function. - */ -export function useCallback( callback, inputs ) { - return _useCallback( withScope( callback ), inputs ); -} - -/** - * Pass a factory function and an array of inputs. `useMemo` will only recompute - * the memoized value when one of the inputs has changed. - * - * This hook is equivalent to Preact's `useMemo` and makes the element's scope - * available so functions like `getElement()` and `getContext()` can be used - * inside the passed factory function. - * - * @template {unknown} T The memoized value. - * - * @param {() => T} factory Factory function that returns that value for memoization. - * @param {ReadonlyArray} inputs If present, the factory will only be run to recompute if - * the values in the list change (using `===`). - * - * @return {T} The memoized value. - */ -export function useMemo( factory, inputs ) { - return _useMemo( withScope( factory ), inputs ); -} - -// For wrapperless hydration. -// See https://gist.github.com/developit/f4c67a2ede71dc2fab7f357f39cff28c -export const createRootFragment = ( parent, replaceNode ) => { - replaceNode = [].concat( replaceNode ); - const s = replaceNode[ replaceNode.length - 1 ].nextSibling; - function insert( c, r ) { - parent.insertBefore( c, r || s ); - } - return ( parent.__k = { - nodeType: 1, - parentNode: parent, - firstChild: replaceNode[ 0 ], - childNodes: replaceNode, - insertBefore: insert, - appendChild: insert, - removeChild( c ) { - parent.removeChild( c ); - }, - } ); -}; diff --git a/packages/interactivity/src/utils.ts b/packages/interactivity/src/utils.ts new file mode 100644 index 0000000000000..e9162f0dd7cd7 --- /dev/null +++ b/packages/interactivity/src/utils.ts @@ -0,0 +1,290 @@ +/** + * External dependencies + */ +import { + useMemo as _useMemo, + useCallback as _useCallback, + useEffect as _useEffect, + useLayoutEffect as _useLayoutEffect, + type EffectCallback, + type Inputs, +} from 'preact/hooks'; +import { effect } from '@preact/signals'; + +/** + * Internal dependencies + */ +import { + getScope, + setScope, + resetScope, + getNamespace, + setNamespace, + resetNamespace, +} from './hooks'; + +interface Flusher { + readonly flush: () => void; + readonly dispose: () => void; +} + +/** + * Executes a callback function after the next frame is rendered. + * + * @param callback The callback function to be executed. + * @return A promise that resolves after the callback function is executed. + */ +const afterNextFrame = ( callback: () => void ) => { + return new Promise< void >( ( resolve ) => { + const done = () => { + clearTimeout( timeout ); + window.cancelAnimationFrame( raf ); + setTimeout( () => { + callback(); + resolve(); + } ); + }; + const timeout = setTimeout( done, 100 ); + const raf = window.requestAnimationFrame( done ); + } ); +}; + +/** + * Creates a Flusher object that can be used to flush computed values and notify listeners. + * + * Using the mangled properties: + * this.c: this._callback + * this.x: this._compute + * https://github.com/preactjs/signals/blob/main/mangle.json + * + * @param compute The function that computes the value to be flushed. + * @param notify The function that notifies listeners when the value is flushed. + * @return The Flusher object with `flush` and `dispose` properties. + */ +function createFlusher( compute: () => unknown, notify: () => void ): Flusher { + let flush: () => void; + const dispose = effect( function () { + flush = this.c.bind( this ); + this.x = compute; + this.c = notify; + return compute(); + } ); + return { flush, dispose } as const; +} + +/** + * Custom hook that executes a callback function whenever a signal is triggered. + * Version of `useSignalEffect` with a `useEffect`-like execution. This hook + * implementation comes from this PR, but we added short-cirtuiting to avoid + * infinite loops: https://github.com/preactjs/signals/pull/290 + * + * @param callback The callback function to be executed. + */ +export function useSignalEffect( callback: () => unknown ) { + _useEffect( () => { + let eff = null; + let isExecuting = false; + + const notify = async () => { + if ( eff && ! isExecuting ) { + isExecuting = true; + await afterNextFrame( eff.flush ); + isExecuting = false; + } + }; + + eff = createFlusher( callback, notify ); + return eff.dispose; + }, [] ); +} + +/** + * Returns the passed function wrapped with the current scope so it is + * accessible whenever the function runs. This is primarily to make the scope + * available inside hook callbacks. + * + * Asyncronous functions should use generators that yield promises instead of awaiting them. + * See the documentation for details: https://developer.wordpress.org/block-editor/reference-guides/packages/packages-interactivity/packages-interactivity-api-reference/#the-store + * + * @param func The passed function. + * @return The wrapped function. + */ +export function withScope< + Func extends ( ...args: any[] ) => Generator< any, any >, +>( + func: Func +): ( + ...args: Parameters< Func > +) => ReturnType< Func > extends Generator< any, infer Return > + ? Promise< Return > + : never; +export function withScope< Func extends Function >( func: Func ): Func; +export function withScope( func: ( ...args: unknown[] ) => unknown ) { + const scope = getScope(); + const ns = getNamespace(); + if ( func?.constructor?.name === 'GeneratorFunction' ) { + return async ( ...args: Parameters< typeof func > ) => { + const gen = func( ...args ) as Generator; + let value: any; + let it: any; + while ( true ) { + setNamespace( ns ); + setScope( scope ); + try { + it = gen.next( value ); + } finally { + resetNamespace(); + resetScope(); + } + try { + value = await it.value; + } catch ( e ) { + gen.throw( e ); + } + if ( it.done ) break; + } + return value; + }; + } + return ( ...args: Parameters< typeof func > ) => { + setNamespace( ns ); + setScope( scope ); + try { + return func( ...args ); + } finally { + resetNamespace(); + resetScope(); + } + }; +} + +/** + * Accepts a function that contains imperative code which runs whenever any of + * the accessed _reactive_ properties (e.g., values from the global state or the + * context) is modified. + * + * This hook makes the element's scope available so functions like + * `getElement()` and `getContext()` can be used inside the passed callback. + * + * @param callback The hook callback. + */ +export function useWatch( callback: () => unknown ) { + useSignalEffect( withScope( callback ) ); +} + +/** + * Accepts a function that contains imperative code which runs only after the + * element's first render, mainly useful for intialization logic. + * + * This hook makes the element's scope available so functions like + * `getElement()` and `getContext()` can be used inside the passed callback. + * + * @param callback The hook callback. + */ +export function useInit( callback: EffectCallback ) { + _useEffect( withScope( callback ), [] ); +} + +/** + * Accepts a function that contains imperative, possibly effectful code. The + * effects run after browser paint, without blocking it. + * + * This hook is equivalent to Preact's `useEffect` and makes the element's scope + * available so functions like `getElement()` and `getContext()` can be used + * inside the passed callback. + * + * @param callback Imperative function that can return a cleanup + * function. + * @param inputs If present, effect will only activate if the + * values in the list change (using `===`). + */ +export function useEffect( callback: EffectCallback, inputs: Inputs ) { + _useEffect( withScope( callback ), inputs ); +} + +/** + * Accepts a function that contains imperative, possibly effectful code. Use + * this to read layout from the DOM and synchronously re-render. + * + * This hook is equivalent to Preact's `useLayoutEffect` and makes the element's + * scope available so functions like `getElement()` and `getContext()` can be + * used inside the passed callback. + * + * @param callback Imperative function that can return a cleanup + * function. + * @param inputs If present, effect will only activate if the + * values in the list change (using `===`). + */ +export function useLayoutEffect( callback: EffectCallback, inputs: Inputs ) { + _useLayoutEffect( withScope( callback ), inputs ); +} + +/** + * Returns a memoized version of the callback that only changes if one of the + * inputs has changed (using `===`). + * + * This hook is equivalent to Preact's `useCallback` and makes the element's + * scope available so functions like `getElement()` and `getContext()` can be + * used inside the passed callback. + * + * @param callback Callback function. + * @param inputs If present, the callback will only be updated if the + * values in the list change (using `===`). + * + * @return The callback function. + */ +export function useCallback< T extends Function >( + callback: T, + inputs: Inputs +): T { + return _useCallback< T >( withScope( callback ), inputs ); +} + +/** + * Pass a factory function and an array of inputs. `useMemo` will only recompute + * the memoized value when one of the inputs has changed. + * + * This hook is equivalent to Preact's `useMemo` and makes the element's scope + * available so functions like `getElement()` and `getContext()` can be used + * inside the passed factory function. + * + * @param factory Factory function that returns that value for memoization. + * @param inputs If present, the factory will only be run to recompute if + * the values in the list change (using `===`). + * + * @return The memoized value. + */ +export function useMemo< T >( factory: () => T, inputs: Inputs ): T { + return _useMemo( withScope( factory ), inputs ); +} + +/** + * Creates a root fragment by replacing a node or an array of nodes in a parent element. + * For wrapperless hydration. + * See https://gist.github.com/developit/f4c67a2ede71dc2fab7f357f39cff28c + * + * @param parent The parent element where the nodes will be replaced. + * @param replaceNode The node or array of nodes to replace in the parent element. + * @return The created root fragment. + */ +export const createRootFragment = ( + parent: Element, + replaceNode: Node | Node[] +) => { + replaceNode = [].concat( replaceNode ); + const sibling = replaceNode[ replaceNode.length - 1 ].nextSibling; + function insert( child: any, root: any ) { + parent.insertBefore( child, root || sibling ); + } + return ( ( parent as any ).__k = { + nodeType: 1, + parentNode: parent, + firstChild: replaceNode[ 0 ], + childNodes: replaceNode, + insertBefore: insert, + appendChild: insert, + removeChild( c: Node ) { + parent.removeChild( c ); + }, + } ); +}; diff --git a/packages/interactivity/src/utils/kebab-to-camelcase.js b/packages/interactivity/src/utils/kebab-to-camelcase.js deleted file mode 100644 index a2c0d3403db3c..0000000000000 --- a/packages/interactivity/src/utils/kebab-to-camelcase.js +++ /dev/null @@ -1,14 +0,0 @@ -/** - * Transforms a kebab-case string to camelCase. - * - * @param {string} str The kebab-case string to transform to camelCase. - * @return {string} The transformed camelCase string. - */ -export function kebabToCamelCase( str ) { - return str - .replace( /^-+|-+$/g, '' ) - .toLowerCase() - .replace( /-([a-z])/g, function ( match, group1 ) { - return group1.toUpperCase(); - } ); -} diff --git a/packages/interactivity/src/utils/kebab-to-camelcase.ts b/packages/interactivity/src/utils/kebab-to-camelcase.ts new file mode 100644 index 0000000000000..b6c3c6f307124 --- /dev/null +++ b/packages/interactivity/src/utils/kebab-to-camelcase.ts @@ -0,0 +1,14 @@ +/** + * Transforms a kebab-case string to camelCase. + * + * @param str The kebab-case string to transform to camelCase. + * @return The transformed camelCase string. + */ +export function kebabToCamelCase( str: string ): string { + return str + .replace( /^-+|-+$/g, '' ) + .toLowerCase() + .replace( /-([a-z])/g, function ( _match, group1: string ) { + return group1.toUpperCase(); + } ); +} diff --git a/packages/interface/src/components/complementary-area/index.js b/packages/interface/src/components/complementary-area/index.js index 3e2913b20b18b..15b528c18c5a5 100644 --- a/packages/interface/src/components/complementary-area/index.js +++ b/packages/interface/src/components/complementary-area/index.js @@ -6,13 +6,25 @@ import classnames from 'classnames'; /** * WordPress dependencies */ -import { Button, Panel, Slot, Fill } from '@wordpress/components'; +import { + Button, + Panel, + Slot, + Fill, + __unstableMotion as motion, + __unstableAnimatePresence as AnimatePresence, +} from '@wordpress/components'; import { useDispatch, useSelect } from '@wordpress/data'; import { __ } from '@wordpress/i18n'; import { check, starEmpty, starFilled } from '@wordpress/icons'; -import { useEffect, useRef } from '@wordpress/element'; +import { useEffect, useRef, useState } from '@wordpress/element'; import { store as viewportStore } from '@wordpress/viewport'; import { store as preferencesStore } from '@wordpress/preferences'; +import { + useReducedMotion, + useViewportMatch, + usePrevious, +} from '@wordpress/compose'; /** * Internal dependencies @@ -24,16 +36,77 @@ import withComplementaryAreaContext from '../complementary-area-context'; import PinnedItems from '../pinned-items'; import { store as interfaceStore } from '../../store'; +const ANIMATION_DURATION = 0.3; + function ComplementaryAreaSlot( { scope, ...props } ) { return ; } -function ComplementaryAreaFill( { scope, children, className, id } ) { +const SIDEBAR_WIDTH = 280; +const variants = { + open: { width: SIDEBAR_WIDTH }, + closed: { width: 0 }, + mobileOpen: { width: '100vw' }, +}; + +function ComplementaryAreaFill( { + activeArea, + isActive, + scope, + children, + className, + id, +} ) { + const disableMotion = useReducedMotion(); + const isMobileViewport = useViewportMatch( 'medium', '<' ); + // This is used to delay the exit animation to the next tick. + // The reason this is done is to allow us to apply the right transition properties + // When we switch from an open sidebar to another open sidebar. + // we don't want to animate in this case. + const previousActiveArea = usePrevious( activeArea ); + const previousIsActive = usePrevious( isActive ); + const [ , setState ] = useState( {} ); + useEffect( () => { + setState( {} ); + }, [ isActive ] ); + const transition = { + type: 'tween', + duration: + disableMotion || + isMobileViewport || + ( !! previousActiveArea && + !! activeArea && + activeArea !== previousActiveArea ) + ? 0 + : ANIMATION_DURATION, + ease: [ 0.6, 0, 0.4, 1 ], + }; + return ( -
- { children } -
+ + { ( previousIsActive || isActive ) && ( + +
+ { children } +
+
+ ) } +
); } @@ -110,6 +183,11 @@ function ComplementaryArea( { toggleShortcut, isActiveByDefault, } ) { + // This state is used to delay the rendering of the Fill + // until the initial effect runs. + // This prevents the animation from running on mount if + // the complementary area is active by default. + const [ isReady, setIsReady ] = useState( false ); const { isLoading, isActive, @@ -163,6 +241,7 @@ function ComplementaryArea( { } else if ( activeArea === undefined && isSmall ) { disableComplementaryArea( scope, identifier ); } + setIsReady( true ); }, [ activeArea, isActiveByDefault, @@ -173,6 +252,10 @@ function ComplementaryArea( { disableComplementaryArea, ] ); + if ( ! isReady ) { + return; + } + return ( <> { isPinnable && ( @@ -204,59 +287,57 @@ function ComplementaryArea( { { title } ) } - { isActive && ( - + disableComplementaryArea( scope ) } + smallScreenTitle={ smallScreenTitle } + toggleButtonProps={ { + label: closeLabel, + shortcut: toggleShortcut, + scope, + identifier, + } } > - disableComplementaryArea( scope ) } - smallScreenTitle={ smallScreenTitle } - toggleButtonProps={ { - label: closeLabel, - shortcut: toggleShortcut, - scope, - identifier, - } } - > - { header || ( - <> -

- { title } -

- { isPinnable && ( -