Description
Hello,
I've used the FlatTermSelector component (https://github.com/WordPress/gutenberg/blob/trunk/packages/editor/src/components/post-taxonomies/flat-term-selector.js) (extracted from the sidebar for custom usage), and injected an ID of object to use.
So instead of using
- getCurrentPost
- getEditedPostAttribute
- editPost
I've used
- useEntityRecord
- editedRecord[property] returned by useEntityRecord
- edit function returned by useEntityRecord
The tag selector is working globally, but as soon as I have existing values when adding the term it appears and then disapears almost immeditaly.
The editor indicates that there is a pending modification on the specific object, if I refresh the part that is displaying the selector, the terms are appearing as expected with the new one.
Maybe this behaviour is on the core component too but not visible ?
Consider that the unescapeString, unescapeTerm and most-used-terms have just been extracted from the component base.
I've not changed the global logic.
The edited component :
/**
* WordPress dependencies
*/
import { __, _x, sprintf } from '@wordpress/i18n';
import { useEffect, useMemo, useState } from '@wordpress/element';
import { FormTokenField } from '@wordpress/components';
import { useSelect, useDispatch } from '@wordpress/data';
import {useEntityRecord,store as coreStore} from '@wordpress/core-data';
import { useDebounce } from '@wordpress/compose';
import { speak } from '@wordpress/a11y';
import { store as noticesStore } from '@wordpress/notices';
import { useCallback } from 'react';
/**
* Internal dependencies
*/
import { unescapeString, unescapeTerm } from '../../Utils';
import MostUsedTerms from './most-used-terms';
/**
* Shared reference to an empty array for cases where it is important to avoid
* returning a new array reference on every invocation.
*
* @type {Array<any>}
*/
const EMPTY_ARRAY = [];
/**
* Module constants
*/
const MAX_TERMS_SUGGESTIONS = 20;
const DEFAULT_QUERY = {
per_page: MAX_TERMS_SUGGESTIONS,
_fields: 'id,name',
context: 'view',
};
const isSameTermName = ( termA, termB ) =>
unescapeString( termA ).toLowerCase() ===
unescapeString( termB ).toLowerCase();
const termNamesToIds = ( names, terms ) => {
return names
.map(
( termName ) =>
terms.find( ( term ) => isSameTermName( term.name, termName ) )
?.id
)
.filter( ( id ) => id !== undefined );
};
export function FlatTermSelector( { slug, id, postType } ) {
const [ values, setValues ] = useState( [] );
const [ search, setSearch ] = useState( '' );
const debouncedSearch = useDebounce( setSearch, 500 );
const post = useEntityRecord(
'postType',
postType,
id
);
const editPost = useCallback(
( tags ) => {
post.edit( tags );
},
[ post.edit ]
);
const {
terms,
termIds,
taxonomy,
hasAssignAction,
hasCreateAction,
hasResolvedTerms,
} = useSelect(
( select ) => {
const { getEntityRecord, getEntityRecords, getTaxonomy, hasFinishedResolution } =
select( coreStore );
const _taxonomy = getTaxonomy( slug );
const _termIds = post.editedRecord[_taxonomy.rest_base];
const query = {
...DEFAULT_QUERY,
include: _termIds.join( ',' ),
per_page: -1
};
return {
hasCreateAction: _taxonomy
? post.record._links?.[
'wp:action-create-' + _taxonomy.rest_base
] ?? false
: false,
hasAssignAction: _taxonomy
? post.record._links?.[
'wp:action-assign-' + _taxonomy.rest_base
] ?? false
: false,
taxonomy: _taxonomy,
termIds: _termIds,
terms: _termIds.length
? getEntityRecords( 'taxonomy', slug, query )
: EMPTY_ARRAY,
hasResolvedTerms: hasFinishedResolution( 'getEntityRecords', [
'taxonomy',
slug,
query,
] )
};
},
[ slug ]
);
const { searchResults } = useSelect(
( select ) => {
const { getEntityRecords } = select( coreStore );
return {
searchResults: !! search
? getEntityRecords( 'taxonomy', slug, {
...DEFAULT_QUERY,
search,
} )
: EMPTY_ARRAY,
};
},
[ search, slug ]
);
// Update terms state only after the selectors are resolved.
// We're using this to avoid terms temporarily disappearing on slow networks
// while core data makes REST API requests.
useEffect( () => {
if ( hasResolvedTerms ) {
const newValues = ( terms ?? [] ).map( ( term ) =>
unescapeString( term.name )
);
setValues( newValues );
}
}, [ terms, hasResolvedTerms ] );
const suggestions = useMemo( () => {
return ( searchResults ?? [] ).map( ( term ) =>
unescapeString( term.name )
);
}, [ searchResults ] );
const { saveEntityRecord } = useDispatch( coreStore );
const { createErrorNotice } = useDispatch( noticesStore );
if ( ! hasAssignAction ) {
return null;
}
async function findOrCreateTerm( term ) {
try {
const newTerm = await saveEntityRecord( 'taxonomy', slug, term, {
throwOnError: true,
} );
return unescapeTerm( newTerm );
} catch ( error ) {
if ( error.code !== 'term_exists' ) {
throw error;
}
return {
id: error.data.term_id,
name: term.name,
};
}
}
function onUpdateTerms( newTermIds ) {
editPost( { [ taxonomy.rest_base ]: newTermIds } );
}
function onChange( termNames ) {
const availableTerms = [
...( terms ?? [] ),
...( searchResults ?? [] ),
];
const uniqueTerms = termNames.reduce( ( acc, name ) => {
if (
! acc.some( ( n ) => n.toLowerCase() === name.toLowerCase() )
) {
acc.push( name );
}
return acc;
}, [] );
const newTermNames = uniqueTerms.filter(
( termName ) =>
! availableTerms.find( ( term ) =>
isSameTermName( term.name, termName )
)
);
// Optimistically update term values.
// The selector will always re-fetch terms later.
setValues( uniqueTerms );
if ( newTermNames.length === 0 ) {
onUpdateTerms( termNamesToIds( uniqueTerms, availableTerms ) );
return;
}
if ( ! hasCreateAction ) {
return;
}
Promise.all(
newTermNames.map( ( termName ) =>
findOrCreateTerm( { name: termName } )
)
)
.then( ( newTerms ) => {
const newAvailableTerms = availableTerms.concat( newTerms );
onUpdateTerms(
termNamesToIds( uniqueTerms, newAvailableTerms )
);
} )
.catch( ( error ) => {
createErrorNotice( error.message, {
type: 'snackbar',
} );
// In case of a failure, try assigning available terms.
// This will invalidate the optimistic update.
onUpdateTerms( termNamesToIds( uniqueTerms, availableTerms ) );
} );
}
function appendTerm( newTerm ) {
if ( termIds.includes( newTerm.id ) ) {
return;
}
const newTermIds = [ ...termIds, newTerm.id ];
const defaultName = slug === 'post_tag' ? __( 'Tag' ) : __( 'Term' );
const termAddedMessage = sprintf(
/* translators: %s: term name. */
_x( '%s added', 'term' ),
taxonomy?.labels?.singular_name ?? defaultName
);
speak( termAddedMessage, 'assertive' );
onUpdateTerms( newTermIds );
}
const newTermLabel =
taxonomy?.labels?.add_new_item ??
( slug === 'post_tag' ? __( 'Add new tag' ) : __( 'Add new Term' ) );
const singularName =
taxonomy?.labels?.singular_name ??
( slug === 'post_tag' ? __( 'Tag' ) : __( 'Term' ) );
const termAddedLabel = sprintf(
/* translators: %s: term name. */
_x( '%s added', 'term' ),
singularName
);
const termRemovedLabel = sprintf(
/* translators: %s: term name. */
_x( '%s removed', 'term' ),
singularName
);
const removeTermLabel = sprintf(
/* translators: %s: term name. */
_x( 'Remove %s', 'term' ),
singularName
);
return (
<>
<FormTokenField
__next40pxDefaultSize
value={ values }
suggestions={ suggestions }
onChange={ onChange }
onInputChange={ debouncedSearch }
maxSuggestions={ MAX_TERMS_SUGGESTIONS }
label={ newTermLabel }
messages={ {
added: termAddedLabel,
removed: termRemovedLabel,
remove: removeTermLabel,
} }
/>
<MostUsedTerms taxonomy={ taxonomy } onSelect={ appendTerm } />
</>
);
}
export default FlatTermSelector;
Example of how I've used it :
<TagSelector
slug='category'
id={12}
postType="post"
/>
Step-by-step reproduction instructions
- Append the component in sidebar with specific ID and taxonomy
- Create new terms
- Save
- Refresh page and add a new not existing term to the selector
- Validate
- The list should be updated then get back to previous original list
- The editor will notify that this object have an update
Screenshots, screen recording, code snippet
Here is the demo of the bug encountered
https://github.com/WordPress/gutenberg/assets/1007502/3d03265d-6d61-40c2-8325-5d68a3c083bf
Environment info
Latest WordPress
Default theme
wp-env
Chrome
Please confirm that you have searched existing issues in the repo.
Yes
Please confirm that you have tested with all plugins deactivated except Gutenberg.
Yes
Description
Hello,
I've used the FlatTermSelector component (https://github.com/WordPress/gutenberg/blob/trunk/packages/editor/src/components/post-taxonomies/flat-term-selector.js) (extracted from the sidebar for custom usage), and injected an ID of object to use.
So instead of using
I've used
The tag selector is working globally, but as soon as I have existing values when adding the term it appears and then disapears almost immeditaly.
The editor indicates that there is a pending modification on the specific object, if I refresh the part that is displaying the selector, the terms are appearing as expected with the new one.
Maybe this behaviour is on the core component too but not visible ?
Consider that the
unescapeString,unescapeTermandmost-used-termshave just been extracted from the component base.I've not changed the global logic.
The edited component :
Example of how I've used it :
Step-by-step reproduction instructions
Screenshots, screen recording, code snippet
Here is the demo of the bug encountered
https://github.com/WordPress/gutenberg/assets/1007502/3d03265d-6d61-40c2-8325-5d68a3c083bf
Environment info
Latest WordPress
Default theme
wp-env
Chrome
Please confirm that you have searched existing issues in the repo.
Yes
Please confirm that you have tested with all plugins deactivated except Gutenberg.
Yes