Content Guidelines: Add block guidelines management#76187
Content Guidelines: Add block guidelines management#76187ramonjd merged 26 commits intoWordPress:trunkfrom
Conversation
|
The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message. To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook. |
953daab to
ab59a35
Compare
routes/content-guidelines/components/block-guideline-modal.scss
Outdated
Show resolved
Hide resolved
| guidelines: categories.additional, | ||
| }, | ||
| blocks: Object.fromEntries( | ||
| Object.entries( blockGuidelines ).map( |
There was a problem hiding this comment.
nit: Both the remove action in block-guidelines.tsx:110 and block-guideline-modal.tsx:87 use setBlockGuideline(blockName, '') to represent deletion. But I think blocks would still be sent as { "core/image": { "guidelines": "" } } rather than being omitted. This does not happen in practise because we disable the button when the textarea is empty. But wondering if the reducer should filter out empty-string entries, or saveContentGuidelines should exclude them before sending.
There was a problem hiding this comment.
Had to revert ^ because it breaks the block-guideline deletion. Basically in the backend if we see that a block's guideline is empty string, we remove the guideline from meta. So we have to send empty string.
Here is the relevant code:
| getGuideline( | ||
| state: ContentGuidelinesState, | ||
| category: string | ||
| ): string | Record< string, string > { |
There was a problem hiding this comment.
Claude suggested here to use the discriminated return type / separate selector.
..by keeping getGuideline returning string only, since there's already getBlockGuidelines() and getBlockGuideline(blockName) for block lookups. Just guard against the blocks key:
getGuideline(state: ContentGuidelinesState, category: string): string {
if (category === 'blocks') {
return ''; // callers should use getBlockGuideline(blockName) instead
}
return (state.categories[category] as string) ?? '';
}Or even better, type category more narrowly so TypeScript enforces it at compile time:
type TextCategory = 'site' | 'copy' | 'images' | 'additional';
getGuideline(state: ContentGuidelinesState, category: TextCategory): string {
return state.categories[category] ?? '';
}That way if someone writes getGuideline('blocks'), TypeScript rejects it and points them to getBlockGuideline() / getBlockGuidelines() instead. No union return type, no as string casts at call sites, and the two data shapes stay cleanly separated.
The TextCategory type could live in types.ts alongside the existing Categories interface, keeping it DRY if other selectors/actions need the same constraint.
There was a problem hiding this comment.
@aswasif007 : perhaps this is superseded with @aagam-shah 's suggestion below: #76187 (comment) .
| }; | ||
|
|
||
| const CATEGORIES = [ 'site', 'copy', 'images', 'additional' ]; | ||
| const CATEGORIES = [ 'site', 'copy', 'images', 'additional', 'blocks' ]; |
There was a problem hiding this comment.
This is more of a note for future reference than feedback that needs addressing.
When I first started to design this, I wanted to keep two clear category types: primitive and complex (like blocks). This is also to avoid having to hardcode categories like we've done here.
Here's a rough sketch of it could look like:
1. Types — define the two classes:
// types.ts
interface PrimitiveCategory {
kind: 'primitive';
slug: string;
title: string;
description: string;
}
interface ComplexCategory {
kind: 'complex';
slug: string;
title: string;
description: string;
component: React.ComponentType; // custom renderer
}
type GuidelineCategory = PrimitiveCategory | ComplexCategory;2. Registry — declare categories with their class:
// stage.tsx
const GUIDELINE_CATEGORIES: GuidelineCategory[] = [
{ kind: 'primitive', slug: 'site', title: __('Site'), description: __("Describe your site's purpose...") },
{ kind: 'primitive', slug: 'copy', title: __('Copy'), description: __('Set your writing standards...') },
{ kind: 'primitive', slug: 'images', title: __('Images'), description: __('Outline your style...') },
{ kind: 'complex', slug: 'blocks', title: __('Blocks'), description: __('Create tailored guidelines...'), component: BlockGuidelines },
{ kind: 'primitive', slug: 'additional', title: __('Additional'), description: __('Add additional guidelines...') },
];3. Rendering — dispatch on kind, no slug checks:
{GUIDELINE_CATEGORIES.map((item) => (
<li key={item.slug}>
<GuidelineAccordion title={item.title} description={item.description} ...>
{item.kind === 'primitive' ? (
<GuidelineAccordionForm slug={item.slug} ... />
) : (
<item.component />
)}
</GuidelineAccordion>
</li>
))}The ternary still exists, but it's generic — it dispatches on kind, not on a specific slug. Adding a future complex category (say, "media" with its own custom UI) is just one more entry in the array with kind: 'complex' and a component.
4. State — separate storage by kind:
// types.ts
export interface ContentGuidelinesState {
id: number | null;
status: string | null;
primitives: Record<string, string>; // site, copy, images, additional
complex: Record<string, Record<string, string>>; // blocks, future ones...
}5. Store parsing — loop by kind, no special-case branches:
const PRIMITIVE_SLUGS = GUIDELINE_CATEGORIES
.filter(c => c.kind === 'primitive')
.map(c => c.slug);
const COMPLEX_SLUGS = GUIDELINE_CATEGORIES
.filter(c => c.kind === 'complex')
.map(c => c.slug);
function parseResponse(response: RestGuidelinesResponse) {
const cats = response.guideline_categories ?? {};
const primitives: Record<string, string> = {};
for (const slug of PRIMITIVE_SLUGS) {
primitives[slug] = cats[slug]?.guidelines ?? '';
}
const complex: Record<string, Record<string, string>> = {};
for (const slug of COMPLEX_SLUGS) {
complex[slug] = {};
const entries = cats[slug] ?? {};
for (const key in entries) {
complex[slug][key] = entries[key]?.guidelines ?? '';
}
}
return { id: response.id ?? null, status: response.status ?? null, primitives, complex };
}6. Selectors — one per kind class, not per category:
// Primitive categories
getGuideline(state, slug: string): string {
return state.primitives[slug] ?? '';
},
// Complex categories (blocks today, anything tomorrow)
getComplexGuidelines(state, slug: string): Record<string, string> {
return state.complex[slug] ?? {};
},
getComplexGuideline(state, slug: string, key: string): string {
return state.complex[slug]?.[key] ?? '';
},This will obviously introduce abstraction that we don't need today. So we shouldn't do it right now.
There was a problem hiding this comment.
+1 to this direction — the full registry pattern with kind discriminant is the right end state.
To keep things simple for now, could we start with just the type foundation? It fixes the immediate mixed Categories type issue without introducing the registry abstraction yet:
// types.ts — REST layer types that match the actual PHP response shapes
interface BaseGuideline {
guidelines?: string;
}
interface PrimitiveGuideline extends BaseGuideline {
label?: string;
}
type AdvancedGuideline = Record< string, BaseGuideline >;
interface RestGuidelineCategories {
site?: PrimitiveGuideline;
copy?: PrimitiveGuideline;
images?: PrimitiveGuideline;
additional?: PrimitiveGuideline;
blocks?: AdvancedGuideline;
}
interface RestGuidelinesResponse {
id: number;
status: string;
guideline_categories?: RestGuidelineCategories;
}
// Store state — two flat maps, no mixed union
interface ContentGuidelinesState {
id: number | null;
status: string | null;
categories: Record< string, string >; // primitive: slug -> text
blockGuidelines: Record< string, string >; // advanced: blockName -> text
}getGuideline() stays string-only, blocks get their own path via getBlockGuideline(name), and the REST types self-document the actual API shapes. When the full registry pattern is needed later, the REST types carry forward unchanged — only the store state evolves.
There was a problem hiding this comment.
@aagam-shah : let's take up your suggestion in a follow-up PR 👍
|
Nice work @aswasif007. This is a solid start! Here's what I found: Block list spacing
Ideally we can use some custom styles to achieve this: Data views controls
Adding block guidelines After you add a block guideline, can we exclude it from the list? The adding > edit behaviour was unexpected and really subtle:
Removing guidelines I mocked up some notices around removing guidlines in Figma. What do you think about adding those now or would you prefer to do that separately?
Transition Screen.Recording.2026-03-06.at.1.21.48.PM.mov |
|
Maybe one for @fditrapani I mentioned it over in the previous PR, but I think it'd be helpful to do some simple validation in a follow up. Or, if it's expected that users "clear" entries then provide a dedicated "Reset/Clear/Remove" button. Kapture.2026-03-09.at.16.33.00.mp4Also this point:
All can be follow ups. |
Filippo has shared designs for this which will be taken up in future PRs |
And omit dataviews search too when there are less than 6 items
Also, use textcontrol instead of combobox in edit mode
95c8815 to
86ba626
Compare
saroshaga
left a comment
There was a problem hiding this comment.
I left one small suggestion but I think it looks good to do! Thank you for working through all the feedback!
| </Notice.Title> | ||
| </Notice.Root> | ||
| ) } | ||
| <HStack |
There was a problem hiding this comment.
I'm seeing the pointer on mine. I forgot to mention this in my feedback but if you click the row/title, can we open the Edit guideline modal?
There was a problem hiding this comment.
if you click the row/title, can we open the Edit guideline modal?
Addressed here: c1af24a
There was a problem hiding this comment.
Wasif, I could also see the cursor when the submenu was closed but it was a pointer . Since the bar is not clickable, there's no use for the pointer style over that portion.
|
@fditrapani regarding
Should we show the confirmation modal on top of the edit modal? Or replace the edit modal with the confirmation modal? Since this PR is already approved, I will add it in a separate PR so that we are free to roll this one out. |
andrewserong
left a comment
There was a problem hiding this comment.
Functionality-wise this is testing pretty well for me! Just left a few comments, mostly for code quality things that would be good to fix up (either now or in a follow-up). The main one is that to match the PHP backend logic, it looks the list of blocks should probably be filtered?
Nice progress here!
ramonjd
left a comment
There was a problem hiding this comment.
Thanks again @aswasif007
Like @andrewserong I left only comments to keep in mind for follow ups.
I think the createSuccessNotice translation should be fixed in this PR though. Once that's done we can merge so folks can concentrate on the follow ups.
| getGuideline( | ||
| state: ContentGuidelinesState, | ||
| category: string | ||
| ): string | Record< string, string > { | ||
| return state.categories[ category ]; | ||
| }, | ||
| getAllGuidelines( | ||
| state: ContentGuidelinesState | ||
| ): Partial< Record< string, string > > { | ||
| getAllGuidelines( state: ContentGuidelinesState ): Categories { | ||
| return state.categories; | ||
| }, | ||
| getBlockGuidelines( | ||
| state: ContentGuidelinesState | ||
| ): Record< string, string > { | ||
| return state.categories.blocks; | ||
| }, | ||
| getBlockGuideline( | ||
| state: ContentGuidelinesState, | ||
| blockName: string | ||
| ): string { | ||
| return state.categories.blocks[ blockName ] ?? ''; | ||
| }, |
There was a problem hiding this comment.
Just a reminder that it might be wise to add test coverage in follow ups to guard against bugs/regressions.
👍🏻
| .components-modal__header-heading { | ||
| font-weight: 500; | ||
| } |
There was a problem hiding this comment.
I know this stuff is sometime unavoidable, but there's a general preference not to target component internals since components might change internal classes.
Just a fragility thing. Okay for now.
There was a problem hiding this comment.
Unfortunately we don't have a way to adjust css of the modal header.
There was a problem hiding this comment.
Another option for this header would be to leave it at its existing font-weight for consistency with other modals? The default across wp-admin and in Gutenberg appears to be 600 so I'm not sure if this needs to be different 🤔
(Again, not a blocker, just a thought!)
ramonjd
left a comment
There was a problem hiding this comment.
Thanks for the fast update @aswasif007
I gave it another smoke test.
I think an immediate follow up could be to look at @andrewserong's comments in relation to content blocks in the block list: #76187 (comment)
Anyway, let's get this in so folks can iterate. Cheers.
|
Thanks @ramonjd and @andrewserong for the reviews and approval! I've addressed the comments as well. |
Nicely done, thanks for updating all of that before merge! 🚀 |
#76155) * Add block guidelines management * Fix spacing of the dataviews * Remove dataviews sort and filter And omit dataviews search too when there are less than 6 items * Exclude blocks that already have guidelines from add-guideline list Also, use textcontrol instead of combobox in edit mode * Show confirmation modal when deleting block guideline from table * Improve save/delete logic * Use base-style variable * Fix bootstrapBlockRegistry() placement * Add types * Use hstack wrapper for add-block-guideline btn * Do not remove block-key when removing block guideline This is because the backend expects empty string in order to delete a block * Fix issue with empty page after deleting rows * Fix mistake with wrapping up action-text in double quote * Add . after success notice text * Remove undefined keys from block entries * Add clarifying comment for setting empty string to delete a block guideline * Content Guidelines: Add actions section UI * Adds simple import and export function * Content Guidelines: Add manage access and revision screens * Content Guidelines: Search in revision history screen * Content guidelines: Use filterSortAndPaginate from DataViews to implement search in guidelines revision history screen * Content guidelines: Hide Manage Access action button for MVP * Improve spacing * Content guidelines: Use DataViews inbuilt filtering * Refetch content-guidelines after restoring a revision * Improve revision-history page styles * Content guidelines: remove manage-access from MVP * Content Guidelines: Use Notice as error message component instead of Snackbar * Content Guidelines: Addresses PR feedback: 1. Uses downloadBlob from wordpress/blob 2. Adds isRTL support to the chevron 3. Uses dateI18n to format date on revision screen 4. Adds efficient catching of error during revision restoration * Content Guidelines: Align author to the right of revision history table * Content guidelines: Rebase with #76187 * Content guideliens: Adds confirmation modal to import guidelines * Content guidelines: Updates modal sequence and copy * Content guidelines: Addresses PR feedback to:1. Fix unnecessary try/catch 2. Remov unnecessary lint escapes 3. Use memoisation to avoid rerenders * Content guidelines: Remove unnecessary overrides in CSS, where possible * Content guidelines: Use memoisation in filter and pagination to avoid rerenders * Content guidelines: Revert changes done by incorrect rebase and merge * Content guidelines: Revert unnecessary changes brought in incorrect rebase * Content guidelines: Handle guidelines import error correctly by restoring previous version * Content guidelines: Pre-emptively add override for wordpress/ui Notice component * Content guidelines: Update copy for content guidelines import error * Guidelines: Rename "Content Guidelines" to "Guidelines" in all user-facing strings Rename the feature from "Content Guidelines" to "Guidelines" across all user-facing strings (page title, menu item, experiments toggle, notices, modals, aria labels, error messages) while keeping variable and file names unchanged. This aligns with the decision to use the shorter name while the feature remains experimental. Co-Authored-By: Claude Opus 4.6 <[email protected]> * Content guidelines: Change the text 'content guidelines' to 'guidelines' in user facing interfaces * Content Guidelines: Remove lint override which is not live yet * Content guidelines: Updates copy of import error notice and removes moving revision history columns feature * Content guidelines: Updates snackbar/toast message copy --------- Co-authored-by: Ahmed Sayeed Wasif <[email protected]> Co-authored-by: Aagam Shah <[email protected]> Co-authored-by: Claude Opus 4.6 <[email protected]> Co-authored-by: aswasif007 <[email protected]> Co-authored-by: iamchughmayank <[email protected]> Co-authored-by: aagam-shah <[email protected]> Co-authored-by: fditrapani <[email protected]> Co-authored-by: saroshaga <[email protected]> Co-authored-by: mirka <[email protected]> Co-authored-by: Jameswlepage <[email protected]>
|
I just cherry-picked this PR to the release/22.7 branch to get it included in the next release: c7b5c09 |
Co-authored-by: aswasif007 <[email protected]> Co-authored-by: andrewserong <[email protected]> Co-authored-by: ramonjd <[email protected]> Co-authored-by: saroshaga <[email protected]> Co-authored-by: aagam-shah <[email protected]> Co-authored-by: fditrapani <[email protected]>
#76155) * Add block guidelines management * Fix spacing of the dataviews * Remove dataviews sort and filter And omit dataviews search too when there are less than 6 items * Exclude blocks that already have guidelines from add-guideline list Also, use textcontrol instead of combobox in edit mode * Show confirmation modal when deleting block guideline from table * Improve save/delete logic * Use base-style variable * Fix bootstrapBlockRegistry() placement * Add types * Use hstack wrapper for add-block-guideline btn * Do not remove block-key when removing block guideline This is because the backend expects empty string in order to delete a block * Fix issue with empty page after deleting rows * Fix mistake with wrapping up action-text in double quote * Add . after success notice text * Remove undefined keys from block entries * Add clarifying comment for setting empty string to delete a block guideline * Content Guidelines: Add actions section UI * Adds simple import and export function * Content Guidelines: Add manage access and revision screens * Content Guidelines: Search in revision history screen * Content guidelines: Use filterSortAndPaginate from DataViews to implement search in guidelines revision history screen * Content guidelines: Hide Manage Access action button for MVP * Improve spacing * Content guidelines: Use DataViews inbuilt filtering * Refetch content-guidelines after restoring a revision * Improve revision-history page styles * Content guidelines: remove manage-access from MVP * Content Guidelines: Use Notice as error message component instead of Snackbar * Content Guidelines: Addresses PR feedback: 1. Uses downloadBlob from wordpress/blob 2. Adds isRTL support to the chevron 3. Uses dateI18n to format date on revision screen 4. Adds efficient catching of error during revision restoration * Content Guidelines: Align author to the right of revision history table * Content guidelines: Rebase with #76187 * Content guideliens: Adds confirmation modal to import guidelines * Content guidelines: Updates modal sequence and copy * Content guidelines: Addresses PR feedback to:1. Fix unnecessary try/catch 2. Remov unnecessary lint escapes 3. Use memoisation to avoid rerenders * Content guidelines: Remove unnecessary overrides in CSS, where possible * Content guidelines: Use memoisation in filter and pagination to avoid rerenders * Content guidelines: Revert changes done by incorrect rebase and merge * Content guidelines: Revert unnecessary changes brought in incorrect rebase * Content guidelines: Handle guidelines import error correctly by restoring previous version * Content guidelines: Pre-emptively add override for wordpress/ui Notice component * Content guidelines: Update copy for content guidelines import error * Guidelines: Rename "Content Guidelines" to "Guidelines" in all user-facing strings Rename the feature from "Content Guidelines" to "Guidelines" across all user-facing strings (page title, menu item, experiments toggle, notices, modals, aria labels, error messages) while keeping variable and file names unchanged. This aligns with the decision to use the shorter name while the feature remains experimental. Co-Authored-By: Claude Opus 4.6 <[email protected]> * Content guidelines: Change the text 'content guidelines' to 'guidelines' in user facing interfaces * Content Guidelines: Remove lint override which is not live yet * Content guidelines: Updates copy of import error notice and removes moving revision history columns feature * Content guidelines: Updates snackbar/toast message copy --------- Co-authored-by: Ahmed Sayeed Wasif <[email protected]> Co-authored-by: Aagam Shah <[email protected]> Co-authored-by: Claude Opus 4.6 <[email protected]> Co-authored-by: aswasif007 <[email protected]> Co-authored-by: iamchughmayank <[email protected]> Co-authored-by: aagam-shah <[email protected]> Co-authored-by: fditrapani <[email protected]> Co-authored-by: saroshaga <[email protected]> Co-authored-by: mirka <[email protected]> Co-authored-by: Jameswlepage <[email protected]>










What?
Related issue: #75260
Adds a new Blocks section to the Content Guidelines page, allowing administrators to define and manage tailored guidelines for specific block types.
Why?
The Content Guidelines feature currently supports site-wide categories (Site, Copy, Images, Additional), but has no way to express guidelines scoped to a particular block type. This is useful for teams who want to constrain or document how specific blocks should be used — for example, requiring alt text on image blocks or restricting heading hierarchy.
How?
blockscategory to the guidelines data model and REST payload. Each entry maps a block name (e.g.core/image) to a guidelines string.fetchBlockTypes()inapi.tsto load all registered top-level block types via/wp/v2/block-typesand store them as searchable options.BlockGuidelinescomponent that renders aDataViewslist of all configured block guidelines, with each row showing the block's icon, name, and guideline text. Row-level Edit and Delete actions are available.BlockGuidelineModalcomponent for adding or editing a block guideline. It uses aComboboxControlto search and select a block, and aTextareaControlfor the guideline content. The modal title and submit label adapt between add and edit modes.blockIconMapinutils.tsx— a mapping of core block names to their@wordpress/iconsequivalents, used for visual display in the list.blocksstate,SET_BLOCK_GUIDELINE/SET_BLOCK_TYPESactions, andgetBlockGuideline/getBlockGuidelines/getBlockTypesselectors.Testing Instructions
Testing Instructions for Keyboard
Enterto expand it.Enterto open the modal.Enterto save.Screenshots