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
17f9989
Block Bindings: Add unit test coverage for source
ockham Nov 17, 2025
60d228f
Hoist select for getFieldsList tests
ockham Nov 17, 2025
56be9ae
Check all field values, remove redundant tests
ockham Nov 18, 2025
910318e
Cover all fields in field key fallback test
ockham Nov 18, 2025
d45fba8
Fall back to undefined
ockham Nov 19, 2025
176fd43
Check for fallback to field name
ockham Nov 19, 2025
eddc7d1
Expect getValues() to fall back to field labels if present
ockham Nov 19, 2025
170f855
Block Bindings: Untangle core/term-data source
ockham Nov 3, 2025
3d4dcc5
Fall back to field name instead of empty object
ockham Nov 19, 2025
fe1ce94
Refactor for legibility
ockham Nov 19, 2025
01d5be3
Wrap term count in parens
ockham Nov 19, 2025
46b3aad
Return empty array from getFieldsList() if taxonomy/termId or termDat…
ockham Nov 19, 2025
12bca88
Make test more descriptive
ockham Nov 19, 2025
521f6c9
Tweak test description
ockham Nov 19, 2025
2599580
Remove obsolete TODO
ockham Nov 19, 2025
c51128b
Tweak Navigation block test
ockham Nov 19, 2025
2fed169
Remove redundant Navigation Submenu test
ockham Nov 19, 2025
ff7a920
Compare to const imported from term-data.js
ockham Nov 19, 2025
db85574
Add test coverage for Navigation block
ockham Nov 19, 2025
54f9a6b
Reuse variable
ockham Nov 19, 2025
dbba6bf
Remove setValues and canUserEditValue tests
ockham Nov 19, 2025
eac7e3e
Remove unnecessary optional chaining
ockham Nov 24, 2025
d6ef32d
Reorganize logic a bit
ockham Nov 24, 2025
c6f0f62
id, not termId
ockham Nov 24, 2025
7d779ba
Reuse termData
ockham Nov 24, 2025
b981dfa
Rearrange tests
ockham Nov 24, 2025
dc632b7
No need for beforeAll
ockham Nov 24, 2025
91bacbe
Check all fields
ockham Nov 24, 2025
4f0d9e8
Fall back to field label
ockham Nov 24, 2025
0f3e1df
Fix logic
ockham Nov 24, 2025
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
275 changes: 124 additions & 151 deletions packages/editor/src/bindings/term-data.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,131 +11,43 @@ const NAVIGATION_BLOCK_TYPES = [
'core/navigation-submenu',
];

/**
* Creates the data fields object with the given term data values and ID value.
*
* @param {Object} termDataValues The term data values.
* @param {string|number} idValue The ID value to use.
* @return {Object} The data fields object.
*/
function createDataFields( termDataValues, idValue ) {
return {
id: {
label: __( 'Term ID' ),
value: idValue,
type: 'string',
},
name: {
label: __( 'Name' ),
value: termDataValues?.name,
type: 'string',
},
slug: {
label: __( 'Slug' ),
value: termDataValues?.slug,
type: 'string',
},
link: {
label: __( 'Link' ),
value: termDataValues?.link,
type: 'string',
},
description: {
label: __( 'Description' ),
value: termDataValues?.description,
type: 'string',
},
parent: {
label: __( 'Parent ID' ),
value: termDataValues?.parent,
type: 'string',
},
count: {
label: __( 'Count' ),
value: `(${ termDataValues?.count ?? 0 })`,
type: 'string',
},
};
}

/**
* Gets a list of term data fields with their values and labels
* to be consumed in the needed callbacks.
* If the value is not available based on context, like in templates,
* it falls back to the default value, label, or key.
*
* @param {Object} select The select function from the data store.
* @param {Object} context The context provided.
* @param {string} clientId The block client ID used to read attributes.
* @return {Object} List of term data fields with their value and label.
*
* @example
* ```js
* {
* name: {
* label: 'Term Name',
* value: 'Category Name',
* },
* count: {
* label: 'Term Count',
* value: 5,
* },
* ...
* }
* ```
*/
function getTermDataFields( select, context, clientId ) {
const { getEntityRecord } = select( coreDataStore );
const { getBlockAttributes, getBlockName } = select( blockEditorStore );

let termDataValues, dataFields;

/*
* BACKWARDS COMPATIBILITY: Hardcoded exception for navigation blocks.
* Required for WordPress 6.9+ navigation blocks. DO NOT REMOVE.
*/
const blockName = getBlockName?.( clientId );
const isNavigationBlock = NAVIGATION_BLOCK_TYPES.includes( blockName );

let termId, taxonomy;

if ( isNavigationBlock ) {
// Navigation blocks: read from block attributes
const blockAttributes = getBlockAttributes?.( clientId );
termId = blockAttributes?.id;
const typeFromAttributes = blockAttributes?.type;
taxonomy =
typeFromAttributes === 'tag' ? 'post_tag' : typeFromAttributes;
} else {
// All other blocks: use context
termId = context?.termId;
taxonomy = context?.taxonomy;
}

if ( taxonomy && termId ) {
termDataValues = getEntityRecord( 'taxonomy', taxonomy, termId );

if ( ! termDataValues && context?.termData ) {
termDataValues = context.termData;
}

if ( termDataValues ) {
dataFields = createDataFields( termDataValues, termId );
}
} else if ( context?.termData ) {
termDataValues = context.termData;
dataFields = createDataFields(
termDataValues,
termDataValues?.term_id
);
}

if ( ! dataFields || ! Object.keys( dataFields ).length ) {
return null;
}

return dataFields;
}
export const termDataFields = [
{
label: __( 'Term ID' ),
args: { field: 'id' },
type: 'string',
},
{
label: __( 'Name' ),
args: { field: 'name' },
type: 'string',
},
{
label: __( 'Slug' ),
args: { field: 'slug' },
type: 'string',
},
{
label: __( 'Link' ),
args: { field: 'link' },
type: 'string',
},
{
label: __( 'Description' ),
args: { field: 'description' },
type: 'string',
},
{
label: __( 'Parent ID' ),
args: { field: 'parent' },
type: 'string',
},
{
label: __( 'Count' ),
args: { field: 'count' },
type: 'string',
},
];

/**
* @type {WPBlockBindingsSource}
Expand All @@ -144,15 +56,67 @@ export default {
name: 'core/term-data',
usesContext: [ 'taxonomy', 'termId', 'termData' ],
getValues( { select, context, bindings, clientId } ) {
const dataFields = getTermDataFields( select, context, clientId );
const { getEntityRecord } = select( coreDataStore );

/*
* BACKWARDS COMPATIBILITY: Hardcoded exception for navigation blocks.
* Required for WordPress 6.9+ navigation blocks. DO NOT REMOVE.
*/
const { getBlockAttributes, getBlockName } = select( blockEditorStore );
const blockName = getBlockName( clientId );
const isNavigationBlock = NAVIGATION_BLOCK_TYPES.includes( blockName );

let termDataValues;

if ( isNavigationBlock ) {
// Navigation blocks: read from block attributes
const blockAttributes = getBlockAttributes( clientId );
const typeFromAttributes = blockAttributes?.type;
const taxonomy =
typeFromAttributes === 'tag' ? 'post_tag' : typeFromAttributes;
termDataValues = getEntityRecord(
'taxonomy',
taxonomy,
blockAttributes?.id
);
} else if ( context.termId && context.taxonomy ) {
// All other blocks: use context
termDataValues = getEntityRecord(
'taxonomy',
context.taxonomy,
context.termId
);
}

// Fall back to context termData if available.
if ( ! termDataValues && context?.termData && ! isNavigationBlock ) {
termDataValues = context.termData;
}

const newValues = {};
for ( const [ attributeName, source ] of Object.entries( bindings ) ) {
// Use the value, the field label, or the field key.
const fieldKey = source.args.field;
const { value: fieldValue, label: fieldLabel } =
dataFields?.[ fieldKey ] || {};
newValues[ attributeName ] = fieldValue ?? fieldLabel ?? fieldKey;
for ( const [ attributeName, binding ] of Object.entries( bindings ) ) {
const termDataField = termDataFields.find(
( field ) => field.args.field === binding.args.field
);

if ( ! termDataField ) {
// If the field is unknown, return the field name.
newValues[ attributeName ] = binding.args.field;
} else if (
! termDataValues ||
termDataValues[ binding.args.field ] === undefined
) {
// If the term data does not exist, return the field label.
newValues[ attributeName ] = termDataField.label;
} else if ( binding.args.field === 'count' ) {
// Return the term count value in parentheses.
newValues[ attributeName ] =
'(' + termDataValues[ binding.args.field ] + ')';
} else {
// If the term data exists, return the term data value.
newValues[ attributeName ] =
termDataValues[ binding.args.field ];
}
}
return newValues;
},
Expand All @@ -161,12 +125,12 @@ export default {
// Terms are typically not editable through block bindings in most contexts.
return false;
},
canUserEditValue( { select, context, args } ) {
canUserEditValue( { select, context } ) {
const { getBlockName, getSelectedBlockClientId } =
select( blockEditorStore );

const clientId = getSelectedBlockClientId();
const blockName = getBlockName?.( clientId );
const blockName = getBlockName( clientId );

// Navigaton block types are read-only.
// See https://github.com/WordPress/gutenberg/pull/72165.
Expand All @@ -184,26 +148,35 @@ export default {
return false;
}

const fieldValue = getTermDataFields( select, context, undefined )?.[
args.field
]?.value;
// Empty string or `false` could be a valid value, so we need to check if the field value is undefined.
if ( fieldValue === undefined ) {
return false;
}

return false;
},
getFieldsList( { select, context } ) {
const clientId = select( blockEditorStore ).getSelectedBlockClientId();
const termDataFields = getTermDataFields( select, context, clientId );
if ( ! termDataFields ) {
getFieldsList( { context, select } ) {
const { getBlockAttributes, getBlockName, getSelectedBlockClientId } =
select( blockEditorStore );
const clientId = getSelectedBlockClientId();
const blockName = getBlockName( clientId );

if ( NAVIGATION_BLOCK_TYPES.includes( blockName ) ) {
// Navigation blocks: read from block attributes
const blockAttributes = getBlockAttributes( clientId );
if (
! blockAttributes ||
! blockAttributes.id ||
! blockAttributes.type
) {
return [];
}
return termDataFields;
}

if ( ! context ) {
return [];
}
return Object.entries( termDataFields ).map( ( [ key, field ] ) => ( {
label: field.label,
type: field.type,
args: { field: key },
} ) );

if ( ( context.taxonomy && context.termId ) || context.termData ) {
return termDataFields;
}

return [];
},
};
Loading
Loading