Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
dd82d31
chore: scaffold
dinhtungdu Sep 20, 2025
a3b78ca
feat: update field type
dinhtungdu Sep 20, 2025
2ab54e8
feat: update field control
dinhtungdu Sep 20, 2025
161d167
feat: add step support
dinhtungdu Sep 20, 2025
f76354e
feat: update validation
dinhtungdu Sep 20, 2025
754a11b
feat: register new field
dinhtungdu Sep 20, 2025
6b78a80
feat: add suffix and prefix support
dinhtungdu Sep 20, 2025
3000524
feat: update min max of between control
dinhtungdu Sep 20, 2025
429a54b
fix: update custom validation trigger condition
dinhtungdu Sep 20, 2025
209f2cd
test: add number tests
dinhtungdu Sep 21, 2025
b8cb310
fix: between number onChange callback
dinhtungdu Sep 21, 2025
5b37b13
fix: Ensure the value of min max are always different
dinhtungdu Sep 21, 2025
5f3d926
fix: type consistency
dinhtungdu Sep 21, 2025
ce9c026
fix: number validation logic
dinhtungdu Sep 21, 2025
483510e
dev: story
dinhtungdu Sep 20, 2025
7f81c2b
refactor: base integer on number
dinhtungdu Sep 21, 2025
8e20976
docs: update README file
dinhtungdu Sep 21, 2025
b605910
chore: changelog
dinhtungdu Sep 21, 2025
74acb65
chore: restore integer field definition
dinhtungdu Sep 27, 2025
8ce0f13
fix: simplify the number field for the first iteration, remove steps/…
dinhtungdu Sep 27, 2025
9cfddf9
fix: remove unused helper
dinhtungdu Sep 27, 2025
d2c03b7
fix: remove unused types related to number
dinhtungdu Sep 30, 2025
0716841
fix: make the render function respect default decimal number
dinhtungdu Sep 30, 2025
1ccff00
fix: unify validation behavior
dinhtungdu Sep 30, 2025
93c776c
refactor: extract number control into a shared component for number a…
dinhtungdu Sep 30, 2025
157b176
fix: sync the validation with integer
dinhtungdu Sep 30, 2025
17aab3a
Update story components to prevent conflicts with global namespace
oandregal Sep 30, 2025
41af593
fix: update number with element story
dinhtungdu Sep 30, 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
1 change: 1 addition & 0 deletions packages/dataviews/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

- DataViews: Require at least one field to be visible. ([#71625](https://github.com/WordPress/gutenberg/pull/71625))
- DataViews: Expose `DataViews.FiltersToggled` component to be used in free composition. [#71907](https://github.com/WordPress/gutenberg/pull/71907)
- DataViews: Add `number` field and refactor `integer` field based on the `number` field. ([#71797](https://github.com/WordPress/gutenberg/pull/71797))

## 9.0.0 (2025-09-17)

Expand Down
4 changes: 2 additions & 2 deletions packages/dataviews/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -939,7 +939,7 @@ Example:

### `type`

Field type. One of `text`, `integer`, `datetime`.
Field type. One of `text`, `integer`, `number`, `datetime`, `date`, `media`, `boolean`, `email`, `password`, `telephone`, `color`, `url`, `array`.

If a field declares a `type`, it gets default implementations for the `sort`, `isValid`, and `Edit` functions if no other values are specified.

Expand Down Expand Up @@ -1132,7 +1132,7 @@ Example:

React component that renders the control to edit the field.

- Type: React component | `string`. If it's a string, it needs to be one of `text`, `integer`, `datetime`, `radio`, `select`.
- Type: React component | `string`. If it's a string, it needs to be one of `array`, `checkbox`, `color`, `datetime`, `date`, `email`, `telephone`, `url`, `integer`, `number`, `password`, `radio`, `select`, `text`, `toggle`, `textarea`, `toggleGroup`.
- Required by DataForm. Optional if the field provided a `type`.
- Props:
- `data`: the item to be processed
Expand Down
2 changes: 2 additions & 0 deletions packages/dataviews/src/dataform-controls/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import email from './email';
import telephone from './telephone';
import url from './url';
import integer from './integer';
import number from './number';
import radio from './radio';
import select from './select';
import text from './text';
Expand All @@ -43,6 +44,7 @@ const FORM_CONTROLS: FormControls = {
telephone,
url,
integer,
number,
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we remove "integer"? Can we make "number" generic enough to handle all the cases. What's the props and cons of each approach?

Copy link
Member

Choose a reason for hiding this comment

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

We need to consider both the field type and the Edit function as separate entities. Introducing new field types is only justified if they provide value in more places than Edit, otherwise, it's just Edit configuration.

Everything becomes a bit more clear if we go with type: decimal and type: integer. This is how I envision the two of them working together and evolving:

  • type: integer
    • render: an integer
    • Edit: 'integer' by default (uses ValidatedNumberControl under the hood)
    • Edit config:
      • prefix
      • suffix
      • separatorThousand
    • isValid: checks validity for integer
  • type: decimal
    • render: a decimal number with two decimals by default
    • Edit: 'decimal' by default (uses ValidatedNumberControl under the hood with step set at 0.01)
    • Edit config
      • prefix
      • suffix
      • decimals (e.g., decimals: 2 would set step=0.01 under the hood)
      • separatorThousand
      • separatorDecimal
    • isValid: checks validity for the decimal number

I think we should cover all use cases. We don't introduce separatorThousand and separatorDecimal in this PR (this is powered by a new Edit component, separate from integer and number). I think we could challenge a bit this idea and, if they are really necessary, we can introduce them later, in a follow-up PR.

I'd rather expose a decimals prop for Edit config than a step prop that mimics the component/browser's. decimals is clearer than step. decimals works better as a prop to configure the render function too, where step doesn't make sense, if/when we do that (e.g., render: { decimals: 2 }). We can introduce step later as something specific to Edit, if it becomes necessary. Happy to discuss this and be convinced otherwise if anyone has ever used steps in practice and has feedback to provide.

Copy link
Contributor

Choose a reason for hiding this comment

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

If decimals = 0 it becomes an integer field, I'm not really convinced that these should be separate.

Copy link
Member Author

@dinhtungdu dinhtungdu Sep 23, 2025

Choose a reason for hiding this comment

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

Everything becomes a bit more clear if we go with type: decimal and type: integer.

But isn't number more common than decimal? JSON Schema and Open API define two numeric types: number and integer. As we're shaping the API, I think we should also consider the familiarity of a term over the other.

If decimals = 0 it becomes an integer field,

It depends on which level decimals belong to? In this convo context, decimals is an EditConfiguration property, which affects only the Edit component. Then validation is our problem.

If decimals is a field property, then yes, decimals = 0 becomes an integer, but it doesn't make sense to add it as a field level, because only "number" needs it.

With current API design, we have to expose two field types: integer and number/decimal. The field configuration also looks simple for integer fields; consumers just need to specify the field type. Having to provide decimals: 0 to declare integer makes it look too verbose to me.

Copy link
Contributor

Choose a reason for hiding this comment

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

I've always thought min, max, decimals and things like that are not just about validation or "edit config" but more about config for the field type. I know it's not the case today though.

Copy link
Contributor

Choose a reason for hiding this comment

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

children: {
     elementType: 'number',
     decimals: 0,
   }

This one feels wrong to me. children could be just a regular field no? so { type: 'number', format: { decimals: 0 }


The other thought I have is that I'm not sure format is a "wide" enough term that covers all the config that we need but I also don't have strong counter examples at the moment.

Copy link
Contributor

Choose a reason for hiding this comment

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

JSONSchema has both "number" and "integer" as well. So that's a strong argument for me. I invite you to also check how JSON schema define all these formats... I think the closer we get to JSONSchema, the better. It makes our fields even more portable and usable with other tools.

Copy link
Member Author

Choose a reason for hiding this comment

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

Sounds like we should migrate the whole EditConfiguration to format :D, let me do it and see how it looks.

Copy link
Member

Choose a reason for hiding this comment

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

should migrate the whole EditConfiguration to format

We shouldn't do this. The existing EditConfig are actually bound to the edit components: rows for textarea, prefix/suffix for text (e.g., adding the @ prefix for email fields, etc.). That's not a format, but edit configuration and still needs to live as such.

My suggestion would be to scope this PR to just introducing a new number type (number as per your suggestions) — that's something we've got agreement in this thread. Do not add any support for prefix/suffix/steps yet, just make it use two decimals for now. In a follow-up, we'll consolidate the way forward for formatting.

Copy link
Member Author

Choose a reason for hiding this comment

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

Makese sense! I updated the PR to only adding the number field, can you please take a look.

password,
radio,
select,
Expand Down
182 changes: 3 additions & 179 deletions packages/dataviews/src/dataform-controls/integer.tsx
Original file line number Diff line number Diff line change
@@ -1,185 +1,9 @@
/**
* External dependencies
*/
import deepMerge from 'deepmerge';

/**
* WordPress dependencies
*/
import {
Flex,
BaseControl,
__experimentalNumberControl as NumberControl,
privateApis,
} from '@wordpress/components';
import { useCallback, useState } from '@wordpress/element';
import { __ } from '@wordpress/i18n';

/**
* Internal dependencies
*/
import { OPERATOR_BETWEEN } from '../constants';
import type { DataFormControlProps } from '../types';
import { unlock } from '../lock-unlock';

const { ValidatedNumberControl } = unlock( privateApis );

type IntegerBetween = [ number | string, number | string ];

function BetweenControls( {
value,
onChange,
hideLabelFromVision,
}: {
value: IntegerBetween;
onChange: ( [ min, max ]: IntegerBetween ) => void;
hideLabelFromVision?: boolean;
} ) {
const [ min = '', max = '' ] = value;

const onChangeMin = useCallback(
( newValue: string | undefined ) =>
onChange( [ Number( newValue ), max ] ),
[ onChange, max ]
);

const onChangeMax = useCallback(
( newValue: string | undefined ) =>
onChange( [ min, Number( newValue ) ] ),
[ onChange, min ]
);

return (
<BaseControl
__nextHasNoMarginBottom
help={ __( 'The max. value must be greater than the min. value.' ) }
>
<Flex direction="row" gap={ 4 }>
<NumberControl
label={ __( 'Min.' ) }
value={ min }
max={ max ? Number( max ) - 1 : undefined }
onChange={ onChangeMin }
__next40pxDefaultSize
hideLabelFromVision={ hideLabelFromVision }
/>
<NumberControl
label={ __( 'Max.' ) }
value={ max }
min={ min ? Number( min ) + 1 : undefined }
onChange={ onChangeMax }
__next40pxDefaultSize
hideLabelFromVision={ hideLabelFromVision }
/>
</Flex>
</BaseControl>
);
}

export default function Integer< Item >( {
data,
field,
onChange,
hideLabelFromVision,
operator,
}: DataFormControlProps< Item > ) {
const { label, description, getValue, setValue } = field;
const value = getValue( { item: data } ) ?? '';
const [ customValidity, setCustomValidity ] =
useState<
React.ComponentProps<
typeof ValidatedNumberControl
>[ 'customValidity' ]
>( undefined );

const onChangeControl = useCallback(
( newValue: string | undefined ) => {
onChange(
setValue( {
item: data,
// Do not convert an empty string or undefined to a number,
// otherwise there's a mismatch between the UI control (empty)
// and the data relied by onChange (0).
value: [ '', undefined ].includes( newValue )
? undefined
: Number( newValue ),
} )
);
},
[ data, onChange, setValue ]
);

const onChangeBetweenControls = useCallback(
( newValue: IntegerBetween ) => {
onChange(
setValue( {
item: data,
value: newValue,
} )
);
},
[ data, onChange, setValue ]
);

const onValidateControl = useCallback(
( newValue: any ) => {
const message = field.isValid?.custom?.(
deepMerge(
data,
setValue( {
item: data,
value: [ undefined, '', null ].includes( newValue )
? undefined
: Number( newValue ),
} ) as Partial< Item >
),
field
);

if ( message ) {
setCustomValidity( {
type: 'invalid',
message,
} );
return;
}

setCustomValidity( undefined );
},
[ data, field, setValue ]
);

if ( operator === OPERATOR_BETWEEN ) {
let valueBetween: IntegerBetween = [ '', '' ];
if (
Array.isArray( value ) &&
value.length === 2 &&
value.every(
( element ) => typeof element === 'number' || element === ''
)
) {
valueBetween = value as IntegerBetween;
}
return (
<BetweenControls
value={ valueBetween }
onChange={ onChangeBetweenControls }
hideLabelFromVision={ hideLabelFromVision }
/>
);
}
import ValidatedNumber from './utils/validated-number';

return (
<ValidatedNumberControl
required={ !! field.isValid?.required }
onValidate={ onValidateControl }
customValidity={ customValidity }
label={ label }
help={ description }
value={ value }
onChange={ onChangeControl }
__next40pxDefaultSize
hideLabelFromVision={ hideLabelFromVision }
/>
);
export default function Number< Item >( props: DataFormControlProps< Item > ) {
return <ValidatedNumber { ...props } decimals={ 0 } />;
}
10 changes: 10 additions & 0 deletions packages/dataviews/src/dataform-controls/number.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/**
* Internal dependencies
*/
import type { DataFormControlProps } from '../types';
import ValidatedNumber from './utils/validated-number';

export default function Number< Item >( props: DataFormControlProps< Item > ) {
// TODO: remove this hardcoded value when the decimal number is configurable
return <ValidatedNumber { ...props } decimals={ 2 } />;
}
Loading
Loading