Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,5 @@ assets/build/**/*.php
# Release folder
release/

# Local agent planning notes
.zcode/
96 changes: 96 additions & 0 deletions assets/src/js/create-taxonomies/helpers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/**
* Pure helpers for the create taxonomy sidebar panel.
*
* Kept free of React and WordPress globals so the payload and validation
* rules can be unit tested directly.
*
* @since SCF 6.9.6
*/

import { __ } from '@wordpress/i18n';

/**
* Builds a taxonomy key from a label.
*
* Matches the key rules the classic editor enforces: lower case letters,
* numbers, underscores and dashes, capped at 32 characters.
*
* @since SCF 6.9.6
*
* @param {string} label The label to convert.
* @return {string} The generated taxonomy key.
*/
export function slugifyTaxonomyKey( label ) {
return String( label ?? '' )
.trim()
.toLowerCase()
.replace( /[^a-z0-9_-]+/g, '-' )
.replace( /-+/g, '-' )
.replace( /^-|-$/g, '' )
.substring( 0, 32 );
}

/**
* Validates the create taxonomy form values.
*
* @since SCF 6.9.6
*
* @param {Object} values The form values.
* @return {Object} A map of field name to error message. Empty when valid.
*/
export function validateTaxonomyForm( values ) {
const errors = {};
const taxonomy = String( values.taxonomy ?? '' ).trim();

if ( ! String( values.singularLabel ?? '' ).trim() ) {
errors.singularLabel = __(
'The singular label is required.',
'secure-custom-fields'
);
}

if ( ! String( values.pluralLabel ?? '' ).trim() ) {
errors.pluralLabel = __(
'The plural label is required.',
'secure-custom-fields'
);
}

if ( ! taxonomy ) {
errors.taxonomy = __(
'The taxonomy key is required.',
'secure-custom-fields'
);
} else if ( taxonomy.length > 32 ) {
errors.taxonomy = __(
'The taxonomy key may be 32 characters or fewer.',
'secure-custom-fields'
);
} else if ( ! /^[a-z0-9_-]+$/.test( taxonomy ) ) {
errors.taxonomy = __(
'The taxonomy key must only contain lower case letters, numbers, underscores or dashes.',
'secure-custom-fields'
);
}

return errors;
}

/**
* Builds the REST payload for creating a taxonomy.
*
* @since SCF 6.9.6
*
* @param {Object} values The form values.
* @param {string} postType The post type the taxonomy should be bound to.
* @return {Object} The request body.
*/
export function buildTaxonomyPayload( values, postType ) {
return {
singular_label: String( values.singularLabel ?? '' ).trim(),
plural_label: String( values.pluralLabel ?? '' ).trim(),
taxonomy: String( values.taxonomy ?? '' ).trim(),
hierarchical: Boolean( values.hierarchical ),
object_type: postType ? [ postType ] : [],
};
}
216 changes: 216 additions & 0 deletions assets/src/js/create-taxonomies/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
/**
* Create Taxonomy editor sidebar panel.
*
* Adds a document settings panel to the block editor for creating a new SCF
* taxonomy in context, bound to the post type currently being edited. Gated
* in PHP on the create_taxonomies beta feature.
*
* @since SCF 6.9.6
*/

import { useState } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import { registerPlugin } from '@wordpress/plugins';
import { store as editorStore } from '@wordpress/editor';
import { store as coreDataStore } from '@wordpress/core-data';
import { useSelect, useDispatch, select } from '@wordpress/data';
import apiFetch from '@wordpress/api-fetch';
import {
Button,
Notice,
TextControl,
ToggleControl,
Spinner,
} from '@wordpress/components';

import {
buildTaxonomyPayload,
slugifyTaxonomyKey,
validateTaxonomyForm,
} from './helpers';

// PluginDocumentSettingPanel moved into the @wordpress/editor package in
// WordPress 6.6. On older versions it is only exposed from @wordpress/edit-post.
// Resolve it at runtime so the panel keeps working on every supported version,
// since the package held the component too late to import it from one place.
const PluginDocumentSettingPanel =
window.wp?.editor?.PluginDocumentSettingPanel ||
window.wp?.editPost?.PluginDocumentSettingPanel;

const CreateTaxonomyPanel = () => {
const postType = useSelect(
( selectStore ) =>
selectStore( editorStore )?.getCurrentPostType?.() ?? '',
[]
);

// The panel only makes sense for content post types, so it is hidden on the
// internal wp_* entities (templates, reusable blocks) the post editor also
// renders, and on any post type that is not shown in the admin.
const isSupportedPostType = useSelect(
( selectStore ) => {
if ( ! postType || postType.startsWith( 'wp_' ) ) {
return false;
}

const postTypeObject =
selectStore( coreDataStore )?.getPostType?.( postType );

return Boolean(
postTypeObject?.viewable && postTypeObject?.visibility?.show_ui
);
},
[ postType ]
);

const { savePost } = useDispatch( editorStore );

const [ isOpen, setIsOpen ] = useState( false );
const [ singularLabel, setSingularLabel ] = useState( '' );
const [ pluralLabel, setPluralLabel ] = useState( '' );
const [ taxonomy, setTaxonomy ] = useState( '' );
const [ hierarchical, setHierarchical ] = useState( false );
const [ error, setError ] = useState( '' );
const [ isSaving, setIsSaving ] = useState( false );

// Keep the key in step with the singular label until the user edits it.
const onSingularLabelChange = ( value ) => {
setSingularLabel( value );

if ( ! taxonomy || taxonomy === slugifyTaxonomyKey( singularLabel ) ) {
setTaxonomy( slugifyTaxonomyKey( value ) );
}
};

const onSubmit = () => {
const values = { singularLabel, pluralLabel, taxonomy, hierarchical };
const errors = validateTaxonomyForm( values );

if ( Object.keys( errors ).length ) {
setError( Object.values( errors )[ 0 ] );
return;
}

setError( '' );
setIsSaving( true );

apiFetch( {
path: '/scf/v1/taxonomies',
method: 'POST',
data: buildTaxonomyPayload( values, postType ),
} )
.then( async () => {
// A new taxonomy is registered on the next request, so reload to
// make it available on the current screen. Save first when the post
// has unsaved edits, otherwise the reload would discard them.
if ( select( editorStore ).isEditedPostDirty?.() && savePost ) {
await savePost();
}
window.location.reload();
} )
.catch( ( response ) => {
setIsSaving( false );
setError(
response?.message ||
__(
'The taxonomy could not be created.',
'secure-custom-fields'
)
);
} );
};

if ( ! isSupportedPostType ) {
return null;
}

return (
<PluginDocumentSettingPanel
name="scf-create-taxonomy"
title={ __( 'Create Taxonomy', 'secure-custom-fields' ) }
className="scf-create-taxonomy-panel"
>
{ ! isOpen && (
<Button variant="secondary" onClick={ () => setIsOpen( true ) }>
{ __( 'New Taxonomy', 'secure-custom-fields' ) }
</Button>
) }

{ isOpen && (
<>
{ error && (
<Notice status="error" isDismissible={ false }>
{ error }
</Notice>
) }

<TextControl
label={ __( 'Singular Label', 'secure-custom-fields' ) }
value={ singularLabel }
onChange={ onSingularLabelChange }
__next40pxDefaultSize
__nextHasNoMarginBottom
/>

<TextControl
label={ __( 'Plural Label', 'secure-custom-fields' ) }
value={ pluralLabel }
onChange={ setPluralLabel }
__next40pxDefaultSize
__nextHasNoMarginBottom
/>

<TextControl
label={ __( 'Taxonomy Key', 'secure-custom-fields' ) }
value={ taxonomy }
onChange={ setTaxonomy }
maxLength={ 32 }
help={ __(
'Lower case letters, numbers, underscores and dashes only. Max 32 characters.',
'secure-custom-fields'
) }
__next40pxDefaultSize
__nextHasNoMarginBottom
/>

<ToggleControl
label={ __( 'Hierarchical', 'secure-custom-fields' ) }
checked={ hierarchical }
onChange={ setHierarchical }
help={ __(
'Hierarchical taxonomies can have descendants, like categories.',
'secure-custom-fields'
) }
__nextHasNoMarginBottom
/>

<div className="scf-create-taxonomy-panel__actions">
<Button
variant="primary"
onClick={ onSubmit }
isBusy={ isSaving }
disabled={ isSaving }
>
{ __( 'Create Taxonomy', 'secure-custom-fields' ) }
</Button>
<Button
variant="tertiary"
onClick={ () => {
setIsOpen( false );
setError( '' );
} }
disabled={ isSaving }
>
{ __( 'Cancel', 'secure-custom-fields' ) }
</Button>
{ isSaving && <Spinner /> }
</div>
</>
) }
</PluginDocumentSettingPanel>
);
};

registerPlugin( 'scf-create-taxonomy', {
render: PluginDocumentSettingPanel ? CreateTaxonomyPanel : () => null,
} );
19 changes: 18 additions & 1 deletion includes/admin/beta-features.php
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,13 @@ public function get_beta_features() {
* @return void
*/
public function admin_menu() {
// Include and register the beta features before the block editor
// enqueues its assets. Features that add editor scripts attach their
// own callbacks in initialize(), and "enqueue_block_editor_assets"
// fires before "admin_enqueue_scripts", so registering any later
// would leave those callbacks unhooked.
$this->include_beta_features();

// bail early if no show_admin
if ( ! acf_get_setting( 'show_admin' ) ) {
return;
Expand Down Expand Up @@ -137,8 +144,16 @@ public function admin_body_class( $classes ) {
* @return void
*/
private function include_beta_features() {
// Only include and register the beta features once.
static $included = false;
if ( $included ) {
return;
}
$included = true;

acf_include( 'includes/admin/beta-features/class-scf-beta-feature.php' );
acf_include( 'includes/admin/beta-features/class-scf-beta-feature-connect-fields.php' );
acf_include( 'includes/admin/beta-features/class-scf-beta-feature-editor-sidebar.php' );
acf_include( 'includes/admin/beta-features/class-scf-beta-feature-create-taxonomies.php' );

add_action( 'scf/include_admin_beta_features', array( $this, 'register_beta_features' ) );

Expand All @@ -153,6 +168,8 @@ private function include_beta_features() {
* @return void
*/
public function register_beta_features() {
$this->register_beta_feature( 'SCF_Admin_Beta_Feature_Editor_Sidebar' );
$this->register_beta_feature( 'SCF_Admin_Beta_Feature_Create_Taxonomies' );
}

/**
Expand Down
Loading
Loading