From d021369637c7dae67f6cdb30838ec74b2ea2b43e Mon Sep 17 00:00:00 2001 From: Luigi Teschio Date: Fri, 1 Nov 2024 15:20:07 +0100 Subject: [PATCH 1/2] Implement validation slug --- package-lock.json | 1 + packages/dataviews/src/normalize-fields.ts | 7 ++-- packages/dataviews/src/types.ts | 8 +++-- packages/dataviews/src/validation.ts | 3 +- packages/fields/package.json | 1 + packages/fields/src/fields/slug/index.ts | 6 +++- packages/fields/src/fields/slug/slug-edit.tsx | 32 ++++++++++++++----- packages/fields/src/fields/slug/style.scss | 12 +++++++ 8 files changed, 53 insertions(+), 17 deletions(-) diff --git a/package-lock.json b/package-lock.json index 24812eff02ef8a..1ade050db66e2d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -54700,6 +54700,7 @@ "@wordpress/warning": "*", "change-case": "4.1.2", "client-zip": "^2.4.5", + "clsx": "^2.1.1", "remove-accents": "^0.5.0" }, "engines": { diff --git a/packages/dataviews/src/normalize-fields.ts b/packages/dataviews/src/normalize-fields.ts index 5ef219e45a4787..6e9d3770e3d47c 100644 --- a/packages/dataviews/src/normalize-fields.ts +++ b/packages/dataviews/src/normalize-fields.ts @@ -38,11 +38,8 @@ export function normalizeFields< Item >( const isValid = field.isValid ?? - function isValid( item, context ) { - return fieldTypeDefinition.isValid( - getValue( { item } ), - context - ); + function isValid( value, item, context ) { + return fieldTypeDefinition.isValid( value, context ); }; const Edit = getControl( field, fieldTypeDefinition ); diff --git a/packages/dataviews/src/types.ts b/packages/dataviews/src/types.ts index 0ea0965704d18c..6d153716d5e1f4 100644 --- a/packages/dataviews/src/types.ts +++ b/packages/dataviews/src/types.ts @@ -121,7 +121,11 @@ export type Field< Item > = { /** * Callback used to validate the field. */ - isValid?: ( item: Item, context?: ValidationContext ) => boolean; + isValid?: ( + value: any, + item?: Item, + context?: ValidationContext + ) => boolean; /** * Callback used to decide if a field should be displayed. @@ -167,7 +171,7 @@ export type NormalizedField< Item > = Field< Item > & { render: ComponentType< DataViewRenderFieldProps< Item > >; Edit: ComponentType< DataFormControlProps< Item > >; sort: ( a: Item, b: Item, direction: SortDirection ) => number; - isValid: ( item: Item, context?: ValidationContext ) => boolean; + isValid: ( value: any, item: Item, context?: ValidationContext ) => boolean; enableHiding: boolean; enableSorting: boolean; }; diff --git a/packages/dataviews/src/validation.ts b/packages/dataviews/src/validation.ts index 41969a7960af65..39f2ef28df71a4 100644 --- a/packages/dataviews/src/validation.ts +++ b/packages/dataviews/src/validation.ts @@ -13,6 +13,7 @@ export function isItemValid< Item >( fields.filter( ( { id } ) => !! form.fields?.includes( id ) ) ); return _fields.every( ( field ) => { - return field.isValid( item, { elements: field.elements } ); + const value = field.getValue( { item } ); + return field.isValid( value, item, { elements: field.elements } ); } ); } diff --git a/packages/fields/package.json b/packages/fields/package.json index e0b7125cf3e4eb..4d42bb0c500374 100644 --- a/packages/fields/package.json +++ b/packages/fields/package.json @@ -55,6 +55,7 @@ "@wordpress/warning": "*", "change-case": "4.1.2", "client-zip": "^2.4.5", + "clsx": "^2.1.1", "remove-accents": "^0.5.0" }, "peerDependencies": { diff --git a/packages/fields/src/fields/slug/index.ts b/packages/fields/src/fields/slug/index.ts index 4e81996ceaa6e8..e5f65aab6777f6 100644 --- a/packages/fields/src/fields/slug/index.ts +++ b/packages/fields/src/fields/slug/index.ts @@ -10,12 +10,16 @@ import type { BasePost } from '../../types'; import { __ } from '@wordpress/i18n'; import SlugEdit from './slug-edit'; import SlugView from './slug-view'; +import { getSlug } from './utils'; const slugField: Field< BasePost > = { id: 'slug', type: 'text', label: __( 'Slug' ), - getValue: ( { item } ) => item.slug, + getValue: ( { item } ) => getSlug( item ), + isValid: ( value ) => { + return ( value && value.length > 0 ) || false; + }, Edit: SlugEdit, render: SlugView, }; diff --git a/packages/fields/src/fields/slug/slug-edit.tsx b/packages/fields/src/fields/slug/slug-edit.tsx index aad6610550069c..23a9db7c706c66 100644 --- a/packages/fields/src/fields/slug/slug-edit.tsx +++ b/packages/fields/src/fields/slug/slug-edit.tsx @@ -11,17 +11,21 @@ import { import { copySmall } from '@wordpress/icons'; import { useCopyToClipboard, useInstanceId } from '@wordpress/compose'; import { useDispatch } from '@wordpress/data'; -import { useCallback, useEffect, useRef } from '@wordpress/element'; +import { useCallback, useEffect, useRef, useState } from '@wordpress/element'; import { store as noticesStore } from '@wordpress/notices'; import { safeDecodeURIComponent } from '@wordpress/url'; import type { DataFormControlProps } from '@wordpress/dataviews'; import { __ } from '@wordpress/i18n'; +/** + * External dependencies + */ +import clsx from 'clsx'; + /** * Internal dependencies */ import type { BasePost } from '../../types'; -import { getSlug } from './utils'; const SlugEdit = ( { field, @@ -30,7 +34,10 @@ const SlugEdit = ( { }: DataFormControlProps< BasePost > ) => { const { id } = field; - const slug = field.getValue( { item: data } ) || getSlug( data ); + const slug = field.getValue( { item: data } ); + + const [ isValid, setIsValid ] = useState( true ); + const permalinkTemplate = data.permalink_template || ''; const PERMALINK_POSTNAME_REGEX = /%(?:postname|pagename)%/; const [ prefix, suffix ] = permalinkTemplate.split( @@ -40,7 +47,7 @@ const SlugEdit = ( { const permalinkSuffix = suffix; const isEditable = PERMALINK_POSTNAME_REGEX.test( permalinkTemplate ); const originalSlugRef = useRef( slug ); - const slugToDisplay = slug || originalSlugRef.current; + const slugToDisplay = slug; const permalink = isEditable ? `${ permalinkPrefix }${ slugToDisplay }${ permalinkSuffix }` : safeDecodeURIComponent( data.link || '' ); @@ -52,11 +59,13 @@ const SlugEdit = ( { }, [ slug ] ); const onChangeControl = useCallback( - ( newValue?: string ) => + ( newValue?: string ) => { + setIsValid( field.isValid( newValue ?? '' ) ); onChange( { [ id ]: newValue, - } ), - [ id, onChange ] + } ); + }, + [ field, id, onChange ] ); const { createNotice } = useDispatch( noticesStore ); @@ -106,7 +115,9 @@ const SlugEdit = ( { autoComplete="off" spellCheck="false" type="text" - className="fields-controls__slug-input" + className={ clsx( 'fields-controls__slug-input', { + 'fields-controls__slug-input--invalid': ! isValid, + } ) } onChange={ ( newValue?: string ) => { onChangeControl( newValue ); } } @@ -117,6 +128,11 @@ const SlugEdit = ( { } } aria-describedby={ postUrlSlugDescriptionId } /> + { ! isValid && ( +
+ { __( 'The slug is invalid.' ) } +
+ ) }
{ __( 'Permalink:' ) } diff --git a/packages/fields/src/fields/slug/style.scss b/packages/fields/src/fields/slug/style.scss index aad99b3731e9eb..150072e32926c7 100644 --- a/packages/fields/src/fields/slug/style.scss +++ b/packages/fields/src/fields/slug/style.scss @@ -7,6 +7,18 @@ padding-inline-start: 0 !important; } + .fields-controls__slug-input--invalid { + .components-input-control__backdrop, + .components-input-control__backdrop:focus-within { + border-color: $alert-red !important; + box-shadow: 0 0 0 0.5px $alert-red !important; + } + } + + .fields-controls__slug-error { + color: $alert-red; + } + .fields-controls__slug-help-link { word-break: break-word; } From 04e08cff21d8bead6b52a8af96946151b1dcd126 Mon Sep 17 00:00:00 2001 From: Luigi Teschio Date: Thu, 7 Nov 2024 17:11:39 +0100 Subject: [PATCH 2/2] WIP --- .../edit-site/src/components/app/index.js | 5 +++- .../src/components/post-edit/context.js | 23 +++++++++++++++++++ .../src/components/post-edit/index.js | 12 +++++++++- .../src/components/save-hub/index.js | 6 +++++ packages/fields/src/actions/reorder-page.tsx | 1 + packages/fields/src/fields/slug/slug-edit.tsx | 8 ++++--- packages/fields/src/fields/slug/slug-view.tsx | 11 ++++++++- packages/fields/src/fields/slug/utils.ts | 4 +--- 8 files changed, 61 insertions(+), 9 deletions(-) create mode 100644 packages/edit-site/src/components/post-edit/context.js diff --git a/packages/edit-site/src/components/app/index.js b/packages/edit-site/src/components/app/index.js index 133a376c9c246d..edffbee2b11fc7 100644 --- a/packages/edit-site/src/components/app/index.js +++ b/packages/edit-site/src/components/app/index.js @@ -23,6 +23,7 @@ import useInitEditedEntityFromURL from '../sync-state-with-url/use-init-edited-e import useActiveRoute from '../layout/router'; import useSetCommandContext from '../../hooks/commands/use-set-command-context'; import { useRegisterSiteEditorRoutes } from '../site-editor-routes'; +import { PostEditProvider } from '../post-edit/context'; const { RouterProvider } = unlock( routerPrivateApis ); const { GlobalStylesProvider } = unlock( editorPrivateApis ); @@ -59,7 +60,9 @@ export default function App() { - + + + diff --git a/packages/edit-site/src/components/post-edit/context.js b/packages/edit-site/src/components/post-edit/context.js new file mode 100644 index 00000000000000..6f16b871bf60f4 --- /dev/null +++ b/packages/edit-site/src/components/post-edit/context.js @@ -0,0 +1,23 @@ +/** + * WordPress dependencies + */ +import { createContext, useContext, useState } from '@wordpress/element'; + +export const PostEditContext = createContext( { + isValidForm: false, + setIsValidForm: () => {}, +} ); + +export const PostEditProvider = ( { children } ) => { + const [ isValidForm, setIsValidForm ] = useState( false ); + + return ( + + { children } + + ); +}; + +export const usePostEditContext = () => { + return useContext( PostEditContext ); +}; diff --git a/packages/edit-site/src/components/post-edit/index.js b/packages/edit-site/src/components/post-edit/index.js index fbff29ed67afa1..facae5280c1fcc 100644 --- a/packages/edit-site/src/components/post-edit/index.js +++ b/packages/edit-site/src/components/post-edit/index.js @@ -7,7 +7,7 @@ import clsx from 'clsx'; * WordPress dependencies */ import { __ } from '@wordpress/i18n'; -import { DataForm } from '@wordpress/dataviews'; +import { DataForm, isItemValid } from '@wordpress/dataviews'; import { useDispatch, useSelect } from '@wordpress/data'; import { store as coreDataStore } from '@wordpress/core-data'; import { __experimentalVStack as VStack } from '@wordpress/components'; @@ -20,6 +20,7 @@ import { privateApis as editorPrivateApis } from '@wordpress/editor'; import Page from '../page'; import usePostFields from '../post-fields'; import { unlock } from '../../lock-unlock'; +import { usePostEditContext } from './context'; const { PostCardPanel } = unlock( editorPrivateApis ); @@ -96,6 +97,9 @@ function PostEditForm( { postType, postId } ) { } ), [ ids ] ); + + const { setIsValidForm } = usePostEditContext(); + const onChange = ( edits ) => { for ( const id of ids ) { if ( @@ -120,6 +124,12 @@ function PostEditForm( { postType, postId } ) { ...edits, } ) ); } + const isValidForm = isItemValid( + { ...record, ...edits }, + fields, + form + ); + setIsValidForm( isValidForm ); } }; useEffect( () => { diff --git a/packages/edit-site/src/components/save-hub/index.js b/packages/edit-site/src/components/save-hub/index.js index 61f4df072a7db8..6c12f13323a63d 100644 --- a/packages/edit-site/src/components/save-hub/index.js +++ b/packages/edit-site/src/components/save-hub/index.js @@ -11,6 +11,7 @@ import { check } from '@wordpress/icons'; */ import SaveButton from '../save-button'; import { isPreviewingTheme } from '../../utils/is-previewing-theme'; +import { usePostEditContext } from '../post-edit/context'; export default function SaveHub() { const { isDisabled, isSaving } = useSelect( ( select ) => { @@ -27,6 +28,11 @@ export default function SaveHub() { ( ! dirtyEntityRecords.length && ! isPreviewingTheme() ), }; }, [] ); + + const { isValidForm } = usePostEditContext(); + + console.log( isValidForm ); + return ( diff --git a/packages/fields/src/fields/slug/slug-edit.tsx b/packages/fields/src/fields/slug/slug-edit.tsx index 23a9db7c706c66..d8ebe49a0fc6eb 100644 --- a/packages/fields/src/fields/slug/slug-edit.tsx +++ b/packages/fields/src/fields/slug/slug-edit.tsx @@ -34,7 +34,9 @@ const SlugEdit = ( { }: DataFormControlProps< BasePost > ) => { const { id } = field; - const slug = field.getValue( { item: data } ); + const value = field.getValue( { item: data } ); + + const slug = value.length > 0 ? value : data.id.toString(); const [ isValid, setIsValid ] = useState( true ); @@ -47,7 +49,7 @@ const SlugEdit = ( { const permalinkSuffix = suffix; const isEditable = PERMALINK_POSTNAME_REGEX.test( permalinkTemplate ); const originalSlugRef = useRef( slug ); - const slugToDisplay = slug; + const slugToDisplay = slug.length > 0 ? slug : data.id.toString(); const permalink = isEditable ? `${ permalinkPrefix }${ slugToDisplay }${ permalinkSuffix }` : safeDecodeURIComponent( data.link || '' ); @@ -60,10 +62,10 @@ const SlugEdit = ( { const onChangeControl = useCallback( ( newValue?: string ) => { - setIsValid( field.isValid( newValue ?? '' ) ); onChange( { [ id ]: newValue, } ); + setIsValid( field.isValid( newValue ) ); }, [ field, id, onChange ] ); diff --git a/packages/fields/src/fields/slug/slug-view.tsx b/packages/fields/src/fields/slug/slug-view.tsx index c418fafd1a9af9..90620542295c38 100644 --- a/packages/fields/src/fields/slug/slug-view.tsx +++ b/packages/fields/src/fields/slug/slug-view.tsx @@ -9,8 +9,17 @@ import { useEffect, useRef } from '@wordpress/element'; import type { BasePost } from '../../types'; import { getSlug } from './utils'; +const getSlugOrFallback = ( item: BasePost ): string => { + if ( typeof item === 'object' ) { + const slug = getSlug( item ); + return slug.length > 0 ? slug : item.id.toString(); + } + + return ''; +}; + const SlugView = ( { item }: { item: BasePost } ) => { - const slug = typeof item === 'object' ? getSlug( item ) : ''; + const slug = getSlugOrFallback( item ); const originalSlugRef = useRef( slug ); useEffect( () => { diff --git a/packages/fields/src/fields/slug/utils.ts b/packages/fields/src/fields/slug/utils.ts index a422afaf898f96..d515a358c26045 100644 --- a/packages/fields/src/fields/slug/utils.ts +++ b/packages/fields/src/fields/slug/utils.ts @@ -9,7 +9,5 @@ import type { BasePost } from '../../types'; import { getItemTitle } from '../../actions/utils'; export const getSlug = ( item: BasePost ): string => { - return ( - item.slug || cleanForSlug( getItemTitle( item ) ) || item.id.toString() - ); + return item.slug || cleanForSlug( getItemTitle( item ) ); };