Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
f1f6f73
Migrate custom status to use term_meta, and add in support for requir…
ingeniumed Sep 26, 2024
6128a85
Remove the is_required field entirely since the other PR will be remo…
ingeniumed Sep 26, 2024
31e68cd
Delete unused utilities
ingeniumed Sep 26, 2024
d793fd6
Remove removed required file
ingeniumed Sep 26, 2024
ddfff29
Fix errors found through tests
ingeniumed Sep 26, 2024
2820861
Fix up the tests and ensure the slug is generated when it's not provided
ingeniumed Sep 26, 2024
9d92e67
Redo the way terms are fetched
ingeniumed Sep 26, 2024
4128d98
Correct the type for get by
ingeniumed Sep 26, 2024
381cc6e
Fix the get_by
ingeniumed Sep 26, 2024
2e7a564
Move the init methods around for endpoint
ingeniumed Sep 27, 2024
ad1bbaa
Fix merge conflicts with trunk
ingeniumed Sep 27, 2024
ced0fc7
Delete the editorial metadata field from the status when that field i…
ingeniumed Sep 27, 2024
2e061c2
Fix the custom status endpoint tests
ingeniumed Sep 27, 2024
c3ae567
Look for the right field on the custom status
ingeniumed Sep 27, 2024
d7ecd74
Expose the editorial metadata fields on the frontend
ingeniumed Sep 27, 2024
8a313ac
Fix all the bugs related to the editorial metadata search not working
ingeniumed Sep 27, 2024
cfb4847
Make the user lookup faster
ingeniumed Sep 27, 2024
3ed428f
Cleanup the required fields cleaner, so it's all in one file
ingeniumed Sep 29, 2024
ef20ac6
Added docs
ingeniumed Sep 30, 2024
9cbbc9c
Add tests for the new required fields cron cleaner as well
ingeniumed Sep 30, 2024
44ddca9
Add better error handling
ingeniumed Sep 30, 2024
97b9f38
Adding the missing use in the tests
ingeniumed Sep 30, 2024
0bd0654
Fix the tests interfering with each other
ingeniumed Sep 30, 2024
992e2f6
Add date_floating to our custom statuses so we can remove 2 core hacks
ingeniumed Sep 30, 2024
154401c
Move required fields cleanup to individual files, split the taxonomy …
ingeniumed Oct 1, 2024
2331ce2
Remove the setting of publishing capability to avoid security problems
ingeniumed Oct 1, 2024
cd17a92
Move the adding of term_meta to the individual meta files
ingeniumed Oct 1, 2024
e97739e
Update tests for the metadata handlers
ingeniumed Oct 1, 2024
1b8689a
Change the name of the field on the custom status, for the metadata ids
ingeniumed Oct 1, 2024
0a14498
Fix the failure on the update
ingeniumed Oct 1, 2024
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
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');
2 changes: 1 addition & 1 deletion dist/modules/custom-status/custom-status-configure.js

Large diffs are not rendered by default.

495 changes: 208 additions & 287 deletions modules/custom-status/custom-status.php

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( {
Copy link
Contributor Author

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.

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( () => {
Copy link
Contributor Author

@ingeniumed ingeniumed Sep 30, 2024

Choose a reason for hiding this comment

The 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.

Copy link
Contributor

Choose a reason for hiding this comment

The 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.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

What I originally had:

  • Filter against get_term and get_terms in EM that used it's taxonomy to populate each term with the necessary term_meta.
  • Filter against get_term and get_terms in CS that used it's taxonomy to populate each term with the necessary term_meta.

What ended up happening was related to init problems:

  • Within EM, CS taxonomy was given back. That was rejected because we only target the EM taxonomy so no term_meta was populated.
  • Within CS, EM taxonomy was given back. That was rejected because we only target the CS taxonomy so no term_meta was populated.

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;
Expand All @@ -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 {
Expand Down Expand Up @@ -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 }
Expand All @@ -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>
</>
Expand Down
9 changes: 6 additions & 3 deletions modules/custom-status/lib/components/workflow-manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,19 @@ import { Button, Flex, __experimentalHeading as Heading, Tooltip } from '@wordpr
import { useState } from '@wordpress/element';
import { __, sprintf } from '@wordpress/i18n';

import DraggableCustomStatus from './draggable-custom-status';
import CreateEditCustomStatusModal from './modals/create-edit-custom-status-modal';
import ErrorNotice from '../../../shared/js/components/error-notice';
import ConfirmDeleteModal from '../../../shared/js/components/modals/confirm-delete-modal';
import SuccessNotice from '../../../shared/js/components/success-notice';
import DraggableCustomStatus from './draggable-custom-status';
import CreateEditCustomStatusModal from './modals/create-edit-custom-status-modal';

export default function WorkflowManager( { customStatuses } ) {
export default function WorkflowManager( { customStatuses, editorialMetadatas } ) {
const [ success, setSuccess ] = useState( null );
const [ error, setError ] = useState( null );

const [ statuses, setStatuses ] = useState( customStatuses );
const [ metadatas, setMetadatas ] = useState( editorialMetadatas );

const [ status, setStatus ] = useState( null );

const [ isConfirmingDelete, setIsConfirmingDelete ] = useState( false );
Expand Down Expand Up @@ -82,6 +84,7 @@ export default function WorkflowManager( { customStatuses } ) {
const createEditModal = (
<CreateEditCustomStatusModal
customStatus={ status }
editorialMetadatas={ metadatas }
onCancel={ () => setIsCreateEditModalVisible( false ) }
onSuccess={ handleSuccess }
/>
Expand Down
5 changes: 4 additions & 1 deletion modules/custom-status/lib/custom-status-configure.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@ domReady( () => {
if ( workflowManagerRoot ) {
const root = createRoot( workflowManagerRoot );
root.render(
<WorkflowManager customStatuses={ VW_CUSTOM_STATUS_CONFIGURE.custom_statuses } />
<WorkflowManager
customStatuses={ VW_CUSTOM_STATUS_CONFIGURE.custom_statuses }
editorialMetadatas={ VW_CUSTOM_STATUS_CONFIGURE.editorial_metadatas }
/>
);
}
} );
Expand Down
50 changes: 50 additions & 0 deletions modules/custom-status/meta/position-handler.php
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();
67 changes: 67 additions & 0 deletions modules/custom-status/meta/required-metadata-id-handler.php
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();
Loading