diff --git a/.github/workflows/enforce-pr-labels.yml b/.github/workflows/enforce-pr-labels.yml index 093eb9a325e36..7493459a6ff35 100644 --- a/.github/workflows/enforce-pr-labels.yml +++ b/.github/workflows/enforce-pr-labels.yml @@ -12,7 +12,7 @@ jobs: with: mode: exactly count: 1 - labels: '[Type] Automated Testing, [Type] Breaking Change, [Type] Bug, [Type] Build Tooling, [Type] Code Quality, [Type] Copy, [Type] Developer Documentation, [Type] Enhancement, [Type] Experimental, [Type] Feature, [Type] New API, [Type] Task, [Type] Technical Prototype, [Type] Performance, [Type] Project Management, [Type] Regression, [Type] Security, [Type] WP Core Ticket, Backport from WordPress Core' + labels: '[Type] Automated Testing, [Type] Breaking Change, [Type] Bug, [Type] Build Tooling, [Type] Code Quality, [Type] Copy, [Type] Developer Documentation, [Type] Enhancement, [Type] Experimental, [Type] Feature, [Type] New API, [Type] Task, [Type] Technical Prototype, [Type] Performance, [Type] Project Management, [Type] Regression, [Type] Security, [Type] WP Core Ticket, Backport from WordPress Core, Gutenberg Plugin' add_comment: true message: "**Warning: Type of PR label mismatch**\n\n To merge this PR, it requires {{ errorString }} {{ count }} label indicating the type of PR. Other labels are optional and not being checked here. \n- **Type-related labels to choose from**: {{ provided }}.\n- **Labels found**: {{ applied }}.\n\nRead more about [Type labels in Gutenberg](https://github.com/WordPress/gutenberg/labels?q=type). Don't worry if you don't have the required permissions to add labels; the PR reviewer should be able to help with the task." exit_type: failure diff --git a/changelog.txt b/changelog.txt index dca31f9afc622..b802a88a14202 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,6 +1,6 @@ == Changelog == -= 19.3.0-rc.1 = += 19.3.0 = ## Changelog @@ -17,12 +17,12 @@ - Media placeholders: Add "drag" to the text. ([65149](https://github.com/WordPress/gutenberg/pull/65149)) - Restore: Move to trash button in Document settings. ([65087](https://github.com/WordPress/gutenberg/pull/65087)) - Inspector Controls: Use custom block name in inspector controls when available. ([65398](https://github.com/WordPress/gutenberg/pull/65398)) -- Plugin: Don't force iframe editor when gutenberg plugin and block theme are enabled. ([65372](https://github.com/WordPress/gutenberg/pull/65372)) - Icons: Adds bell and bell-unread icons. ([65324](https://github.com/WordPress/gutenberg/pull/65324)) - Editor topbar: Reorder the actions on the right. ([65163](https://github.com/WordPress/gutenberg/pull/65163)) - Patterns: Add opt out preference to the 'Choose a Pattern' modal when adding a page. ([65026](https://github.com/WordPress/gutenberg/pull/65026)) - Locked Templates: Blocks with contentOnly locking should not be transformable. ([64917](https://github.com/WordPress/gutenberg/pull/64917)) - Block Locking: Add border to Replace item in content only image toolbar. ([64849](https://github.com/WordPress/gutenberg/pull/64849)) +- DataViews: Improve UX of bundled views for Pages. ([65295](https://github.com/WordPress/gutenberg/pull/65295)) #### Components - Styling: Apply elevation scale in components package. ([65159](https://github.com/WordPress/gutenberg/pull/65159)) @@ -50,7 +50,6 @@ #### Block Editor - Link Editing: Automatically add tel to phone number when linking URL. ([64865](https://github.com/WordPress/gutenberg/pull/64865)) -thub.com/WordPress/gutenberg/pull/65300)) - Drag and Drop: When dragging a mix of video, audio, and image blocks, create individual blocks as appropriate. ([65144](https://github.com/WordPress/gutenberg/pull/65144)) - URLInput: Replace input with InputControl. ([65158](https://github.com/WordPress/gutenberg/pull/65158)) - Normalize block inspector controls spacing. ([64526](https://github.com/WordPress/gutenberg/pull/64526)) @@ -70,10 +69,6 @@ thub.com/WordPress/gutenberg/pull/65300)) - Refactor site background controls and move site global styles into Background group. ([65304](https://github.com/WordPress/gutenberg/pull/65304)) - Spacing control: Replace sides dropdwon with link button. ([65193](https://github.com/WordPress/gutenberg/pull/65193)) -#### Data Views -- DataViews Sidebar: Display item count on DataViews sidebar. ([65223](https://github.com/WordPress/gutenberg/pull/65223)) -- DataViews: Improve UX of bundled views for Pages. ([65295](https://github.com/WordPress/gutenberg/pull/65295)) - #### Interactivity API - Refactor context proxies. ([64713](https://github.com/WordPress/gutenberg/pull/64713)) - Update: Rephrase "Force page reload" and move to Advanced. ([65081](https://github.com/WordPress/gutenberg/pull/65081)) @@ -86,6 +81,7 @@ thub.com/WordPress/gutenberg/pull/65300)) - Add @wordpress/fields package. - Introduce the package. ([65230](https://github.com/WordPress/gutenberg/pull/65230)) - Make the package private. ([65269](https://github.com/WordPress/gutenberg/pull/65269)) +- Interactivity API: Add `getServerState()` and `getServerContext()`. ([65151](https://github.com/WordPress/gutenberg/pull/65151)) ### Bug Fixes @@ -97,7 +93,6 @@ thub.com/WordPress/gutenberg/pull/65300)) - Fix: Moving a page to the trash on the site editor does not goes back to the pages list. ([65119](https://github.com/WordPress/gutenberg/pull/65119)) - Fix: Moving the last page item to the the trash causes a crash. ([65236](https://github.com/WordPress/gutenberg/pull/65236)) - Preferences: Fix back button on mobile. ([65141](https://github.com/WordPress/gutenberg/pull/65141)) -- Revert "Don't force iframe editor when gutenberg plugin and block the me are enabled (#65372)". ([65402](https://github.com/WordPress/gutenberg/pull/65402)) - Post Summary Panel: Restore `height:Auto` for toggle buttons. ([65362](https://github.com/WordPress/gutenberg/pull/65362)) - Fix Tabs styling in Font Library modal. ([65330](https://github.com/WordPress/gutenberg/pull/65330)) - E2E: Change deprecated social icons for standard in end-to-end. ([65312](https://github.com/WordPress/gutenberg/pull/65312)) @@ -120,14 +115,17 @@ thub.com/WordPress/gutenberg/pull/65300)) - Fix: Embed blocks: Figcaption inserted via toolbar not nested within figure element - #64960. ([64970](https://github.com/WordPress/gutenberg/pull/64970)) - Image cropping: Skip making an API request if there are no changes to apply. ([65384](https://github.com/WordPress/gutenberg/pull/65384)) - Comments Pagination: Pass the comments query `paged` arg to functions `get_next_comments_link` and `get_previous_comments_link`. ([63698](https://github.com/WordPress/gutenberg/pull/63698)) -- Query Loop: Default to querying posts when on singular content. ([65067](https://github.com/WordPress/gutenberg/pull/65067)) +- Query Loop + - Default to querying posts when on singular content. ([65067](https://github.com/WordPress/gutenberg/pull/65067)) + - Remove is_singular() check and fix test. ([65483](https://github.com/WordPress/gutenberg/pull/65483)) + - Format controls: Fix JavaScript error. ([65551](https://github.com/WordPress/gutenberg/pull/65551)) #### Block Editor - Inserter: Fix loading indicator for reusable blocks. ([64839](https://github.com/WordPress/gutenberg/pull/64839)) - Normalize spacing in Layout hook controls. ([65132](https://github.com/WordPress/gutenberg/pull/65132)) - Pattern Inserter: Fix pattern list overflow. ([65192](https://github.com/WordPress/gutenberg/pull/65192)) - Remove reset styles RTL from the iframe. ([65150](https://github.com/WordPress/gutenberg/pull/65150)) -- Revert "Block Insertion: Clear the insertion point when selecting a d…. ([65208](https://github.com/WordPress/gutenberg/pull/65208)) +- Revert "Block Insertion: Clear the insertion point when selecting a different block or clearing block selection (https://github.com/WordPress/gutenberg/pull/64048)" ([65208](https://github.com/WordPress/gutenberg/pull/65208)) #### Components - BoxControl: Unify input filed width whether linked or not. ([65348](https://github.com/WordPress/gutenberg/pull/65348)) @@ -137,8 +135,10 @@ thub.com/WordPress/gutenberg/pull/65300)) #### Block bindings - Fix empty strings placeholders in post meta bindings. ([65089](https://github.com/WordPress/gutenberg/pull/65089)) -- Prioritize existing `placeholder` over `bindingsPlaceholder`. ([65154](https://github.com/WordPress/gutenberg/pull/65154)) -- Revert "Block Bindings: Prioritize existing `placeholder` over `bindingsPlaceholder`". ([65190](https://github.com/WordPress/gutenberg/pull/65190)) +- Remove key fallback in bindings get values and rely on source label. ([65517](https://github.com/WordPress/gutenberg/pull/65517)) +- Fix passing bindings context to `canUserEditValue`. ([65599](https://github.com/WordPress/gutenberg/pull/65599)) +- Prioritize existing placeholder over bindingsPlaceholder. ([65220](https://github.com/WordPress/gutenberg/pull/65220)) +- Only use `canUserEditValue` when `setValues` is defined. ([65565](https://github.com/WordPress/gutenberg/pull/65566)) #### Zoom Out - Force device type to Desktop whenever zoom out is invoked. ([64476](https://github.com/WordPress/gutenberg/pull/64476)) @@ -150,7 +150,6 @@ thub.com/WordPress/gutenberg/pull/65300)) - A11y: Add script-module. ([65101](https://github.com/WordPress/gutenberg/pull/65101)) - Interactivity API: Use a11y Script Module in Gutenberg. ([65123](https://github.com/WordPress/gutenberg/pull/65123)) - Script Modules API: Print script module live regions HTML in page HTML. ([65380](https://github.com/WordPress/gutenberg/pull/65380)) -- Post Editor: Support keyboard resizing of meta boxes pane. ([65325](https://github.com/WordPress/gutenberg/pull/65325)) - DatePicker: Better hover/focus styles. ([65117](https://github.com/WordPress/gutenberg/pull/65117)) - Form Input: Don't use `flex-direction: Row-reverse` for checkbox field. ([64232](https://github.com/WordPress/gutenberg/pull/64232)) - Navigation Menus: Remove Warning and add notice for Navigation. ([63921](https://github.com/WordPress/gutenberg/pull/63921)) @@ -158,6 +157,11 @@ thub.com/WordPress/gutenberg/pull/65300)) - Block Editor: Fix accessibility of the hooked blocks toggles. ([63133](https://github.com/WordPress/gutenberg/pull/63133)) +#### Post Editor +- Support keyboard resizing of meta boxes pane. ([65325](https://github.com/WordPress/gutenberg/pull/65325)) +- Swap position of the Pre-publish checks buttons. ([65317](https://github.com/WordPress/gutenberg/pull/65317)) + + ### Performance - Core Data: Batch remaining actions in resolvers. ([65176](https://github.com/WordPress/gutenberg/pull/65176)) @@ -199,7 +203,7 @@ thub.com/WordPress/gutenberg/pull/65300)) - Fix: Replace remaining 40px default size violations [Block Editor 1]. ([65034](https://github.com/WordPress/gutenberg/pull/65034)) - BoxControl - Add lint rule for 40px size prop usage. ([65341](https://github.com/WordPress/gutenberg/pull/65341)) - - DimensionsPanel: Apply 40px default size to UI when no spacing preset is available. ([65300](https://gi + - DimensionsPanel: Apply 40px default size to UI when no spacing preset is available. ([65300](https://github.com/WordPress/gutenberg/pull/65300)) - Add `useEvent` and revamped `useResizeObserver` to `@wordpress/compose`. ([64943](https://github.com/WordPress/gutenberg/pull/64943)) - DataViews: Use Dropdown for views configuration dialog. ([65314](https://github.com/WordPress/gutenberg/pull/65314)) - Platform docs: Upgrade dependencies. ([65445](https://github.com/WordPress/gutenberg/pull/65445)) diff --git a/docs/manifest.json b/docs/manifest.json index d7f74d47995b6..d76717fbdedfc 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -521,6 +521,12 @@ "markdown_source": "../docs/reference-guides/interactivity-api/core-concepts/server-side-rendering.md", "parent": "core-concepts" }, + { + "title": "Using TypeScript", + "slug": "using-typescript", + "markdown_source": "../docs/reference-guides/interactivity-api/core-concepts/using-typescript.md", + "parent": "core-concepts" + }, { "title": "Quick start guide", "slug": "iapi-quick-start-guide", diff --git a/docs/reference-guides/interactivity-api/core-concepts/README.md b/docs/reference-guides/interactivity-api/core-concepts/README.md index f4e6891c4ff16..695a4d622f6c5 100644 --- a/docs/reference-guides/interactivity-api/core-concepts/README.md +++ b/docs/reference-guides/interactivity-api/core-concepts/README.md @@ -7,3 +7,5 @@ This section provides some guides on important concepts and mental models relate 2. **[Understanding global state, local context and derived state](/docs/reference-guides/interactivity-api/core-concepts/undestanding-global-state-local-context-and-derived-state.md):** The guide explains how to effectively use global state, local context, and derived state within the Interactivity API emphasizing the importance of choosing the appropriate state management technique based on the scope and requirements of your data. 3. **[Server-side rendering: Processing directives on the server](/docs/reference-guides/interactivity-api/core-concepts/server-side-rendering.md):** The Interactivity API allows WordPress to use server-side rendering to create interactive and state-aware HTML, smoothly connected with client-side features while maintaining performance and SEO benefits. + +4. **[Using TypeScript](/docs/reference-guides/interactivity-api/core-concepts/using-typescript.md):** This guide will walk you through the process of using TypeScript with Interactivity API stores, covering everything from basic type definitions to advanced techniques for handling complex store structures. diff --git a/docs/reference-guides/interactivity-api/core-concepts/using-typescript.md b/docs/reference-guides/interactivity-api/core-concepts/using-typescript.md new file mode 100644 index 0000000000000..ed0bdd88211d1 --- /dev/null +++ b/docs/reference-guides/interactivity-api/core-concepts/using-typescript.md @@ -0,0 +1,746 @@ +# Using TypeScript + +The Interactivity API provides robust support for TypeScript, enabling developers to build type-safe stores to enhance the development experience with static type checking, improved code completion, and simplified refactoring. This guide will walk you through the process of using TypeScript with Interactivity API stores, covering everything from basic type definitions to advanced techniques for handling complex store structures. + +These are the core principles of TypeScript's interaction with the Interactivity API: + +- **Inferred client types**: When you create a store using the `store` function, TypeScript automatically infers the types of the store's properties (`state`, `actions`, etc.). This means that you can often get away with just writing plain JavaScript objects, and TypeScript will figure out the types for you. +- **Explicit server types**: When dealing with data defined on the server, like local context or the initial values of the global state, you can explicitly define its types to ensure that everything is correctly typed. +- **Mutiple store parts**: Even if your store is split into multiple parts, you can define or infer the types of each part of the store and then merge them into a single type that represents the entire store. +- **Typed external stores**: You can import typed stores from external namespaces, allowing you to use other plugins' functionality with type safety. + +## Installing `@wordpress/interactivity` locally + +If you haven't done so already, you need to install the package `@wordpress/interactivity` locally so TypeScript can use its types in your IDE. You can do this using the following command: + +`npm install @wordpress/interactivity` + +It is also a good practice to keep that package updated. + +## Scaffolding a new typed interactive block + +If you want to explore an example of an interactive block using TypeScript in your local environment, you can use the `@wordpress/create-block-interactive-template`. + +Start by ensuring you have Node.js and `npm` installed on your computer. Review the [Node.js development environment](https://developer.wordpress.org/block-editor/getting-started/devenv/nodejs-development-environment/) guide if not. + +Next, use the [`@wordpress/create-block`](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-create-block/) package and the [`@wordpress/create-block-interactive-template`](https://www.npmjs.com/package/@wordpress/create-block-interactive-template) template to scaffold the block. + +Choose the folder where you want to create the plugin, execute the following command in the terminal from within that folder, and choose the `typescript` variant when asked. + +``` +npx @wordpress/create-block@latest --template @wordpress/create-block-interactive-template +``` + +**Important**: Do not provide a slug in the terminal. Otherwise, `create-block` will not ask you which variant you want to choose and it will select the default non-TypeScript variant by default. + +Finally, you can keep following the instructions in the [Getting Started Guide](https://developer.wordpress.org/block-editor/reference-guides/interactivity-api/iapi-quick-start-guide/) as the rest of the instructions remain the same. + +## Typing the store + +Depending on the structure of your store and your preference, there are three options you can choose from to generate your store's types: + +1. Infer the types from your client store definition. +2. Manually type the server state, but infer the rest from your client store definition. +3. Manually write all the types. + +### 1. Infer the types from your client store definition + +When you create a store using the `store` function, TypeScript automatically infers the types of the store's properties (`state`, `actions`, `callbacks`, etc.). This means that you can often get away with just writing plain JavaScript objects, and TypeScript will figure out the correct types for you. + +Let's start with a basic example of a counter block. We will define the store in the `view.ts` file of the block, which contains the initial global state, an action and a callback. + +```ts +// view.ts +const myStore = store( 'myCounterPlugin', { + state: { + counter: 0, + }, + actions: { + increment() { + myStore.state.counter += 1; + }, + }, + callbacks: { + log() { + console.log( `counter: ${ myStore.state.counter }` ); + }, + }, +} ); +``` + +If you inspect the types of `myStore` using TypeScript, you will see that TypeScript has been able to infer the types correctly. + +```ts +const myStore: { + state: { + counter: number; + }; + actions: { + increment(): void; + }; + callbacks: { + log(): void; + }; +}; +``` + +You can also destructure the `state`, `actions` and `callbacks` properties, and the types will still work correctly. + +```ts +const { state } = store( 'myCounterPlugin', { + state: { + counter: 0, + }, + actions: { + increment() { + state.counter += 1; + }, + }, + callbacks: { + log() { + console.log( `counter: ${ state.counter }` ); + }, + }, +} ); +``` + +In conclusion, inferring the types is useful when you have a simple store defined in a single call to the `store` function and you do not need to type any state that has been initialized on the server. + +### 2. Manually type the server state, but infer the rest from your client store definition + +The global state that is initialized on the server with the `wp_interactivity_state` function doesn't exist on your client store definition and, therefore, needs to be manually typed. But if you don't want to define all the types of your store, you can infer the types of your client store definition and merge them with the types of your server initialized state. + +_Please, visit [the Server-side Rendering guide](/docs/reference-guides/interactivity-api/core-concepts/server-side-rendering.md) to learn more about `wp_interactivity_state` and how directives are processed on the server._ + +Following our previous example, let's move our `counter` state initialization to the server. + +```php +wp_interactivity_state( 'myCounterPlugin', array( + 'counter' => 1, +)); +``` + +Now, let's define the server state types and merge it with the types inferred from the client store definition. + +```ts +// Types the server state. +type ServerState = { + state: { + counter: number; + }; +}; + +// Defines the store in a variable to be able to extract its type later. +const storeDef = { + actions: { + increment() { + state.counter += 1; + }, + }, + callbacks: { + log() { + console.log( `counter: ${ state.counter }` ); + }, + }, +}; + +// Merges the types of the server state and the client store definition. +type Store = ServerState & typeof storeDef; + +// Injects the final types when calling the `store` function. +const { state } = store< Store >( 'myCounterPlugin', storeDef ); +``` + +Alternatively, if you don't mind typing the entire state including both the values defined on the server and the values defined on the client, you can cast the `state` property and let TypeScript infer the rest of the store. + +Let's imagine you have an additional property in the client global state called `product`. + +```ts +type State = { + counter: number; // The server state. + product: number; // The client state. +}; + +const { state } = store( 'myCounterPlugin', { + state: { + product: 2, + } as State, // Casts the entire state manually. + actions: { + increment() { + state.counter * state.product; + }, + }, +} ); +``` + +That's it. Now, TypeScript will infer the types of the `actions` and `callbacks` properties from the store definition, but it will use the type `State` for the `state` property so it contains the correct types from both the client and server definitions. + +In conclusion, this approach is useful when you have a server state that needs to be manually typed, but you still want to infer the types of the rest of the store. + +### 3. Manually write all the types + +If you prefer to define all the types of the store manually instead of letting TypeScript infer them from your client store definition, you can do that too. You simply need to pass them to the `store` function. + +```ts +// Defines the store types. +interface Store { + state: { + counter: number; // Initial server state + }; + actions: { + increment(): void; + }; + callbacks: { + log(): void; + }; +} + +// Pass the types when calling the `store` function. +const { state } = store< Store >( 'myCounterPlugin', { + actions: { + increment() { + state.counter += 1; + }, + }, + callbacks: { + log() { + console.log( `counter: ${ state.counter }` ); + }, + }, +} ); +``` + +That's it! In conclusion, this approach is useful when you want to control all the types of your store and you don't mind writing them by hand. + +## Typing the local context + +The initial local context is defined on the server using the `data-wp-context` directive. + +```html +
...
+``` + +For that reason, you need to define its type manually and pass it to the `getContext` function to ensure the returned properties are correctly typed. + +```ts +// Defines the types of your context. +type MyContext = { + counter: number; +}; + +store( 'myCounterPlugin', { + actions: { + increment() { + // Passes it to the getContext function. + const context = getContext< MyContext >(); + // Now `context` is properly typed. + context.counter += 1; + }, + }, +} ); +``` + +To avoid having to pass the context types over and over, you can also define a typed function and use that function instead of `getContext`. + +```ts +// Defines the types of your context. +type MyContext = { + counter: number; +}; + +// Defines a typed function. You only have to do this once. +const getMyContext = getContext< MyContext >; + +store( 'myCounterPlugin', { + actions: { + increment() { + // Use your typed function. + const context = getMyContext(); + // Now `context` is properly typed. + context.counter += 1; + }, + }, +} ); +``` + +That's it! Now you can access the context properties with the correct types. + +## Typing the derived state + +The derived state is data that is calculated based on the global state or local context. In the client store definition, it is defined using a getter in the `state` object. + +_Please, visit the [Understanding global state, local context and derived state](./undestanding-global-state-local-context-and-derived-state.md) guide to learn more about how derived state works in the Interactivity API._ + +Following our previous example, let's create a derived state that is the double of our counter. + +```ts +type MyContext = { + counter: number; +}; + +const myStore = store( 'myCounterPlugin', { + state: { + get double() { + const { counter } = getContext< MyContext >(); + return counter * 2; + }, + }, + actions: { + increment() { + state.counter += 1; // This type is number. + }, + }, +} ); +``` + +Normally, when the derived state depends on the local context, TypeScript will be able to infer the correct types: + +```ts +const myStore: { + state: { + readonly double: number; + }; + actions: { + increment(): void; + }; +}; +``` + +But when the return value of the derived state depends directly on some part of the global state, TypeScript will not be able to infer the types because it will claim that it has a circular reference. + +For example, in this case, TypeScript cannot infer the type of `state.double` because it depends on `state.counter`, and the type of `state` is not completed until the type of `state.double` is defined, creating a circular reference. + +```ts +const { state } = store( 'myCounterPlugin', { + state: { + counter: 0, + get double() { + // TypeScript can't infer this return type because it depends on `state`. + return state.counter * 2; + }, + }, + actions: { + increment() { + state.counter += 1; // This type is now unknown. + }, + }, +} ); +``` + +In this case, depending on your TypeScript configuration, TypeScript will either warn you about a circular reference or simply add the `any` type to the `state` property. + +However, solving this problem is easy; we simply need to manually provide TypeScript with the return type of that getter. Once we do that, the circular reference disappears, and TypeScript can once again infer all the `state` types. + +```ts +const { state } = store( 'myCounterPlugin', { + state: { + counter: 1, + get double(): number { + return state.counter * 2; + }, + }, + actions: { + increment() { + state.counter += 1; // Correctly inferred! + }, + }, +} ); +``` + +These are now the correct inferred types for the previous store. + +```ts +const myStore: { + state: { + counter: number; + readonly double: number; + }; + actions: { + increment(): void; + }; +}; +``` + +When using `wp_interactivity_state` in the server, remember that you also need to define the initial value of your derived state, like this: + +```php +wp_interactivity_state( 'myCounterPlugin', array( + 'counter' => 1, + 'double' => 2, +)); +``` + +But if you are inferring the types, you don't need to manually define the type of the derived state because it already exists in your client's store definition. + +```ts +// You don't need to type `state.double` here. +type ServerState = { + state: { + counter: number; + }; +}; + +// The `state.double` type is inferred from here. +const storeDef = { + state: { + get double(): number { + return state.counter * 2; + }, + }, + actions: { + increment() { + state.counter += 1; + }, + }, +}; + +// Merges the types of the server state and the client store definition. +type Store = ServerState & typeof storeDef; + +// Injects the final types when calling the `store` function. +const { state } = store< Store >( 'myCounterPlugin', storeDef ); +``` + +That's it! Now you can access the derived state properties with the correct types. + +## Typing asynchronous actions + +Another thing to keep in mind when using TypeScript with the Interactivity API is that asynchronous actions must be defined with generators instead of async functions. + +The reason for using generators in the Interactivity API's asynchronous actions is to be able to restore the scope from the initially triggered action once the asynchronous action continues its execution after yielding. But this is a syntax change only, otherwise, **these functions operate just like regular async functions**, and the inferred types from the `store` function reflect this. + +Following our previous example, let's add an asynchronous action to the store. + +```ts +const { state } = store( 'myCounterPlugin', { + state: { + counter: 0, + get double(): number { + return state.counter * 2; + }, + }, + actions: { + increment() { + state.counter += 1; + }, + *delayedIncrement() { + yield new Promise( ( r ) => setTimeout( r, 1000 ) ); + state.counter += 1; + }, + }, +} ); +``` + +The inferred types for this store are: + +```ts +const myStore: { + state: { + counter: number; + readonly double: number; + }; + actions: { + increment(): void; + // This behaves like a regular async function. + delayedIncrement(): Promise< void >; + }; +}; +``` + +This also means that you can use your async actions in external functions, and TypeScript will correctly use the async function types. + +```ts +const someAsyncFunction = async () => { + // This works fine and it's correctly typed. + await actions.delayedIncrement( 2000 ); +}; +``` + +When you are not inferring types but manually writing the types for your entire store, you can use async function types for your async actions. + +```ts +type Store = { + state: { + counter: number; + readonly double: number; + }; + actions: { + increment(): void; + delayedIncrement(): Promise< void >; // You can use async functions here. + }; +}; +``` + +There's something to keep in mind when when using asynchronous actions. Just like with the derived state, if the asynchronous action needs to return a value and this value directly depends on some part of the global state, TypeScript will not be able to infer the type due to a circular reference. + + ```ts + const { state, actions } = store( 'myCounterPlugin', { + state: { + counter: 0, + }, + actions: { + *delayedReturn() { + yield new Promise( ( r ) => setTimeout( r, 1000 ) ); + return state.counter; // TypeScript can't infer this return type. + }, + }, + } ); + ``` + + In this case, just as we did with the derived state, we must manually type the return value of the generator. + + ```ts + const { state, actions } = store( 'myCounterPlugin', { + state: { + counter: 0, + }, + actions: { + *delayedReturn(): Generator< uknown, number, uknown > { + yield new Promise( ( r ) => setTimeout( r, 1000 ) ); + return state.counter; // Now this is correctly inferred. + }, + }, + } ); + ``` + + That's it! Remember that the return type of a Generator is the second generic argument: `Generator< unknown, ReturnType, unknown >`. + +## Typing stores that are divided into multiple parts + +Sometimes, stores can be divided into different files. This can happen when different blocks share the same namespace, with each block loading the part of the store it needs. + +Let's look at an example of two blocks: + +- `todo-list`: A block that displays a list of todos. +- `add-post-to-todo`: A block that shows a button to add a new todo item to the list with the text "Read {$post_title}". + +First, let's initialize the global and derived state of the `todo-list` block on the server. + +```php + $todos, + 'filter' => 'all', + 'filteredTodos' => $todos, +)); +?> + + +``` + +Now, let's type the server state and add the client store definition. Remember, `filteredTodos` is derived state, so you don't need to type it manually. + +```ts +// todo-list-block/view.ts +type ServerState = { + state: { + todos: string[]; + filter: 'all' | 'completed'; + }; +}; + +const todoList = { + state: { + get filteredTodos(): string[] { + return state.filter === 'completed' + ? state.todos.filter( ( todo ) => todo.includes( '✅' ) ) + : state.todos; + }, + }, + actions: { + addTodo( todo: string ) { + state.todos.push( todo ); + }, + }, +}; + +// Merges the inferred types with the server state types. +export type TodoList = ServerState & typeof todoList; + +// Injects the final types when calling the `store` function. +const { state } = store< TodoList >( 'myTodoPlugin', todoList ); +``` + +So far, so good. Now let's create our `add-post-to-todo` block. + +First, let's add the current post title to the server state. + +```php + get_the_title(), +)); +?> + + +``` + +Now, let's type that server state and add the client store definition. + +```ts +// add-post-to-todo-block/view.ts +type ServerState = { + state: { + postTitle: string; + }; +}; + +const addPostToTodo = { + actions: { + addPostToTodo() { + const todo = `Read: ${ state.postTitle }`.trim(); + if ( ! state.todos.includes( todo ) ) { + actions.addTodo( todo ); + } + }, + }, +}; + +// Merges the inferred types with the server state types. +type Store = ServerState & typeof addPostToTodo; + +// Injects the final types when calling the `store` function. +const { state, actions } = store< Store >( 'myTodoPlugin', addPostToTodo ); +``` + +This works fine in the browser, but TypeScript will complain that, in this block, `state` and `actions` do not include `state.todos` and `actions.addtodo`. + +To fix this, we need to import the `TodoList` type from the `todo-list` block and merge it with the other types. + +```ts +import type { TodoList } from '../todo-list-block/view'; + +// ... + +// Merges the inferred types inferred the server state types. +type Store = TodoList & ServerState & typeof addPostToTodo; +``` + +That's it! Now TypeScript will know that `state.todos` and `actions.addTodo` are available in the `add-post-to-todo` block. + +This approach allows the `add-post-to-todo` block to interact with the existing todo list while maintaining type safety and adding its own functionality to the shared store. + +If you need to use the `add-post-to-todo` types in the `todo-list` block, you simply have to export its types and import them in the other `view.ts` file. + +Finally, if you prefer to define all types manually instead of inferring them, you can define them in a separate file and import that definition into each of your store parts. Here's how you could do that for our todo list example: + +```ts +// types.ts +interface Store { + state: { + todos: string[]; + filter: 'all' | 'completed'; + filtered: string[]; + postTitle: string; + }; + actions: { + addTodo( todo: string ): void; + addPostToTodo(): void; + }; +} + +export default Store; +``` + +```ts +// todo-list-block/view.ts +import type Store from '../types'; + +const { state } = store< Store >( 'myTodoPlugin', { + // Everything is correctly typed here +} ); +``` + +```ts +// add-post-to-todo-block/view.ts +import type Store from '../types'; + +const { state, actions } = store< Store >( 'myTodoPlugin', { + // Everything is correctly typed here +} ); +``` + +This approach allows you to have full control over your types and ensures consistency across all parts of your store. It's particularly useful when you have a complex store structure or when you want to enforce a specific interface across multiple blocks or components. + +## Importing and exporting typed stores + +In the Interactivity API, stores from other namespaces can be accessed using the `store` function. + +Let's go back to our `todo-list` block example, but this time, let's imagine that the `add-post-to-todo` block belongs to a different plugin and therefore will use a different namespace. + +```ts +// Import the store of the `todo-list` block. +const myTodoPlugin = store( 'myTodoPlugin' ); + +store( 'myAddPostToTodoPlugin', { + actions: { + addPostToTodo() { + const todo = `Read: ${ state.postTitle }`.trim(); + if ( ! myTodoPlugin.state.todos.includes( todo ) ) { + myTodoPlugin.actions.addTodo( todo ); + } + }, + }, +} ); +``` + +This works fine in the browser, but TypeScript will complain that `myTodoPlugin.state` and `myTodoPlugin.actions` are not typed. + +To fix that, the `myTodoPlugin` plugin can export the result of calling the `store` function with the correct types, and make that available using a script module. + +```ts +// Export the already typed state and actions. +export const { state, actions } = store< TodoList >( 'myTodoPlugin', { + // ... +} ); +``` + +Now, the `add-post-to-todo` block can import the typed store from the `myTodoPlugin` script module, and it not only ensures that the store will be loaded, but that it also contains the correct types. + +```ts +import { store } from '@wordpress/interactivity'; +import { + state as todoState, + actions as todoActions, +} from 'my-todo-plugin-module'; + +store( 'myAddPostToTodoPlugin', { + actions: { + addPostToTodo() { + const todo = `Read: ${ state.postTitle }`.trim(); + if ( ! todoState.todos.includes( todo ) ) { + todoActions.addTodo( todo ); + } + }, + }, +} ); +``` + +Remember that you will need to declare the `my-todo-plugin-module` script module as a dependency. + +If the other store is optional and you don't want to load it eagerly, a dynamic import can be used instead of a static import. + +```ts +import { store } from '@wordpress/interactivity'; + +store( 'myAddPostToTodoPlugin', { + actions: { + *addPostToTodo() { + const todoPlugin = yield import( 'my-todo-plugin-module' ); + const todo = `Read: ${ state.postTitle }`.trim(); + if ( ! todoPlugin.state.todos.includes( todo ) ) { + todoPlugin.actions.addTodo( todo ); + } + }, + }, +} ); +``` + +## Conclusion + +In this guide, we explored different approaches to typing the Interactivity API stores, from inferring types automatically to manually defining them. We also covered how to handle server-initialized state, local context, and derived state, as well as how to type asynchronous actions. + +Remember that the choice between inferring types and manually defining them depends on your specific needs and the complexity of your store. Whichever approach you choose, TypeScript will help you build better and more reliable interactive blocks. diff --git a/docs/toc.json b/docs/toc.json index 719ffa344e374..0d4689811b26e 100644 --- a/docs/toc.json +++ b/docs/toc.json @@ -214,6 +214,9 @@ }, { "docs/reference-guides/interactivity-api/core-concepts/server-side-rendering.md": [] + }, + { + "docs/reference-guides/interactivity-api/core-concepts/using-typescript.md": [] } ] }, diff --git a/lib/rest-api.php b/lib/rest-api.php index ac020e243ec05..7570bb1973723 100644 --- a/lib/rest-api.php +++ b/lib/rest-api.php @@ -13,9 +13,8 @@ /** * Overrides the REST controller for the `wp_global_styles` post type. * - * @param array $args Array of arguments for registering a post type. + * @param array $args Array of arguments for registering a post type. * See the register_post_type() function for accepted arguments. - * @param string $post_type Post type key. * * @return array Array of arguments for registering a post type. */ diff --git a/package-lock.json b/package-lock.json index 035018e97f13f..a40aa8aab4934 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "gutenberg", - "version": "19.3.0-rc.1", + "version": "19.3.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "gutenberg", - "version": "19.3.0-rc.1", + "version": "19.3.0", "hasInstallScript": true, "license": "GPL-2.0-or-later", "dependencies": { diff --git a/package.json b/package.json index a4cc002adbf8e..5ecd0ef57b351 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gutenberg", - "version": "19.3.0-rc.1", + "version": "19.3.0", "private": true, "description": "A new WordPress editor experience.", "author": "The WordPress Contributors", diff --git a/packages/block-editor/src/autocompleters/link.js b/packages/block-editor/src/autocompleters/link.js index ce9af28f19d00..fb64cb151294d 100644 --- a/packages/block-editor/src/autocompleters/link.js +++ b/packages/block-editor/src/autocompleters/link.js @@ -6,6 +6,7 @@ import apiFetch from '@wordpress/api-fetch'; import { addQueryArgs } from '@wordpress/url'; import { Icon, page, post } from '@wordpress/icons'; +import { decodeEntities } from '@wordpress/html-entities'; const SHOWN_SUGGESTIONS = 10; @@ -46,7 +47,7 @@ function createLinkCompleter() { key="icon" icon={ item.subtype === 'page' ? page : post } /> - { item.title } + { decodeEntities( item.title ) } ); }, diff --git a/packages/block-editor/src/components/block-controls/use-has-block-controls.js b/packages/block-editor/src/components/block-controls/use-has-block-controls.js deleted file mode 100644 index f7884cc1882ed..0000000000000 --- a/packages/block-editor/src/components/block-controls/use-has-block-controls.js +++ /dev/null @@ -1,35 +0,0 @@ -/** - * WordPress dependencies - */ -import { __experimentalUseSlotFills as useSlotFills } from '@wordpress/components'; -import warning from '@wordpress/warning'; - -/** - * Internal dependencies - */ -import groups from './groups'; - -export function useHasAnyBlockControls() { - let hasAnyBlockControls = false; - for ( const group in groups ) { - // It is safe to violate the rules of hooks here as the `groups` object - // is static and will not change length between renders. Do not return - // early as that will cause the hook to be called a different number of - // times between renders. - // eslint-disable-next-line react-hooks/rules-of-hooks - if ( useHasBlockControls( group ) ) { - hasAnyBlockControls = true; - } - } - return hasAnyBlockControls; -} - -export function useHasBlockControls( group = 'default' ) { - const Slot = groups[ group ]?.Slot; - const fills = useSlotFills( Slot?.__unstableName ); - if ( ! Slot ) { - warning( `Unknown BlockControls group "${ group }" provided.` ); - return null; - } - return !! fills?.length; -} diff --git a/packages/block-editor/src/components/block-parent-selector/index.js b/packages/block-editor/src/components/block-parent-selector/index.js index 80b314eeb42e5..9090de42f8b7d 100644 --- a/packages/block-editor/src/components/block-parent-selector/index.js +++ b/packages/block-editor/src/components/block-parent-selector/index.js @@ -14,6 +14,7 @@ import useBlockDisplayInformation from '../use-block-display-information'; import BlockIcon from '../block-icon'; import { useShowHoveredOrFocusedGestures } from '../block-toolbar/utils'; import { store as blockEditorStore } from '../../store'; +import { unlock } from '../../lock-unlock'; /** * Block parent selector component, displaying the hierarchy of the @@ -23,24 +24,26 @@ import { store as blockEditorStore } from '../../store'; */ export default function BlockParentSelector() { const { selectBlock } = useDispatch( blockEditorStore ); - const { firstParentClientId, isVisible } = useSelect( ( select ) => { + const { parentClientId, isVisible } = useSelect( ( select ) => { const { getBlockName, getBlockParents, getSelectedBlockClientId, getBlockEditingMode, - } = select( blockEditorStore ); + getParentSectionBlock, + } = unlock( select( blockEditorStore ) ); const { hasBlockSupport } = select( blocksStore ); const selectedBlockClientId = getSelectedBlockClientId(); + const parentSection = getParentSectionBlock( selectedBlockClientId ); const parents = getBlockParents( selectedBlockClientId ); - const _firstParentClientId = parents[ parents.length - 1 ]; - const parentBlockName = getBlockName( _firstParentClientId ); + const _parentClientId = parentSection ?? parents[ parents.length - 1 ]; + const parentBlockName = getBlockName( _parentClientId ); const _parentBlockType = getBlockType( parentBlockName ); return { - firstParentClientId: _firstParentClientId, + parentClientId: _parentClientId, isVisible: - _firstParentClientId && - getBlockEditingMode( _firstParentClientId ) === 'default' && + _parentClientId && + getBlockEditingMode( _parentClientId ) !== 'disabled' && hasBlockSupport( _parentBlockType, '__experimentalParentSelector', @@ -48,7 +51,7 @@ export default function BlockParentSelector() { ), }; }, [] ); - const blockInformation = useBlockDisplayInformation( firstParentClientId ); + const blockInformation = useBlockDisplayInformation( parentClientId ); // Allows highlighting the parent block outline when focusing or hovering // the parent block selector within the child. @@ -65,13 +68,13 @@ export default function BlockParentSelector() { return (
selectBlock( firstParentClientId ) } + onClick={ () => selectBlock( parentClientId ) } label={ sprintf( /* translators: %s: Name of the block's parent. */ __( 'Select parent block: %s' ), diff --git a/packages/block-editor/src/components/block-settings-menu/block-settings-dropdown.js b/packages/block-editor/src/components/block-settings-menu/block-settings-dropdown.js index fff5acc7b79c4..ac2b99ac2bb62 100644 --- a/packages/block-editor/src/components/block-settings-menu/block-settings-dropdown.js +++ b/packages/block-editor/src/components/block-settings-menu/block-settings-dropdown.js @@ -183,6 +183,9 @@ export function BlockSettingsDropdown( { } } + const shouldShowBlockParentMenuItem = + ! parentBlockIsSelected && !! firstParentClientId; + return ( ( - - { ( { onClose } ) => ( - <> - - <__unstableBlockSettingsMenuFirstItem.Slot - fillProps={ { onClose } } - /> - { ! parentBlockIsSelected && - !! firstParentClientId && ( + } ) => { + // It is possible that some plugins register fills for this menu + // even if Core doesn't render anything in the block settings menu. + // in which case, we may want to render the menu anyway. + // That said for now, we can start more conservative. + const isEmpty = + ! canRemove && + ! canDuplicate && + ! canInsertBlock && + isContentOnly; + + if ( isEmpty ) { + return null; + } + + return ( + + { ( { onClose } ) => ( + <> + + <__unstableBlockSettingsMenuFirstItem.Slot + fillProps={ { onClose } } + /> + { shouldShowBlockParentMenuItem && ( ) } - { count === 1 && ( - - ) } - { ! isContentOnly && ( - - ) } - { canDuplicate && ( - - { __( 'Duplicate' ) } - - ) } - { canInsertBlock && ! isContentOnly && ( - <> + { count === 1 && ( + + ) } + { ! isContentOnly && ( + + ) } + { canDuplicate && ( - { __( 'Add before' ) } + { __( 'Duplicate' ) } + + ) } + { canInsertBlock && ! isContentOnly && ( + <> + + { __( 'Add before' ) } + + + { __( 'Add after' ) } + + + ) } + + { canCopyStyles && ! isContentOnly && ( + + + + { __( 'Paste styles' ) } + + ) } + + { typeof children === 'function' + ? children( { onClose } ) + : Children.map( ( child ) => + cloneElement( child, { onClose } ) + ) } + { canRemove && ( + - { __( 'Add after' ) } + { __( 'Delete' ) } - + ) } - - { canCopyStyles && ! isContentOnly && ( - - - - { __( 'Paste styles' ) } - - - ) } - - { typeof children === 'function' - ? children( { onClose } ) - : Children.map( ( child ) => - cloneElement( child, { onClose } ) - ) } - { canRemove && ( - - - { __( 'Delete' ) } - - - ) } - - ) } - - ) } + + ) } + + ); + } } ); } diff --git a/packages/block-editor/src/components/block-switcher/index.js b/packages/block-editor/src/components/block-switcher/index.js index 98e7f7b2d2142..79f33bd30d753 100644 --- a/packages/block-editor/src/components/block-switcher/index.js +++ b/packages/block-editor/src/components/block-switcher/index.js @@ -35,36 +35,40 @@ function BlockSwitcherDropdownMenuContents( { clientIds, hasBlockStyles, canRemove, - isUsingBindings, } ) { const { replaceBlocks, multiSelect, updateBlockAttributes } = useDispatch( blockEditorStore ); - const { possibleBlockTransformations, patterns, blocks } = useSelect( - ( select ) => { - const { - getBlocksByClientId, - getBlockRootClientId, - getBlockTransformItems, - __experimentalGetPatternTransformItems, - } = select( blockEditorStore ); - const rootClientId = getBlockRootClientId( - Array.isArray( clientIds ) ? clientIds[ 0 ] : clientIds - ); - const _blocks = getBlocksByClientId( clientIds ); - return { - blocks: _blocks, - possibleBlockTransformations: getBlockTransformItems( - _blocks, - rootClientId - ), - patterns: __experimentalGetPatternTransformItems( - _blocks, - rootClientId - ), - }; - }, - [ clientIds ] - ); + const { possibleBlockTransformations, patterns, blocks, isUsingBindings } = + useSelect( + ( select ) => { + const { + getBlockAttributes, + getBlocksByClientId, + getBlockRootClientId, + getBlockTransformItems, + __experimentalGetPatternTransformItems, + } = select( blockEditorStore ); + const rootClientId = getBlockRootClientId( clientIds[ 0 ] ); + const _blocks = getBlocksByClientId( clientIds ); + return { + blocks: _blocks, + possibleBlockTransformations: getBlockTransformItems( + _blocks, + rootClientId + ), + patterns: __experimentalGetPatternTransformItems( + _blocks, + rootClientId + ), + isUsingBindings: clientIds.every( + ( clientId ) => + !! getBlockAttributes( clientId )?.metadata + ?.bindings + ), + }; + }, + [ clientIds ] + ); const blockVariationTransformations = useBlockVariationTransforms( { clientIds, blocks, @@ -196,7 +200,7 @@ const BlockIndicator = ( { icon, showTitle, blockTitle } ) => ( ); -export const BlockSwitcher = ( { clientIds, disabled, isUsingBindings } ) => { +export const BlockSwitcher = ( { clientIds } ) => { const { hasContentOnlyLocking, canRemove, @@ -205,6 +209,7 @@ export const BlockSwitcher = ( { clientIds, disabled, isUsingBindings } ) => { invalidBlocks, isReusable, isTemplate, + isDisabled, } = useSelect( ( select ) => { const { @@ -212,6 +217,7 @@ export const BlockSwitcher = ( { clientIds, disabled, isUsingBindings } ) => { getBlocksByClientId, getBlockAttributes, canRemoveBlocks, + getBlockEditingMode, } = select( blockEditorStore ); const { getBlockStyles, getBlockType, getActiveBlockVariation } = select( blocksStore ); @@ -222,6 +228,7 @@ export const BlockSwitcher = ( { clientIds, disabled, isUsingBindings } ) => { const [ { name: firstBlockName } ] = _blocks; const _isSingleBlockSelected = _blocks.length === 1; const blockType = getBlockType( firstBlockName ); + const editingMode = getBlockEditingMode( clientIds[ 0 ] ); let _icon; let _hasTemplateLock; @@ -256,6 +263,7 @@ export const BlockSwitcher = ( { clientIds, disabled, isUsingBindings } ) => { isTemplate: _isSingleBlockSelected && isTemplatePart( _blocks[ 0 ] ), hasContentOnlyLocking: _hasTemplateLock, + isDisabled: editingMode !== 'default', }; }, [ clientIds ] @@ -275,7 +283,7 @@ export const BlockSwitcher = ( { clientIds, disabled, isUsingBindings } ) => { : __( 'Multiple blocks selected' ); const hideDropdown = - disabled || + isDisabled || ( ! hasBlockStyles && ! canRemove ) || hasContentOnlyLocking; @@ -339,7 +347,6 @@ export const BlockSwitcher = ( { clientIds, disabled, isUsingBindings } ) => { clientIds={ clientIds } hasBlockStyles={ hasBlockStyles } canRemove={ canRemove } - isUsingBindings={ isUsingBindings } /> ) } diff --git a/packages/block-editor/src/components/block-switcher/test/use-transformed.patterns.js b/packages/block-editor/src/components/block-switcher/test/use-transformed.patterns.js index 05ce545667d46..4d63c76317479 100644 --- a/packages/block-editor/src/components/block-switcher/test/use-transformed.patterns.js +++ b/packages/block-editor/src/components/block-switcher/test/use-transformed.patterns.js @@ -20,15 +20,15 @@ describe( 'use-transformed-patterns', () => { }, content: { type: 'boolean', - __experimentalRole: 'content', + role: 'content', }, level: { type: 'number', - __experimentalRole: 'content', + role: 'content', }, color: { type: 'string', - __experimentalRole: 'other', + role: 'other', }, }, save() {}, diff --git a/packages/block-editor/src/components/block-switcher/test/utils.js b/packages/block-editor/src/components/block-switcher/test/utils.js index 38009601e1646..eafe5e8a4d937 100644 --- a/packages/block-editor/src/components/block-switcher/test/utils.js +++ b/packages/block-editor/src/components/block-switcher/test/utils.js @@ -18,15 +18,15 @@ describe( 'BlockSwitcher - utils', () => { }, content: { type: 'boolean', - __experimentalRole: 'content', + role: 'content', }, level: { type: 'number', - __experimentalRole: 'content', + role: 'content', }, color: { type: 'string', - __experimentalRole: 'other', + role: 'other', }, }, save() {}, diff --git a/packages/block-editor/src/components/block-switcher/utils.js b/packages/block-editor/src/components/block-switcher/utils.js index ebd95fc460e33..49257a2126cbe 100644 --- a/packages/block-editor/src/components/block-switcher/utils.js +++ b/packages/block-editor/src/components/block-switcher/utils.js @@ -1,7 +1,7 @@ /** * WordPress dependencies */ -import { __experimentalGetBlockAttributesNamesByRole as getBlockAttributesNamesByRole } from '@wordpress/blocks'; +import { getBlockAttributesNamesByRole } from '@wordpress/blocks'; /** * Try to find a matching block by a block's name in a provided diff --git a/packages/block-editor/src/components/block-toolbar/index.js b/packages/block-editor/src/components/block-toolbar/index.js index 6c4789cb2924f..2ac2cbb12ff35 100644 --- a/packages/block-editor/src/components/block-toolbar/index.js +++ b/packages/block-editor/src/components/block-toolbar/index.js @@ -35,6 +35,7 @@ import { store as blockEditorStore } from '../../store'; import __unstableBlockNameContext from './block-name-context'; import NavigableToolbar from '../navigable-toolbar'; import { useHasBlockToolbar } from './use-has-block-toolbar'; +import { unlock } from '../../lock-unlock'; /** * Renders the block toolbar. @@ -58,7 +59,6 @@ export function PrivateBlockToolbar( { const { blockClientId, blockClientIds, - isContentOnlyEditingMode, isDefaultEditingMode, blockType, toolbarKey, @@ -78,12 +78,14 @@ export function PrivateBlockToolbar( { getBlockAttributes, getBlockParentsByBlockName, getTemplateLock, - } = select( blockEditorStore ); + getParentSectionBlock, + } = unlock( select( blockEditorStore ) ); const selectedBlockClientIds = getSelectedBlockClientIds(); const selectedBlockClientId = selectedBlockClientIds[ 0 ]; const parents = getBlockParents( selectedBlockClientId ); - const firstParentClientId = parents[ parents.length - 1 ]; - const parentBlockName = getBlockName( firstParentClientId ); + const parentSection = getParentSectionBlock( selectedBlockClientId ); + const parentClientId = parentSection ?? parents[ parents.length - 1 ]; + const parentBlockName = getBlockName( parentClientId ); const parentBlockType = getBlockType( parentBlockName ); const editingMode = getBlockEditingMode( selectedBlockClientId ); const _isDefaultEditingMode = editingMode === 'default'; @@ -112,21 +114,19 @@ export function PrivateBlockToolbar( { return { blockClientId: selectedBlockClientId, blockClientIds: selectedBlockClientIds, - isContentOnlyEditingMode: editingMode === 'contentOnly', isDefaultEditingMode: _isDefaultEditingMode, blockType: selectedBlockClientId && getBlockType( _blockName ), shouldShowVisualToolbar: isValid && isVisual, - toolbarKey: `${ selectedBlockClientId }${ firstParentClientId }`, + toolbarKey: `${ selectedBlockClientId }${ parentClientId }`, showParentSelector: parentBlockType && - getBlockEditingMode( firstParentClientId ) === 'default' && + getBlockEditingMode( parentClientId ) !== 'disabled' && hasBlockSupport( parentBlockType, '__experimentalParentSelector', true ) && - selectedBlockClientIds.length === 1 && - _isDefaultEditingMode, + selectedBlockClientIds.length === 1, isUsingBindings: _isUsingBindings, hasParentPattern: _hasParentPattern, hasContentOnlyLocking: _hasTemplateLock, @@ -179,36 +179,26 @@ export function PrivateBlockToolbar( { key={ toolbarKey } >
- { ! isMultiToolbar && - isLargeViewport && - isDefaultEditingMode && } + { ! isMultiToolbar && isLargeViewport && ( + + ) } { ( shouldShowVisualToolbar || isMultiToolbar ) && - ( isDefaultEditingMode || - ( isContentOnlyEditingMode && ! hasParentPattern ) || - isSynced ) && ( + ! hasParentPattern && (
- + { ! isMultiToolbar && isDefaultEditingMode && ( + + ) } + - { isDefaultEditingMode && ( - <> - { ! isMultiToolbar && ( - - ) } - - - ) }
) } @@ -242,9 +232,7 @@ export function PrivateBlockToolbar( { ) } - { isDefaultEditingMode && ( - - ) } +
); diff --git a/packages/block-editor/src/components/block-toolbar/style.scss b/packages/block-editor/src/components/block-toolbar/style.scss index 40d748dd0a156..ae03eeed1a817 100644 --- a/packages/block-editor/src/components/block-toolbar/style.scss +++ b/packages/block-editor/src/components/block-toolbar/style.scss @@ -37,6 +37,13 @@ border-right: $border-width solid $gray-300; } + &.is-connected { + .block-editor-block-switcher .components-button::before { + background: color-mix(in srgb, var(--wp-block-synced-color) 10%, transparent); + border-radius: $radius-small; + } + } + &.is-synced, &.is-connected { .block-editor-block-switcher .components-button .block-editor-block-icon { @@ -52,9 +59,18 @@ > :last-child, > :last-child .components-toolbar-group, - > :last-child .components-toolbar { + > :last-child .components-toolbar, + // If the last toolbar group is empty, + // we need to remove the double border from the penultimate one. + &:has(> :last-child:empty) > :nth-last-child(2), + &:has(> :last-child:empty) > :nth-last-child(2) .components-toolbar-group, + &:has(> :last-child:empty) > :nth-last-child(2) .components-toolbar { border-right: none; } + + .components-toolbar-group:empty { + display: none; + } } .block-editor-block-contextual-toolbar { diff --git a/packages/block-editor/src/components/block-toolbar/use-has-block-toolbar.js b/packages/block-editor/src/components/block-toolbar/use-has-block-toolbar.js index c4e228f8a3c07..80ce369114783 100644 --- a/packages/block-editor/src/components/block-toolbar/use-has-block-toolbar.js +++ b/packages/block-editor/src/components/block-toolbar/use-has-block-toolbar.js @@ -7,7 +7,6 @@ import { getBlockType, hasBlockSupport } from '@wordpress/blocks'; * Internal dependencies */ import { store as blockEditorStore } from '../../store'; -import { useHasAnyBlockControls } from '../block-controls/use-has-block-controls'; /** * Returns true if the block toolbar should be shown. @@ -15,40 +14,29 @@ import { useHasAnyBlockControls } from '../block-controls/use-has-block-controls * @return {boolean} Whether the block toolbar component will be rendered. */ export function useHasBlockToolbar() { - const { isToolbarEnabled, isDefaultEditingMode } = useSelect( - ( select ) => { - const { - getBlockEditingMode, - getBlockName, - getBlockSelectionStart, - } = select( blockEditorStore ); + const { isToolbarEnabled, isBlockDisabled } = useSelect( ( select ) => { + const { getBlockEditingMode, getBlockName, getBlockSelectionStart } = + select( blockEditorStore ); - // we only care about the 1st selected block - // for the toolbar, so we use getBlockSelectionStart - // instead of getSelectedBlockClientIds - const selectedBlockClientId = getBlockSelectionStart(); + // we only care about the 1st selected block + // for the toolbar, so we use getBlockSelectionStart + // instead of getSelectedBlockClientIds + const selectedBlockClientId = getBlockSelectionStart(); - const blockType = - selectedBlockClientId && - getBlockType( getBlockName( selectedBlockClientId ) ); + const blockType = + selectedBlockClientId && + getBlockType( getBlockName( selectedBlockClientId ) ); - return { - isToolbarEnabled: - blockType && - hasBlockSupport( blockType, '__experimentalToolbar', true ), - isDefaultEditingMode: - getBlockEditingMode( selectedBlockClientId ) === 'default', - }; - }, - [] - ); + return { + isToolbarEnabled: + blockType && + hasBlockSupport( blockType, '__experimentalToolbar', true ), + isBlockDisabled: + getBlockEditingMode( selectedBlockClientId ) === 'disabled', + }; + }, [] ); - const hasAnyBlockControls = useHasAnyBlockControls(); - - if ( - ! isToolbarEnabled || - ( ! isDefaultEditingMode && ! hasAnyBlockControls ) - ) { + if ( ! isToolbarEnabled || isBlockDisabled ) { return false; } diff --git a/packages/block-editor/src/components/block-variation-picker/index.js b/packages/block-editor/src/components/block-variation-picker/index.js index ecdf8b23bec3f..f3687a305e84f 100644 --- a/packages/block-editor/src/components/block-variation-picker/index.js +++ b/packages/block-editor/src/components/block-variation-picker/index.js @@ -64,8 +64,7 @@ function BlockVariationPicker( { { allowSkip && (
- } - /> + + + { isActive && } + + } + /> + ); } @@ -143,11 +142,7 @@ function renderShadowToggle() { }; return ( - + +
-{{/isBasicVariant}} diff --git a/packages/create-block-interactive-template/block-templates/style.scss.mustache b/packages/create-block-interactive-template/block-templates/style.scss.mustache index 1c73fa1c38ff9..c8aa9f232136e 100644 --- a/packages/create-block-interactive-template/block-templates/style.scss.mustache +++ b/packages/create-block-interactive-template/block-templates/style.scss.mustache @@ -9,4 +9,19 @@ font-size: 1em; background: #ffff001a; padding: 1em; + + &.dark-theme { + background: #333; + color: #fff; + + button { + background: #555; + color: #fff; + border: 1px solid #777; + } + + p { + color: #ddd; + } + } } diff --git a/packages/create-block-interactive-template/block-templates/view.js.mustache b/packages/create-block-interactive-template/block-templates/view.js.mustache index b4bae3939461d..3fcf1ba365d26 100644 --- a/packages/create-block-interactive-template/block-templates/view.js.mustache +++ b/packages/create-block-interactive-template/block-templates/view.js.mustache @@ -1,15 +1,23 @@ -{{#isBasicVariant}} +{{#isDefaultVariant}} /** * WordPress dependencies */ -import { store, getContext } from "@wordpress/interactivity"; +import { store, getContext } from '@wordpress/interactivity'; -store( '{{namespace}}', { +const { state } = store( '{{namespace}}', { + state: { + get themeText() { + return state.isDark ? state.darkText : state.lightText; + } + }, actions: { - toggle: () => { + toggleOpen() { const context = getContext(); context.isOpen = ! context.isOpen; }, + toggleTheme() { + state.isDark = ! state.isDark; + } }, callbacks: { logIsOpen: () => { @@ -19,5 +27,4 @@ store( '{{namespace}}', { }, }, } ); - -{{/isBasicVariant}} +{{/isDefaultVariant}} diff --git a/packages/create-block-interactive-template/block-templates/view.ts.mustache b/packages/create-block-interactive-template/block-templates/view.ts.mustache new file mode 100644 index 0000000000000..11670442d7370 --- /dev/null +++ b/packages/create-block-interactive-template/block-templates/view.ts.mustache @@ -0,0 +1,46 @@ +{{#isTypescriptVariant}} +/** + * WordPress dependencies + */ +import { store, getContext } from '@wordpress/interactivity'; + +type ServerState = { + state: { + isDark: boolean; + darkText: string; + lightText: string; + }; +}; + +type Context = { + isOpen: boolean; +}; + +const storeDef = { + state: { + get themeText(): string { + return state.isDark ? state.darkText : state.lightText; + } + }, + actions: { + toggleOpen() { + const context = getContext< Context >(); + context.isOpen = ! context.isOpen; + }, + toggleTheme() { + state.isDark = ! state.isDark; + } + }, + callbacks: { + logIsOpen: () => { + const { isOpen } = getContext< Context >(); + // Log the value of `isOpen` each time it changes. + console.log( `Is open: ${ isOpen }` ); + }, + }, +}; + +type Store = ServerState & typeof storeDef; + +const { state } = store< Store >( '{{namespace}}', storeDef ); +{{/isTypescriptVariant}} diff --git a/packages/create-block-interactive-template/index.js b/packages/create-block-interactive-template/index.js index bb203b7023e28..94f615df2747f 100644 --- a/packages/create-block-interactive-template/index.js +++ b/packages/create-block-interactive-template/index.js @@ -7,7 +7,7 @@ module.exports = { defaultValues: { slug: 'example-interactive', title: 'Example Interactive', - description: 'An interactive block with the Interactivity API', + description: 'An interactive block with the Interactivity API.', dashicon: 'media-interactive', npmDependencies: [ '@wordpress/interactivity' ], customPackageJSON: { files: [ '[^.]*' ] }, @@ -24,7 +24,14 @@ module.exports = { }, }, variants: { - basic: {}, + default: {}, + typescript: { + slug: 'example-interactive-typescript', + title: 'Example Interactive TypeScript', + description: + 'An interactive block with the Interactivity API using TypeScript.', + viewScriptModule: 'file:./view.ts', + }, }, pluginTemplatesPath: join( __dirname, 'plugin-templates' ), blockTemplatesPath: join( __dirname, 'block-templates' ), diff --git a/packages/edit-site/src/components/global-styles-sidebar/style.scss b/packages/edit-site/src/components/global-styles-sidebar/style.scss index b76192ddfcb5c..4ca87bf200f17 100644 --- a/packages/edit-site/src/components/global-styles-sidebar/style.scss +++ b/packages/edit-site/src/components/global-styles-sidebar/style.scss @@ -22,14 +22,7 @@ flex-direction: column; min-height: 100%; - &__panel, - &__navigator-provider { - display: flex; - flex-direction: column; - flex: 1; - } - - &__navigator-screen { + &__panel { flex: 1; } } diff --git a/packages/edit-site/src/components/global-styles/ui.js b/packages/edit-site/src/components/global-styles/ui.js index 60d7e314d7776..bc6906a769af4 100644 --- a/packages/edit-site/src/components/global-styles/ui.js +++ b/packages/edit-site/src/components/global-styles/ui.js @@ -272,19 +272,6 @@ function GlobalStylesEditorCanvasContainerLink() { goTo( '/' ); } break; - default: - /* - * Example: the user has navigated to "Browse styles" or elsewhere - * and changes the editorCanvasContainerView, e.g., closes the style book. - * The panel should not be affected. - * Exclude revisions panel from this behavior, - * as it should close when the editorCanvasContainerView doesn't correspond. - */ - if ( path !== '/' && ! isRevisionsOpen ) { - return; - } - goTo( '/' ); - break; } }, [ editorCanvasContainerView, isRevisionsOpen, goTo ] ); } diff --git a/packages/edit-site/src/components/resizable-frame/index.js b/packages/edit-site/src/components/resizable-frame/index.js index b589a3861c75d..b5aae01918e69 100644 --- a/packages/edit-site/src/components/resizable-frame/index.js +++ b/packages/edit-site/src/components/resizable-frame/index.js @@ -14,7 +14,7 @@ import { } from '@wordpress/components'; import { useInstanceId, useReducedMotion } from '@wordpress/compose'; import { useDispatch, useSelect } from '@wordpress/data'; -import { __ } from '@wordpress/i18n'; +import { __, isRTL } from '@wordpress/i18n'; /** * Internal dependencies @@ -171,7 +171,10 @@ function ResizableFrame( { event.preventDefault(); const step = 20 * ( event.shiftKey ? 5 : 1 ); - const delta = step * ( event.key === 'ArrowLeft' ? 1 : -1 ); + const delta = + step * + ( event.key === 'ArrowLeft' ? 1 : -1 ) * + ( isRTL() ? -1 : 1 ); const newWidth = Math.min( Math.max( FRAME_MIN_WIDTH, @@ -200,15 +203,17 @@ function ResizableFrame( { const resizeHandleVariants = { hidden: { opacity: 0, - left: 0, + ...( isRTL() ? { right: 0 } : { left: 0 } ), }, visible: { opacity: 1, - left: -14, // Account for the handle's width. + // Account for the handle's width. + ...( isRTL() ? { right: -14 } : { left: -14 } ), }, active: { opacity: 1, - left: -14, // Account for the handle's width. + // Account for the handle's width. + ...( isRTL() ? { right: -14 } : { left: -14 } ), scaleY: 1.3, }, }; @@ -246,10 +251,11 @@ function ResizableFrame( { size={ frameSize } enable={ { top: false, - right: false, bottom: false, // Resizing will be disabled until the editor content is loaded. - left: isReady, + ...( isRTL() + ? { right: isReady, left: false } + : { left: isReady, right: false } ), topRight: false, bottomRight: false, bottomLeft: false, @@ -269,7 +275,7 @@ function ResizableFrame( { onMouseOver={ () => setShouldShowHandle( true ) } onMouseOut={ () => setShouldShowHandle( false ) } handleComponent={ { - left: canvasMode === 'view' && ( + [ isRTL() ? 'right' : 'left' ]: canvasMode === 'view' && ( <> { /* Disable reason: role="separator" does in fact support aria-valuenow */ } diff --git a/packages/edit-site/src/components/sidebar-dataviews/dataview-item.js b/packages/edit-site/src/components/sidebar-dataviews/dataview-item.js index 1a96fdc059143..1e12d6706d81b 100644 --- a/packages/edit-site/src/components/sidebar-dataviews/dataview-item.js +++ b/packages/edit-site/src/components/sidebar-dataviews/dataview-item.js @@ -27,7 +27,6 @@ export default function DataViewItem( { isActive, isCustom, suffix, - navigationItemSuffix, } ) { const { params: { postType }, @@ -56,7 +55,6 @@ export default function DataViewItem( { { title } diff --git a/packages/edit-site/src/components/sidebar-dataviews/default-views.js b/packages/edit-site/src/components/sidebar-dataviews/default-views.js index 20f61e451b21f..658fa319e9c66 100644 --- a/packages/edit-site/src/components/sidebar-dataviews/default-views.js +++ b/packages/edit-site/src/components/sidebar-dataviews/default-views.js @@ -13,7 +13,7 @@ import { notAllowed, } from '@wordpress/icons'; import { useSelect } from '@wordpress/data'; -import { store as coreStore, useEntityRecords } from '@wordpress/core-data'; +import { store as coreStore } from '@wordpress/core-data'; import { useMemo } from '@wordpress/element'; /** @@ -68,50 +68,6 @@ const DEFAULT_POST_BASE = { layout: defaultLayouts[ LAYOUT_LIST ].layout, }; -export function useDefaultViewsWithItemCounts( { postType } ) { - const defaultViews = useDefaultViews( { postType } ); - const { records, totalItems } = useEntityRecords( 'postType', postType, { - per_page: -1, - status: [ 'any', 'trash' ], - } ); - - return useMemo( () => { - if ( ! defaultViews ) { - return []; - } - - // If there are no records, return the default views with no counts. - if ( ! records ) { - return defaultViews; - } - - const counts = { - drafts: records.filter( ( record ) => record.status === 'draft' ) - .length, - future: records.filter( ( record ) => record.status === 'future' ) - .length, - pending: records.filter( ( record ) => record.status === 'pending' ) - .length, - private: records.filter( ( record ) => record.status === 'private' ) - .length, - published: records.filter( - ( record ) => record.status === 'publish' - ).length, - trash: records.filter( ( record ) => record.status === 'trash' ) - .length, - }; - - // All items excluding trashed items as per the default "all" status query. - counts.all = totalItems ? totalItems - counts.trash : 0; - - // Filter out views with > 0 item counts. - return defaultViews.map( ( _view ) => { - _view.count = counts[ _view.slug ]; - return _view; - } ); - }, [ defaultViews, records, totalItems ] ); -} - export function useDefaultViews( { postType } ) { const labels = useSelect( ( select ) => { diff --git a/packages/edit-site/src/components/sidebar-dataviews/index.js b/packages/edit-site/src/components/sidebar-dataviews/index.js index 3f7f5b965fce7..86420c4eec1d1 100644 --- a/packages/edit-site/src/components/sidebar-dataviews/index.js +++ b/packages/edit-site/src/components/sidebar-dataviews/index.js @@ -7,7 +7,7 @@ import { privateApis as routerPrivateApis } from '@wordpress/router'; /** * Internal dependencies */ -import { useDefaultViewsWithItemCounts } from './default-views'; +import { useDefaultViews } from './default-views'; import { unlock } from '../../lock-unlock'; import DataViewItem from './dataview-item'; import CustomDataViewsList from './custom-dataviews-list'; @@ -18,9 +18,7 @@ export default function DataViewsSidebarContent() { const { params: { postType, activeView = 'all', isCustom = 'false' }, } = useLocation(); - - const defaultViews = useDefaultViewsWithItemCounts( { postType } ); - + const defaultViews = useDefaultViews( { postType } ); if ( ! postType ) { return null; } @@ -36,9 +34,6 @@ export default function DataViewsSidebarContent() { slug={ dataview.slug } title={ dataview.title } icon={ dataview.icon } - navigationItemSuffix={ - { dataview.count } - } type={ dataview.view.type } isActive={ ! isCustomBoolean && diff --git a/packages/edit-site/src/components/sidebar-dataviews/style.scss b/packages/edit-site/src/components/sidebar-dataviews/style.scss index 3473c8e20e1a4..14e6bf1d03fca 100644 --- a/packages/edit-site/src/components/sidebar-dataviews/style.scss +++ b/packages/edit-site/src/components/sidebar-dataviews/style.scss @@ -15,10 +15,6 @@ min-width: initial; } - .edit-site-sidebar-navigation-item.with-suffix { - padding-right: $grid-unit-10; - } - &:hover, &:focus, &[aria-current] { diff --git a/packages/edit-widgets/src/components/header/style.scss b/packages/edit-widgets/src/components/header/style.scss index 6e5d8de8142f4..7bd3c41a6a22a 100644 --- a/packages/edit-widgets/src/components/header/style.scss +++ b/packages/edit-widgets/src/components/header/style.scss @@ -82,6 +82,7 @@ padding-right: $grid-unit-10; padding-left: $grid-unit-20; overflow: hidden; + height: $header-height; } .edit-widgets-header__title { diff --git a/packages/editor/src/bindings/post-meta.js b/packages/editor/src/bindings/post-meta.js index 572cd0b525a00..fae010e72d1c8 100644 --- a/packages/editor/src/bindings/post-meta.js +++ b/packages/editor/src/bindings/post-meta.js @@ -9,26 +9,65 @@ import { store as coreDataStore } from '@wordpress/core-data'; import { store as editorStore } from '../store'; import { unlock } from '../lock-unlock'; -function getMetadata( registry, context, registeredFields ) { - let metaFields = {}; - const type = registry.select( editorStore ).getCurrentPostType(); +/** + * Gets a list of post meta fields with their values and labels + * to be consumed in the needed callbacks. + * If the value is not available based on context, like in templates, + * it falls back to the default value, label, or key. + * + * @param {Object} registry The registry context exposed through `useRegistry`. + * @param {Object} context The context provided. + * @return {Object} List of post meta fields with their value and label. + * + * @example + * ```js + * { + * field_1_key: { + * label: 'Field 1 Label', + * value: 'Field 1 Value', + * }, + * field_2_key: { + * label: 'Field 2 Label', + * value: 'Field 2 Value', + * }, + * ... + * } + * ``` + */ +function getPostMetaFields( registry, context ) { const { getEditedEntityRecord } = registry.select( coreDataStore ); + const { getRegisteredPostMeta } = unlock( + registry.select( coreDataStore ) + ); + let entityMetaValues; + // Try to get the current entity meta values. if ( context?.postType && context?.postId ) { - metaFields = getEditedEntityRecord( + entityMetaValues = getEditedEntityRecord( 'postType', context?.postType, context?.postId ).meta; - } else if ( type === 'wp_template' ) { - // Populate the `metaFields` object with the default values. - Object.entries( registeredFields || {} ).forEach( - ( [ key, props ] ) => { - if ( props.default ) { - metaFields[ key ] = props.default; - } - } - ); + } + + const registeredFields = getRegisteredPostMeta( context?.postType ); + const metaFields = {}; + Object.entries( registeredFields || {} ).forEach( ( [ key, props ] ) => { + // Don't include footnotes or private fields. + if ( key !== 'footnotes' && key.charAt( 0 ) !== '_' ) { + metaFields[ key ] = { + label: props.title || key, + value: + // When using the entity value, an empty string IS a valid value. + entityMetaValues?.[ key ] ?? + // When using the default, an empty string IS NOT a valid value. + ( props.default || undefined ), + }; + } + } ); + + if ( ! Object.keys( metaFields || {} ).length ) { + return null; } return metaFields; @@ -37,20 +76,15 @@ function getMetadata( registry, context, registeredFields ) { export default { name: 'core/post-meta', getValues( { registry, context, bindings } ) { - const { getRegisteredPostMeta } = unlock( - registry.select( coreDataStore ) - ); - const registeredFields = getRegisteredPostMeta( context?.postType ); - const metaFields = getMetadata( registry, context, registeredFields ); + const metaFields = getPostMetaFields( registry, context ); const newValues = {}; for ( const [ attributeName, source ] of Object.entries( bindings ) ) { // Use the value, the field label, or the field key. - const metaKey = source.args.key; - newValues[ attributeName ] = - metaFields?.[ metaKey ] ?? - registeredFields?.[ metaKey ]?.title ?? - metaKey; + const fieldKey = source.args.key; + const { value: fieldValue, label: fieldLabel } = + metaFields?.[ fieldKey ] || {}; + newValues[ attributeName ] = fieldValue ?? fieldLabel ?? fieldKey; } return newValues; }, @@ -110,31 +144,6 @@ export default { return true; }, getFieldsList( { registry, context } ) { - const { getRegisteredPostMeta } = unlock( - registry.select( coreDataStore ) - ); - const registeredFields = getRegisteredPostMeta( context?.postType ); - const metaFields = getMetadata( registry, context, registeredFields ); - - if ( ! metaFields || ! Object.keys( metaFields ).length ) { - return null; - } - - return Object.fromEntries( - Object.entries( metaFields ) - // Remove footnotes or private keys from the list of fields. - .filter( - ( [ key ] ) => - key !== 'footnotes' && key.charAt( 0 ) !== '_' - ) - // Return object with label and value. - .map( ( [ key, value ] ) => [ - key, - { - label: registeredFields?.[ key ]?.title || key, - value, - }, - ] ) - ); + return getPostMetaFields( registry, context ); }, }; diff --git a/packages/editor/src/components/block-settings-menu/content-only-settings-menu.js b/packages/editor/src/components/block-settings-menu/content-only-settings-menu.js index c772a062b9e3b..af0e9b30ae83b 100644 --- a/packages/editor/src/components/block-settings-menu/content-only-settings-menu.js +++ b/packages/editor/src/components/block-settings-menu/content-only-settings-menu.js @@ -27,7 +27,10 @@ function ContentOnlySettingsMenuItems( { clientId, onClose } ) { getBlockParentsByBlockName, getSettings, getBlockAttributes, + getBlockParents, } = select( blockEditorStore ); + const { getCurrentTemplateId, getRenderingMode } = + select( editorStore ); const patternParent = getBlockParentsByBlockName( clientId, 'core/block', @@ -41,23 +44,17 @@ function ContentOnlySettingsMenuItems( { clientId, onClose } ) { 'wp_block', getBlockAttributes( patternParent ).ref ); - } else { - const { getCurrentTemplateId } = select( editorStore ); - const templateId = getCurrentTemplateId(); - const { getBlockParents } = unlock( - select( blockEditorStore ) + } else if ( + getRenderingMode() === 'template-locked' && + ! getBlockParents( clientId ).some( ( parent ) => + postContentBlocks.includes( parent ) + ) + ) { + record = select( coreStore ).getEntityRecord( + 'postType', + 'wp_template', + getCurrentTemplateId() ); - if ( - ! getBlockParents( clientId ).some( ( parent ) => - postContentBlocks.includes( parent ) - ) - ) { - record = select( coreStore ).getEntityRecord( - 'postType', - 'wp_template', - templateId - ); - } } if ( ! record ) { return {}; diff --git a/packages/editor/src/components/header/index.js b/packages/editor/src/components/header/index.js index fb034ba8bb857..aca91daaac732 100644 --- a/packages/editor/src/components/header/index.js +++ b/packages/editor/src/components/header/index.js @@ -7,7 +7,6 @@ import { __unstableMotion as motion } from '@wordpress/components'; import { store as preferencesStore } from '@wordpress/preferences'; import { useState } from '@wordpress/element'; import { PinnedItems } from '@wordpress/interface'; -import { store as blockEditorStore } from '@wordpress/block-editor'; /** * Internal dependencies @@ -65,7 +64,6 @@ function Header( { getEditorSettings, isPublishSidebarOpened: _isPublishSidebarOpened, } = select( editorStore ); - const { __unstableGetEditorMode } = select( blockEditorStore ); return { isTextEditor: getEditorMode() === 'text', @@ -74,7 +72,6 @@ function Header( { hasFixedToolbar: getPreference( 'core', 'fixedToolbar' ), isNestedEntity: !! getEditorSettings().onNavigateToPreviousEntityRecord, - isZoomedOutView: __unstableGetEditorMode() === 'zoom-out', }; }, [] ); @@ -83,9 +80,10 @@ function Header( { const hasCenter = isBlockToolsCollapsed && ! isTooNarrowForDocumentBar; const hasBackButton = useHasBackButton(); - - // The edit-post-header classname is only kept for backward compatibilty - // as some plugins might be relying on its presence. + /* + * The edit-post-header classname is only kept for backward compatability + * as some plugins might be relying on its presence. + */ return (
{ hasBackButton && ( @@ -127,11 +125,13 @@ function Header( { className="editor-header__settings" > { ! customSaveButton && ! isPublishSidebarOpened && ( - // This button isn't completely hidden by the publish sidebar. - // We can't hide the whole toolbar when the publish sidebar is open because - // we want to prevent mounting/unmounting the PostPublishButtonOrToggle DOM node. - // We track that DOM node to return focus to the PostPublishButtonOrToggle - // when the publish sidebar has been closed. + /* + * This button isn't completely hidden by the publish sidebar. + * We can't hide the whole toolbar when the publish sidebar is open because + * we want to prevent mounting/unmounting the PostPublishButtonOrToggle DOM node. + * We track that DOM node to return focus to the PostPublishButtonOrToggle + * when the publish sidebar has been closed. + */ ) } { - const { isZoomOut } = useSelect( ( select ) => ( { + const { isZoomOut, showIconLabels } = useSelect( ( select ) => ( { isZoomOut: unlock( select( blockEditorStore ) ).isZoomOut(), + showIconLabels: select( preferencesStore ).get( + 'core', + 'showIconLabels' + ), } ) ); const { resetZoomLevel, setZoomLevel, __unstableSetEditorMode } = unlock( @@ -35,9 +40,10 @@ const ZoomOutToggle = () => {