Repository navigation
Content Guidelines: Add block guidelines management #76187
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 23 commits
bea207a
66fe8fc
2ef2bda
5463643
cff10f9
a36c371
d0e0e0f
3665b68
f229dee
c91aa33
d9f0b24
86ba626
26a2db7
3e2a17b
3776ce7
c098dd7
d6a04c0
7688313
11d1558
c1af24a
3d89fde
d9b3a41
7b00b11
2df6c13
a8ddd23
d16afc0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| 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(); | ||
| } |
| 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
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 (Again, not a blocker, just a thought!) |
||
|
|
||
| &__actions { | ||
| margin-top: $grid-unit-10; | ||
| } | ||
| } | ||
| 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 ] ); | ||
|
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' } | ||
| ); | ||
|
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 | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Addressed here: c1af24a There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
| 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 } | ||
|
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> | ||
| ); | ||
| } | ||
| 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; | ||
| } | ||
| } |


There was a problem hiding this comment.
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:110andblock-guideline-modal.tsx:87usesetBlockGuideline(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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Addressed here
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
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:
gutenberg/lib/experimental/content-guidelines/class-gutenberg-content-guidelines-rest-controller.php
Lines 401 to 413 in 8b4fd02