Skip to content
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
bea207a
Bootstrap core blocks registry with content-guidelines page
aswasif007 Mar 6, 2026
66fe8fc
Update store and api for block guidelines
aswasif007 Mar 6, 2026
2ef2bda
Add block guidelines management
aswasif007 Mar 6, 2026
5463643
Simplify blocks registry script
aswasif007 Mar 6, 2026
cff10f9
Fix spacing of the dataviews
aswasif007 Mar 9, 2026
a36c371
Remove dataviews sort and filter
aswasif007 Mar 9, 2026
d0e0e0f
Exclude blocks that already have guidelines from add-guideline list
aswasif007 Mar 9, 2026
3665b68
Show confirmation modal when deleting block guideline from table
aswasif007 Mar 9, 2026
f229dee
Improve save/delete logic
aswasif007 Mar 9, 2026
c91aa33
Use base-style variable
aswasif007 Mar 9, 2026
d9f0b24
Fix bootstrapBlockRegistry() placement
aswasif007 Mar 9, 2026
86ba626
Add types
aswasif007 Mar 9, 2026
26a2db7
Use hstack wrapper for add-block-guideline btn
aswasif007 Mar 9, 2026
3e2a17b
Do not remove block-key when removing block guideline
aswasif007 Mar 9, 2026
3776ce7
Fix issue with empty page after deleting rows
aswasif007 Mar 9, 2026
c098dd7
Fix mistake with wrapping up action-text in double quote
aswasif007 Mar 9, 2026
d6a04c0
Add . after success notice text
aswasif007 Mar 9, 2026
7688313
Remove undefined keys from block entries
aswasif007 Mar 9, 2026
11d1558
Add clarifying comment for setting empty string to delete a block gui…
aswasif007 Mar 9, 2026
c1af24a
Trigger modal when block guideline row is clicked
aswasif007 Mar 10, 2026
3d89fde
Fix translation for block guideline notice
aswasif007 Mar 10, 2026
d9b3a41
Set acessibleWhenDisabled to modal buttons
aswasif007 Mar 10, 2026
7b00b11
Fix type issues and replace for..in loop
aswasif007 Mar 10, 2026
2df6c13
Bootstrap block-registry in the beforeLoad attrib
aswasif007 Mar 10, 2026
a8ddd23
Use a div-wrapper around icon to avoid having to target inner css
aswasif007 Mar 10, 2026
d16afc0
Filter out the non-content blocks
aswasif007 Mar 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions lib/experimental/content-guidelines/load.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
*/

add_action( 'admin_menu', 'gutenberg_register_content_guidelines_settings_submenu', 10 );
add_action( 'admin_enqueue_scripts', 'gutenberg_content_guidelines_enqueue_block_registry_scripts', 5 );

/**
* Registers the Content Guidelines submenu item under Settings.
Expand All @@ -21,3 +22,22 @@ function gutenberg_register_content_guidelines_settings_submenu() {
'gutenberg_content_guidelines_wp_admin_render_page'
);
}

/**
* Enqueues wp-block-library on the Content Guidelines admin page so
* registerCoreBlocks() is available when the app bootstraps the block
* registry (Core blocks only) on the client.
*
* Priority 5 ensures this runs before the main asset enqueue (priority 10).
*/
function gutenberg_content_guidelines_enqueue_block_registry_scripts( $hook_suffix ) {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}

if ( 'settings_page_content-guidelines-wp-admin' !== $hook_suffix ) {
return;
}

wp_enqueue_script( 'wp-block-library' );
}
2 changes: 2 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

26 changes: 14 additions & 12 deletions routes/content-guidelines/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,12 @@
*/
import apiFetch from '@wordpress/api-fetch';
import { dispatch, select } from '@wordpress/data';
import { __ } from '@wordpress/i18n';
import { store as noticesStore } from '@wordpress/notices';

/**
* Internal dependencies
*/
import { STORE_NAME } from './store';
import type { RestGuidelinesResponse } from './types';
import type { Categories, RestGuidelinesResponse } from './types';

export async function fetchContentGuidelines(): Promise< RestGuidelinesResponse > {
const { setFromResponse } = dispatch( STORE_NAME ) as {
Expand All @@ -30,16 +28,18 @@ export async function saveContentGuidelines(): Promise< RestGuidelinesResponse >
// @ts-ignore
const { setFromResponse } = dispatch( STORE_NAME );

const guidelinesStore = select( STORE_NAME ) as {
const guidelinesStore = select( STORE_NAME ) as unknown as {
getId: () => number | null;
getStatus: () => string | null;
getAllGuidelines: () => Partial< Record< string, string > >;
getGuideline: ( category: string ) => string;
getAllGuidelines: () => Categories;
getBlockGuidelines: () => Record< string, string >;
getGuideline: ( category: string ) => string | Record< string, string >;
};

const id = guidelinesStore.getId();
const status = guidelinesStore.getStatus() || 'draft';
const categories = guidelinesStore.getAllGuidelines();
const blockGuidelines = guidelinesStore.getBlockGuidelines();

const data = {
id,
Expand All @@ -57,11 +57,17 @@ export async function saveContentGuidelines(): Promise< RestGuidelinesResponse >
additional: {
guidelines: categories.additional,
},
blocks: Object.fromEntries(
Object.entries( blockGuidelines ).map(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed here

@aswasif007 aswasif007 Mar 9, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

// Handle block-specific guidelines as individual meta keys.
if ( isset( $categories['blocks'] ) && is_array( $categories['blocks'] ) ) {
foreach ( $categories['blocks'] as $block_name => $block_data ) {
$meta_key = Gutenberg_Content_Guidelines_Post_Type::block_name_to_meta_key( $block_name );
$value = isset( $block_data['guidelines'] ) ? $block_data['guidelines'] : '';
if ( ! empty( $value ) ) {
update_post_meta( $post_id, $meta_key, $value );
} else {
delete_post_meta( $post_id, $meta_key );
}
}
}

( [ blockName, guidelines ] ) => [
blockName,
{ guidelines },
]
)
),
},
};

const { createSuccessNotice } = dispatch( noticesStore );

const path = id
? `/wp/v2/content-guidelines/${ id }`
: '/wp/v2/content-guidelines';
Expand All @@ -75,9 +81,5 @@ export async function saveContentGuidelines(): Promise< RestGuidelinesResponse >

setFromResponse( response );

createSuccessNotice( __( 'Content guidelines saved.' ), {
type: 'snackbar',
} );

return response;
}
24 changes: 24 additions & 0 deletions routes/content-guidelines/bootstrap-block-registry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/**
* WordPress dependencies
*/
import { dispatch } from '@wordpress/data';
import { store as blocksStore } from '@wordpress/blocks';
import { registerCoreBlocks } from '@wordpress/block-library';

let bootstrapped = false;

/**
* Bootstraps the block registry with Core blocks only.
* Used on the Content Guidelines admin page so block icons (and later
* 3rd party/experimental block support) are available without loading
* the full block editor.
*/
export function bootstrapBlockRegistry(): void {
if ( bootstrapped ) {
return;
}
bootstrapped = true;

dispatch( blocksStore ).reapplyBlockTypeFilters();
registerCoreBlocks();
}
13 changes: 13 additions & 0 deletions routes/content-guidelines/components/block-guideline-modal.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
@use "@wordpress/base-styles/variables" as *;

.block-guideline-modal {
width: min(100%, 660px);

.components-modal__header-heading {
font-weight: 500;
}
Comment on lines +6 to +8

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately we don't have a way to adjust css of the modal header.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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!)


&__actions {
margin-top: $grid-unit-10;
}
}
207 changes: 207 additions & 0 deletions routes/content-guidelines/components/block-guideline-modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
/* @jsx createElement */

/**
* WordPress dependencies
*/
import {
Button,
ComboboxControl,
Modal,
TextControl,
TextareaControl,
__experimentalHStack as HStack,
__experimentalVStack as VStack,
} from '@wordpress/components';
import { __, sprintf } from '@wordpress/i18n';
import { Notice } from '@wordpress/ui';
import { createElement, useMemo, useState } from '@wordpress/element';
import { useDispatch, useSelect } from '@wordpress/data';
import { store as blocksStore } from '@wordpress/blocks';
import { store as noticesStore } from '@wordpress/notices';

/**
* Internal dependencies
*/
import { saveContentGuidelines } from '../api';
import { STORE_NAME } from '../store';
import './block-guideline-modal.scss';

interface BlockGuidelineModalProps {
closeModal: () => void;
initialBlock?: string;
}

export default function BlockGuidelineModal( {
closeModal,
initialBlock,
}: BlockGuidelineModalProps ) {
const [ selectedBlock, setSelectedBlock ] = useState< string | undefined >(
initialBlock
);

const [ isSaving, setIsSaving ] = useState( false );
const [ error, setError ] = useState< string | null >( null );

const blockGuidelines = useSelect(
// @ts-ignore
( select ) => select( STORE_NAME ).getBlockGuidelines(),
[]
);

const isEditing = !! initialBlock;

const currentGuideline = blockGuidelines[ selectedBlock ] ?? '';
const [ guidelineText, setGuidelineText ] = useState( currentGuideline );

const blockOptions = useSelect(
// @ts-ignore
( select ) => select( blocksStore ).getBlockTypes(),
[]
);

const availableBlockOptions = useMemo( () => {
const set = new Set( Object.keys( blockGuidelines ) );
if ( initialBlock ) {
set.delete( initialBlock );
}
if ( selectedBlock ) {
set.delete( selectedBlock );
}

return blockOptions
.filter( ( block ) => ! set.has( block.name ) )
.map( ( block ) => ( {
value: block.name,
label: block.title,
} ) );
}, [ blockGuidelines, blockOptions, initialBlock, selectedBlock ] );
Comment thread
aswasif007 marked this conversation as resolved.

const selectedBlockLabel = useMemo(
() =>
blockOptions.find( ( block ) => block.name === selectedBlock )
?.title || '',
[ blockOptions, selectedBlock ]
);

const { setBlockGuideline } = useDispatch( STORE_NAME );
const { createSuccessNotice } = useDispatch( noticesStore );

const handleSave = ( value: string ) => {
value = value.trim();
if ( ! selectedBlock ) {
return;
}

setIsSaving( true );
const oldValue = blockGuidelines[ selectedBlock ];
setBlockGuideline( selectedBlock, value );
saveContentGuidelines()
.then( () => {
setError( null );
createSuccessNotice(
value
? __( 'Block guideline saved.' )
: __( 'Block guideline removed.' ),
{ type: 'snackbar' }
);
Comment thread
aswasif007 marked this conversation as resolved.
closeModal();
} )
.catch( ( e: Error ) => {
setError( e.message );
setBlockGuideline( selectedBlock, oldValue );
} )
.finally( () => setIsSaving( false ) );
};

const canSubmit = selectedBlock && guidelineText.trim().length > 0;

let submitButtonLabel: string = __( 'Add guideline' );
if ( isSaving ) {
submitButtonLabel = __( 'Saving…' );
} else if ( isEditing ) {
submitButtonLabel = __( 'Update guideline' );
}

return (
<Modal
className="block-guideline-modal"
title={
isEditing
? __( 'Edit block guidelines' )
: __( 'Add block guidelines' )
}
onRequestClose={ closeModal }
>
<VStack spacing={ 4 }>
{ isEditing ? (
<TextControl
__next40pxDefaultSize
label={ __( 'Block' ) }
value={ selectedBlockLabel }
onChange={ () => {} }
disabled
/>
) : (
<ComboboxControl
__next40pxDefaultSize
label={ __( 'Block' ) }
options={ availableBlockOptions }
value={ selectedBlock }
onChange={ ( value ) =>
setSelectedBlock( value ?? undefined )
}
placeholder={ __( 'Search for a block…' ) }
/>
) }
<TextareaControl
label={ __( 'Guideline text' ) }
value={ guidelineText }
onChange={ setGuidelineText }
placeholder={ __(
'Enter guidelines for how this block should be used…'
) }
rows={ 6 }
/>
{ error && (
<Notice.Root intent="error">
<Notice.Title>
{ sprintf(
/* translators: %s: Error message. */
__( 'Error: %s' ),
error
) }
</Notice.Title>
</Notice.Root>
) }
<HStack

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor: can the cursor be styled as pointer only for the submenu ... button?

The entire bar need not have a pointer cursor.

(doesn't show up in the screenshot)

Image

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sarosh is referring to the cursor when submenu is open.
Screenshot 2026-03-10 at 6 58 35 AM

I am afraid this needs to be fixed in the DataView component. I think for such modifications in Gutenberg, we are supposed to create a github-issue in the repository first.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if you click the row/title, can we open the Edit guideline modal?

Addressed here: c1af24a

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

justify="flex-end"
spacing={ 2 }
className="block-guideline-modal__actions"
>
{ isEditing && (
<Button
variant="tertiary"
isDestructive
// We need to pass an empty string to remove the guideline.
// This is because the API will only remove the guideline if the value is an empty string.
onClick={ () => handleSave( '' ) }
disabled={ isSaving }
Comment thread
aswasif007 marked this conversation as resolved.
accessibleWhenDisabled
>
{ __( 'Remove' ) }
</Button>
) }
<Button
variant="primary"
onClick={ () => handleSave( guidelineText ) }
disabled={ ! canSubmit || isSaving }
isBusy={ isSaving }
accessibleWhenDisabled
>
{ submitButtonLabel }
</Button>
</HStack>
</VStack>
</Modal>
);
}
32 changes: 32 additions & 0 deletions routes/content-guidelines/components/block-guidelines.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
@use "@wordpress/base-styles/variables" as *;

.block-guidelines {
margin-top: $grid-unit-30;

.dataviews-view-list__media-wrapper {
display: flex;
align-items: center;
justify-content: center;
}

.dataviews-view-list__field-wrapper {
display: flex;
justify-content: center;
}

.dataviews-wrapper {
margin-right: -$grid-unit-30;
margin-left: -$grid-unit-30;
}

.dataviews-search {
margin-right: $grid-unit-30;
margin-left: $grid-unit-30;
}
}

.block-guidelines__remove-modal {
.components-modal__header-heading {
font-weight: 500;
}
}
Loading
Loading