Skip to content

Conversation

@oandregal
Copy link
Member

@oandregal oandregal commented Nov 18, 2025

Follow-up to #72999

What?

Simplifies field normalization by delegating it to the field types.

Why?

This keeps a cleaner separation of concerns: normalizeFields is a function that calls the proper type, which has all the information to normalize the field.

For example, before this change, normalizeFields needed to do specific things for the date type that has a specific format no other field has, see #72999 (comment)

How?

  • Each field type exports a normalizer function: normalizeField( field<Item> ): NormalizedField<Item>.
  • The concept of "field type definitions" is removed from the codebase.

Testing Instructions

  • Run the storybook locally (npm run storybook:dev) and smoke test.
  • Run the Gutenberg plugin (npm run dev) and smoke test the Pages, Templates, and Patterns screens in the Site Editor.

@oandregal oandregal self-assigned this Nov 18, 2025
@oandregal oandregal added [Type] Enhancement A suggestion for improvement. [Feature] DataViews Work surrounding upgrading and evolving views in the site editor and beyond labels Nov 18, 2025
},
Edit: 'array', // Use array control
render,
getFormat: (): NormalizedFormat => ( {} ),
Copy link
Contributor

Choose a reason for hiding this comment

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

Should this be named normalizeField or something and just does any "type" specific normalization?

Copy link
Member Author

@oandregal oandregal Nov 19, 2025

Choose a reason for hiding this comment

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

I've pushed a more ambitious refactor along the lines we talked in the past. The "field type definition" concept is gone from the codebase, and, instead, there're normalizeField functions that take a Field and returns a NormalizedField.

This will help other parts of the codebase (e.g., validation) to be absorbed in those functions. It's now much clearer that the "field type definitions" are just factories — which wasn't very obvious before.

@oandregal oandregal force-pushed the update/field-api-date-format branch from 7957068 to 6ffdf13 Compare November 19, 2025 11:15
@oandregal oandregal marked this pull request as ready for review November 19, 2025 11:19
@oandregal oandregal requested a review from gigitux as a code owner November 19, 2025 11:19
@github-actions
Copy link

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the props-bot label.

If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message.

Co-authored-by: oandregal <[email protected]>
Co-authored-by: youknowriad <[email protected]>
Co-authored-by: ntsekouras <[email protected]>

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

@@ -0,0 +1,17 @@
const getValueFromId =
Copy link
Member Author

Choose a reason for hiding this comment

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

Extracted from the removed normalizedFields function verbatim.

@@ -0,0 +1,17 @@
const setValueFromId =
Copy link
Member Author

Choose a reason for hiding this comment

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

Extracted from the removed normalizeFields function verbatim.

*/
import type { Field, FilterByConfig, Operator } from '../../types';

function getFilterBy< Item >(
Copy link
Member Author

Choose a reason for hiding this comment

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

Extracted from the removed normalizeFields function almost verbatim. The only change is that instead of receiving a field type definition as the second parameter, it now receives the concrete things that it needs.

* @param fields Fields config.
* @return Normalized fields config.
*/
export default function normalizeFields< Item >(
Copy link
Member Author

Choose a reason for hiding this comment

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

The function still exists, but it was moved here and it just delegates to the normalized function for the type.

Copy link
Contributor

Choose a reason for hiding this comment

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

It's a little unexpected to have this function here in its own file, especially now that it's hollowed out, while the closely related functions normalizeField and getNormalizeFieldFunction are in the index.

isPrimary?: boolean;
}

export interface NormalizedFilterByConfig {
Copy link
Member Author

Choose a reason for hiding this comment

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

This is all removed as it's no longer necessary.

| 'url'
| 'array';

/**
Copy link
Member Author

Choose a reason for hiding this comment

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

This is all removed as it's no longer necessary.

const arrayFieldType: FieldTypeDefinition< any > = {
sort,
isValid: {
const defaultOperators: Operator[] = [ OPERATOR_IS_ANY, OPERATOR_IS_NONE ];
Copy link
Member Author

Choose a reason for hiding this comment

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

The changes for all field types are about converting a (template) object into a function that returns the actual (normalized) object.

@oandregal oandregal force-pushed the update/field-api-date-format branch from 62c07ab to 5f4488d Compare November 19, 2025 17:08
@oandregal oandregal force-pushed the update/field-api-date-format branch from 5f4488d to c336ab3 Compare November 20, 2025 09:46
@oandregal oandregal changed the title Field API: delegate format computation to the Field Type Definition Field API: simplify field normalization Nov 20, 2025
@oandregal oandregal added [Type] Code Quality Issues or PRs that relate to code quality and removed [Type] Enhancement A suggestion for improvement. labels Nov 20, 2025
@github-actions
Copy link

Flaky tests detected in c336ab3.
Some tests passed with failed attempts. The failures may not be related to this commit but are still reported for visibility. See the documentation for more information.

🔍 Workflow run URL: https://github.com/WordPress/gutenberg/actions/runs/19532526294
📝 Reported issues:

@oandregal
Copy link
Member Author

@ntsekouras @youknowriad this is working well on my tests, and it's an improvement over what we have. Is there anything you want me to look or should we go ahead and merge?

@ntsekouras
Copy link
Contributor

ntsekouras commented Nov 20, 2025

@ntsekouras @youknowriad this is working well on my tests, and it's an improvement over what we have. Is there anything you want me to look or should we go ahead and merge?

It would be helpful to me to review first. Can you wait a bit? I'll do it today.

Copy link
Contributor

@ntsekouras ntsekouras left a comment

Choose a reason for hiding this comment

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

This tests well, thank you! I've left some questions to better understand the rationale and the API itself. I'd defer approving to @youknowriad though, as he's more familiar with these parts.

Copy link
Contributor

@ntsekouras ntsekouras left a comment

Choose a reason for hiding this comment

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

Finally with your comment here, I got it and can approve confidently :)

@oandregal oandregal merged commit 01136d1 into trunk Nov 20, 2025
54 of 58 checks passed
@oandregal oandregal deleted the update/field-api-date-format branch November 20, 2025 18:33
@github-actions github-actions bot added this to the Gutenberg 22.2 milestone Nov 20, 2025
Copy link
Contributor

@mcsf mcsf left a comment

Choose a reason for hiding this comment

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

I'm sorry I'm late to the party. This looks good overall. I think it's moving in the right direction both in terms of managing the codebase/responsiblities and polishing the type system for the benefit of API consumers. And I appreciate the emphasis on readability!

I bet we can keep doing more.

Comment on lines +15 to +47
if ( typeof field.filterBy === 'object' ) {
let operators = field.filterBy.operators;

// Assign default values if no operator was provided.
if ( ! operators || ! Array.isArray( operators ) ) {
operators = defaultOperators;
}

// Make sure only valid operators are included.
operators = operators.filter( ( operator ) =>
validOperators.includes( operator )
);

// If no operators are left at this point,
// the filters should be disabled.
if ( operators.length === 0 ) {
return false;
}

return {
isPrimary: !! field.filterBy.isPrimary,
operators,
};
}

if ( defaultOperators.length === 0 ) {
return false;
}

return {
isPrimary: false,
operators: defaultOperators,
};
Copy link
Contributor

Choose a reason for hiding this comment

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

A few things seemed a bit strange in this function: filtering the default operators (unless we really want that extra safety net to prevent user error?); a seemingly superfluous isArray check (unless we believe consumers are not using TS?); the amount of branching and return statements; and the cumbersome requirement for consumers that they provide two identical arrays (defaultOperators, validOperators).

For the latter, maybe that explicitness is welcome, but otherwise I could be a nice touch to let defaultOperators default to validOperators when it is undefined.

For the rest, I found this refactor clearer, but this is probably very subjective:

Suggested change
if ( typeof field.filterBy === 'object' ) {
let operators = field.filterBy.operators;
// Assign default values if no operator was provided.
if ( ! operators || ! Array.isArray( operators ) ) {
operators = defaultOperators;
}
// Make sure only valid operators are included.
operators = operators.filter( ( operator ) =>
validOperators.includes( operator )
);
// If no operators are left at this point,
// the filters should be disabled.
if ( operators.length === 0 ) {
return false;
}
return {
isPrimary: !! field.filterBy.isPrimary,
operators,
};
}
if ( defaultOperators.length === 0 ) {
return false;
}
return {
isPrimary: false,
operators: defaultOperators,
};
const operators =
field.filterBy?.operators?.filter( ( operator ) =>
validOperators.includes( operator )
) ?? defaultOperators;
// If no operators are left at this point, the filters should be disabled.
if ( ! operators.length ) {
return false;
}
return {
isPrimary: !! field.filterBy?.isPrimary,
operators,
};

Copy link
Member Author

Choose a reason for hiding this comment

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

I've addressed this at 47f458c

export default function getNormalizeFieldFunction< Item >(
type?: FieldType
): FieldTypeDefinition< Item > {
): ( field: Field< Item > ) => NormalizedField< Item > {
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: a switch statement seems perfect for the body of this function.

Copy link
Member Author

Choose a reason for hiding this comment

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

Follow-up at d12c817

* @param fields Fields config.
* @return Normalized fields config.
*/
export default function normalizeFields< Item >(
Copy link
Contributor

Choose a reason for hiding this comment

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

It's a little unexpected to have this function here in its own file, especially now that it's hollowed out, while the closely related functions normalizeField and getNormalizeFieldFunction are in the index.

enableGlobalSearch: field.enableGlobalSearch ?? false,
enableHiding: field.enableHiding ?? true,
readOnly: field.readOnly ?? false,
filterBy: getFilterBy( field, defaultOperators, validOperators ),
Copy link
Contributor

Choose a reason for hiding this comment

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

Does this mean that we're making new copies of defaultOperators, validOperators and filterBy every time we normalise a field? If so, is the cost of that relevat, or negligible?

Copy link
Member Author

Choose a reason for hiding this comment

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

#73546 is a follow-up to this PR. It changes things a bit.

format?: FormatDate;
};

export type NormalizedFormat = Required< FormatDate > | {};
Copy link
Contributor

Choose a reason for hiding this comment

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

Unused, no?

Copy link
Member Author

Choose a reason for hiding this comment

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

Addressed this at 5384d94

Comment on lines +45 to +50
weekStartsOn:
field.format?.weekStartsOn !== undefined &&
DAYS_OF_WEEK.includes( field.format?.weekStartsOn as DayString )
? field.format.weekStartsOn
: numberToWeekStartsOn( getSettings().l10n.startOfWeek ),
};
Copy link
Contributor

Choose a reason for hiding this comment

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

My apologies for asking something off topic, but what's the story behind the introduction of weekStartsOn (string), given that startOfWeek (integer) precedes it?

Copy link
Member Author

Choose a reason for hiding this comment

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

In the preceding PR, I thought it'd be more convenient for consumers to set it as sunday | monday | ... instead of 0 | 1 | .... I can see how both approaches have pros/cons. Do you think we should just surface WordPress mechanism?

Copy link
Contributor

Choose a reason for hiding this comment

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

The names are definitely a lot more convenient and clear, it's just that there could be some confusion, because throughout the codebase the property weekStartsOn is sometimes a string and sometimes an integer (e.g. DateCalendar, DateRangeCalendar) or an enum (useLilius).

But maybe the confusion is worth it. And maybe the way forward is to equip WP date to understand both formats. 🤷

Copy link
Member Author

Choose a reason for hiding this comment

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

I've prepared #73538 to fix a bug and revert back to numbers. It's a neat idea, but I realized now it needs to happen across all components.

@youknowriad
Copy link
Contributor

I'm late to the party but for me this feels like it goes in the wrong direction. It makes reasoning about fields harder and push us further away from allowing folks to register third party field types (like they can register third-party block types)

@oandregal
Copy link
Member Author

@youknowriad 🤔 I see we have two alternatives for that:

  • register an object with functions
registerCustomFieldType(
  'CUSTOM_TYPE',
  {
    filter: ( field ) => { /* ... */ }
    validate: ( field ) => { /* ... */ }
    format: ( field ) => { /* ... */ } 
  }
);
  • register a factory function
registerCustomFieldType(
  'CUSTOM_TYPE,
  customTypeFunction,
);

Can you elaborate on why you have a preference for the former? Perhaps you're thinking of server-side custom field type registration? Since we have functions in both approaches, that is going to introduce some limitations whatever approach we use.

@youknowriad
Copy link
Contributor

The former is consistent with the Block API and any registration API we've had so far. It's not really clear to me why we go in another direction, the "why" is very unclear to me.

@oandregal
Copy link
Member Author

@youknowriad I've prepared #73546 so that the field types return objects, and some specific functions would take the field as well (for now, just format). I had a preference for the normalization function for the field type because it allows the type to modify any aspect of the field + ergonomics., but it is not a strong opinion. We can revisit this later, if necessary.

I've also created #73548 to track what needs to happen for 3rd parties to be able to register field types.

@oandregal
Copy link
Member Author

It's a little unexpected to have this function here in its own file, especially now that it's hollowed out, while the closely related functions normalizeField and getNormalizeFieldFunction are in the index.

@mcsf I've addressed this 13d787e#diff-098346a2473f918df906636042419228b26a9c8985c3d7772359072851d00ce5 (not sure why I can't comment in your comment).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

[Feature] DataViews Work surrounding upgrading and evolving views in the site editor and beyond [Type] Code Quality Issues or PRs that relate to code quality

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants