-
Notifications
You must be signed in to change notification settings - Fork 2
Migrate custom status to use term_meta #43
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 all commits
f1f6f73
6128a85
31e68cd
d793fd6
ddfff29
2820861
9d92e67
4128d98
381cc6e
2e7a564
ad1bbaa
ced0fc7
2e061c2
c3ae567
d7ecd74
8a313ac
cfb4847
3ed428f
ef20ac6
9cbbc9c
44ddca9
97b9f38
0bd0654
992e2f6
154401c
2331ce2
cd17a92
e97739e
1b8689a
0a14498
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1 @@ | ||
| <?php return array('dependencies' => array('react', 'react-dom', 'wp-api-fetch', 'wp-components', 'wp-compose', 'wp-dom-ready', 'wp-element', 'wp-i18n', 'wp-primitives'), 'version' => '292b80686f7e56a2b457'); | ||
| <?php return array('dependencies' => array('react', 'react-dom', 'wp-api-fetch', 'wp-components', 'wp-compose', 'wp-dom-ready', 'wp-element', 'wp-i18n', 'wp-primitives'), 'version' => 'e53bfd4765be1e980144'); |
Large diffs are not rendered by default.
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,123 @@ | ||
| import { BaseControl, FormTokenField } from '@wordpress/components'; | ||
| import { debounce } from '@wordpress/compose'; | ||
| import { useEffect, useMemo, useState } from '@wordpress/element'; | ||
| /** | ||
| * Custom status component | ||
| * @param object props | ||
| */ | ||
| export default function MetadataSelectFormTokenField( { | ||
| requiredMetadatas, | ||
| editorialMetadatas, | ||
| onMetadatasChanged, | ||
| help, | ||
| ...formTokenFieldProps | ||
| } ) { | ||
| const [ metadataSearch, setMetadataSearch ] = useState( '' ); | ||
| const debouncedSetMetadataSearch = debounce( setMetadataSearch, 200 ); | ||
| const [ searchedMetadatas, setSearchedMetadatas ] = useState( [] ); | ||
|
|
||
| const [ selectedMetadataTokens, setSelectedMetadataTokens ] = useState( | ||
| // Map login strings to TokenItem objects | ||
| requiredMetadatas.map( requiredMetadata => ( { | ||
| title: requiredMetadata.name, | ||
| value: requiredMetadata.name, | ||
| requiredMetadata, | ||
| } ) ) | ||
| ); | ||
|
|
||
| useEffect( () => { | ||
| if ( | ||
| metadataSearch.trim().length === 0 || | ||
| ! editorialMetadatas || | ||
| editorialMetadatas.length === 0 | ||
| ) { | ||
| return; | ||
| } | ||
|
|
||
| const matchedMetadatas = editorialMetadatas.filter( metadata => | ||
| metadata.name.toLowerCase().includes( metadataSearch.toLowerCase() ) | ||
| ); | ||
|
|
||
| setSearchedMetadatas( matchedMetadatas ); | ||
| }, [ editorialMetadatas, metadataSearch ] ); | ||
|
|
||
| const suggestions = useMemo( () => { | ||
| let metadatasToSuggest = searchedMetadatas; | ||
|
|
||
| if ( searchedMetadatas.length > 0 && requiredMetadatas.length > 0 ) { | ||
| // Remove already-selected editorial metadatas from suggestions | ||
| const selectedMetadataMap = {}; | ||
| requiredMetadatas.forEach( metadata => { | ||
| selectedMetadataMap[ metadata.id ] = true; | ||
| } ); | ||
|
|
||
| metadatasToSuggest = searchedMetadatas.filter( | ||
| metadata => ! ( metadata.term_id in selectedMetadataMap ) | ||
| ); | ||
| } | ||
|
|
||
| return metadatasToSuggest.map( metadata => { | ||
| return `${ metadata.name }`; | ||
| } ); | ||
| }, [ searchedMetadatas, requiredMetadatas ] ); | ||
|
|
||
| const handleOnChange = selectedTokens => { | ||
| const proccessedTokens = []; | ||
| selectedTokens.forEach( token => { | ||
| if ( typeof token === 'string' || token instanceof String ) { | ||
| // This is an unprocessed token that represents a string representation of | ||
| // a metadata selected from the dropdown. Convert it to a TokenItem object. | ||
| const metadata = searchedMetadatas.find( metadata => metadata.name === token ); | ||
|
|
||
| if ( metadata !== undefined ) { | ||
| proccessedTokens.push( convertMetadataToToken( metadata ) ); | ||
| } | ||
| } else { | ||
| // This token has already been processed into a TokenItem. | ||
| proccessedTokens.push( token ); | ||
| } | ||
| return token; | ||
| } ); | ||
|
|
||
| setSelectedMetadataTokens( proccessedTokens ); | ||
|
|
||
| const metadatas = proccessedTokens.map( token => token.metadata ); | ||
| onMetadatasChanged( metadatas ); | ||
| }; | ||
|
|
||
| return ( | ||
| <> | ||
| <FormTokenField | ||
| { ...formTokenFieldProps } | ||
| onChange={ handleOnChange } | ||
| onInputChange={ debouncedSetMetadataSearch } | ||
| suggestions={ suggestions } | ||
| value={ selectedMetadataTokens } | ||
| // Remove "Separate with commas or the Enter key" text that doesn't apply here | ||
| __experimentalShowHowTo={ false } | ||
| // Auto-select first match, so that it's possible to press <Enter> and immediately choose it | ||
| __experimentalAutoSelectFirstMatch={ true } | ||
| /> | ||
|
|
||
| { /* <FormTokenField> doesn't support help text. Provide a BaseControl with the help text instead. */ } | ||
| { help && <BaseControl help={ help }></BaseControl> } | ||
| </> | ||
| ); | ||
| } | ||
|
|
||
| /** | ||
| * Given a metadata object, convert it to a TokenItem object. | ||
| * @param object metadata | ||
| */ | ||
| const convertMetadataToToken = metadata => { | ||
| return { | ||
| // In a TokenItem, the "title" is an HTML title displayed on hover | ||
| title: metadata.name, | ||
|
|
||
| // The "value" is what's shown in the UI | ||
| value: metadata.name, | ||
|
|
||
| // Store the metadata with this token so we can pass it to the parent component easily | ||
| metadata, | ||
| }; | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,33 +1,62 @@ | ||
| import apiFetch from '@wordpress/api-fetch'; | ||
| import { | ||
| Card, | ||
| Button, | ||
| Card, | ||
| CardBody, | ||
| __experimentalDivider as Divider, | ||
| __experimentalHStack as HStack, | ||
| Modal, | ||
| TextControl, | ||
| TextareaControl, | ||
| Tooltip, | ||
| ToggleControl, | ||
| __experimentalDivider as Divider, | ||
| __experimentalHStack as HStack, | ||
| CardBody, | ||
| Tooltip, | ||
| } from '@wordpress/components'; | ||
| import { useState } from '@wordpress/element'; | ||
| import { __, sprintf } from '@wordpress/i18n'; | ||
|
|
||
| import ErrorNotice from '../../../../shared/js/components/error-notice'; | ||
| import MetadataSelectFormTokenField from '../metadata-select-form-token-field'; | ||
| import UserSelectFormTokenField from '../user-select-form-token-field'; | ||
|
|
||
| export default function CreateEditCustomStatusModal( { customStatus, onCancel, onSuccess } ) { | ||
| export default function CreateEditCustomStatusModal( { | ||
| customStatus, | ||
| editorialMetadatas, | ||
| onCancel, | ||
| onSuccess, | ||
| } ) { | ||
| // Custom status properties | ||
| const [ name, setName ] = useState( customStatus?.name || '' ); | ||
| const [ description, setDescription ] = useState( customStatus?.description || '' ); | ||
| const [ requiredUsers, setRequiredUsers ] = useState( customStatus?.required_users || [] ); | ||
| const [ requiredUsers, setRequiredUsers ] = useState( customStatus?.meta?.required_users || [] ); | ||
|
|
||
| // Taxonomy conflicts arise if this is done server side, so this transient field is only set here. | ||
| const [ requiredMetadatas, setRequiredMetadatas ] = useState( () => { | ||
|
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. Due to the taxonomy problems, this is needed. I noticed I had some bad data sitting around so this helps to clean it up whenever an update happens. It's fine if its silent right now, but ideally we should handle this on the backend with a cron cleanup job that has proper logging.
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. What do you mean by "taxonomy problems"/"taxonomy conflicts" here? I'm not sure what problem this is solving.
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. What I originally had:
What ended up happening was related to init problems:
So the safest option to me was: Just do this on the client side for now. What I'd like to do ideally: Add a GET api to CS and EM endpoints and add this functionality there. Ideally that's how all this data massaging is done and it should hopefully not have as many conflicts. |
||
| if ( | ||
| customStatus?.meta?.required_metadata_ids && | ||
| customStatus?.meta?.required_metadata_ids.length > 0 && | ||
| editorialMetadatas.length > 0 | ||
| ) { | ||
| // Get the required metadata fields from the custom status meta and find the corresponding editorial metadata. | ||
| const required_metadatas = customStatus.meta.required_metadata_ids.map( metadata => { | ||
| return editorialMetadatas.find( | ||
| editorialMetadata => editorialMetadata.term_id === metadata | ||
| ); | ||
| } ); | ||
|
|
||
| // Filter out any undefined values. | ||
| return required_metadatas.filter( metadata => metadata ); | ||
| } | ||
|
|
||
| return []; | ||
| } ); | ||
|
|
||
| const [ metadatas, setMetadatas ] = useState( editorialMetadatas ); | ||
|
|
||
| // Modal properties | ||
| const [ error, setError ] = useState( null ); | ||
| const [ isRequesting, setIsRequesting ] = useState( false ); | ||
| const [ isRestrictedSectionVisible, setIsRestrictedSectionVisible ] = useState( | ||
| requiredUsers.length > 0 | ||
| requiredUsers.length > 0 || requiredMetadatas.length > 0 | ||
| ); | ||
|
|
||
| let titleText; | ||
|
|
@@ -43,6 +72,9 @@ export default function CreateEditCustomStatusModal( { customStatus, onCancel, o | |
| if ( isRestrictedSectionVisible ) { | ||
| const userIds = requiredUsers.map( user => user.id ); | ||
| data.required_user_ids = userIds; | ||
|
|
||
| const metadataIds = requiredMetadatas.map( metadata => metadata.term_id ); | ||
| data.required_metadata_ids = metadataIds; | ||
| } | ||
|
|
||
| try { | ||
|
|
@@ -96,7 +128,7 @@ export default function CreateEditCustomStatusModal( { customStatus, onCancel, o | |
| <ToggleControl | ||
| label={ __( 'This status is restricted', 'vip-workflow' ) } | ||
| help={ __( | ||
| 'Require a specific user or role to advance to the next status.', | ||
| 'Require a specific user or editorial metadata field to advance to the next status.', | ||
| 'vip-workflow' | ||
| ) } | ||
| checked={ isRestrictedSectionVisible } | ||
|
|
@@ -113,6 +145,16 @@ export default function CreateEditCustomStatusModal( { customStatus, onCancel, o | |
| requiredUsers={ requiredUsers } | ||
| onUsersChanged={ setRequiredUsers } | ||
| /> | ||
| <MetadataSelectFormTokenField | ||
| label={ __( 'Required metadata fields', 'vip-workflow' ) } | ||
| help={ __( | ||
| 'These editorial metadata fields are required to advance this status.', | ||
| 'vip-workflow' | ||
| ) } | ||
| editorialMetadatas={ metadatas } | ||
| requiredMetadatas={ requiredMetadatas } | ||
| onMetadatasChanged={ setRequiredMetadatas } | ||
| /> | ||
| </CardBody> | ||
| </Card> | ||
| </> | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| <?php | ||
| /** | ||
| * class PositionHandler | ||
| * | ||
| * This class serves as a handler for the position of a custom status. | ||
| */ | ||
|
|
||
| namespace VIPWorkflow\Modules\CustomStatus\Meta; | ||
|
|
||
| use VIPWorkflow\Modules\Custom_Status; | ||
| use VIPWorkflow\Modules\Shared\PHP\MetaCleanupUtilities; | ||
| use WP_Term; | ||
|
|
||
| class PositionHandler { | ||
|
|
||
| public static function init(): void { | ||
| // Add the position to the custom status | ||
| add_filter( 'vw_register_custom_status_meta', [ __CLASS__, 'add_position' ], 10, 2 ); | ||
|
|
||
| // Remove the position on a status | ||
| add_action( 'vw_delete_custom_status_meta', [ __CLASS__, 'delete_position' ], 10, 1 ); | ||
| } | ||
|
|
||
| /** | ||
| * Add the position to the custom status | ||
| * | ||
| * @param array $term_meta The meta keys for the custom status | ||
| * @param WP_Term $custom_status The custom status term | ||
| * @return array The updated meta keys | ||
| */ | ||
| public static function add_position( array $term_meta, WP_Term $custom_status ): array { | ||
| $position = MetaCleanupUtilities::get_int( $custom_status->term_id, Custom_Status::METADATA_POSITION_KEY ); | ||
|
|
||
| $term_meta[ Custom_Status::METADATA_POSITION_KEY ] = $position; | ||
|
|
||
| return $term_meta; | ||
| } | ||
|
|
||
| /** | ||
| * Delete the position on a status | ||
| * | ||
| * @param integer $term_id The term ID of the status | ||
| * @return void | ||
| */ | ||
| public static function delete_position( int $term_id ): void { | ||
| delete_term_meta( $term_id, Custom_Status::METADATA_POSITION_KEY ); | ||
| } | ||
| } | ||
|
|
||
| PositionHandler::init(); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,67 @@ | ||
| <?php | ||
| /** | ||
| * class RequiredMetadataIdHandler | ||
| * | ||
| * Cleans up editorial metadata IDs that are required on a custom status, and should be cleaned up once they are deleted. | ||
| */ | ||
|
|
||
| namespace VIPWorkflow\Modules\CustomStatus\Meta; | ||
|
|
||
| use VIPWorkflow\Modules\Custom_Status; | ||
| use VIPWorkflow\VIP_Workflow; | ||
| use VIPWorkflow\Modules\Shared\PHP\MetaCleanupUtilities; | ||
|
|
||
| use WP_Term; | ||
|
|
||
| class RequiredMetadataIdHandler { | ||
|
|
||
| public static function init(): void { | ||
| // Add the required metadata IDs to the custom status | ||
| add_filter( 'vw_register_custom_status_meta', [ __CLASS__, 'add_required_metadata_ids' ], 10, 2 ); | ||
|
|
||
| // Remove the required metadata fields on a status | ||
| add_action( 'vw_delete_custom_status_meta', [ __CLASS__, 'delete_required_metadata' ], 10, 1 ); | ||
|
|
||
| // Remove deleted metadata fields from required metadata fields | ||
| add_action( 'vw_editorial_metadata_term_deleted', [ __CLASS__, 'remove_deleted_metadata_from_required_metadata' ], 10, 1 ); | ||
| } | ||
|
|
||
| /** | ||
| * Add the required metadata IDs to the custom status | ||
| * | ||
| * @param array $term_meta The meta keys for the custom status | ||
| * @param WP_Term $custom_status The custom status term | ||
| * @return array The updated meta keys | ||
| */ | ||
| public static function add_required_metadata_ids( array $term_meta, WP_Term $custom_status ): array { | ||
| $metadata_ids = MetaCleanupUtilities::get_array( $custom_status->term_id, Custom_Status::METADATA_REQ_EDITORIAL_IDS_KEY ); | ||
|
|
||
| $term_meta[ Custom_Status::METADATA_REQ_EDITORIAL_IDS_KEY ] = $metadata_ids; | ||
|
|
||
| return $term_meta; | ||
| } | ||
|
|
||
| /** | ||
| * Delete the required metadata fields on a status | ||
| * | ||
| * @param integer $term_id The term ID of the status | ||
| * @return void | ||
| */ | ||
| public static function delete_required_metadata( int $term_id ): void { | ||
| delete_term_meta( $term_id, Custom_Status::METADATA_REQ_EDITORIAL_IDS_KEY ); | ||
| } | ||
|
|
||
| /** | ||
| * Remove the delete metadata from the required metadata fields on a status | ||
| * | ||
| * @param integer $meta_id The meta ID that was deleted | ||
| * @return void | ||
| */ | ||
| public static function remove_deleted_metadata_from_required_metadata( int $deleted_meta_id ): void { | ||
| $custom_statuses = VIP_Workflow::instance()->custom_status->get_custom_statuses(); | ||
|
|
||
| MetaCleanupUtilities::cleanup_id( $custom_statuses, $deleted_meta_id, /* id_to_replace */ null, Custom_Status::METADATA_REQ_EDITORIAL_IDS_KEY ); | ||
| } | ||
| } | ||
|
|
||
| RequiredMetadataIdHandler::init(); |
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.
I tried to clean this up by pulling out some common pieces, but it quickly got way too complicated so I've left it as is. I'd like to do this in another PR so it gets the proper attention it needs.