@wordpress/dataviews 9.0.1-next.a730c9c8c.0 → 9.1.1-next.233ccab9b.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -3
- package/README.md +4 -2
- package/build/components/dataviews/index.js +4 -6
- package/build/components/dataviews/index.js.map +1 -1
- package/build/components/dataviews-filters/filters-toggled.js +32 -0
- package/build/components/dataviews-filters/filters-toggled.js.map +1 -0
- package/build/components/dataviews-filters/filters.js +73 -0
- package/build/components/dataviews-filters/filters.js.map +1 -0
- package/build/components/dataviews-filters/index.js +26 -190
- package/build/components/dataviews-filters/index.js.map +1 -1
- package/build/components/dataviews-filters/toggle.js +99 -0
- package/build/components/dataviews-filters/toggle.js.map +1 -0
- package/build/components/dataviews-filters/use-filters.js +63 -0
- package/build/components/dataviews-filters/use-filters.js.map +1 -0
- package/build/components/dataviews-picker/index.js +4 -6
- package/build/components/dataviews-picker/index.js.map +1 -1
- package/build/components/dataviews-view-config/index.js +22 -3
- package/build/components/dataviews-view-config/index.js.map +1 -1
- package/build/dataform-controls/array.js +110 -24
- package/build/dataform-controls/array.js.map +1 -1
- package/build/dataviews-layouts/picker-grid/index.js +4 -1
- package/build/dataviews-layouts/picker-grid/index.js.map +1 -1
- package/build/field-types/array.js +0 -6
- package/build/field-types/array.js.map +1 -1
- package/build/index.js +7 -0
- package/build/index.js.map +1 -1
- package/build/types.js.map +1 -1
- package/build/validation.js +18 -1
- package/build/validation.js.map +1 -1
- package/build-module/components/dataviews/index.js +5 -7
- package/build-module/components/dataviews/index.js.map +1 -1
- package/build-module/components/dataviews-filters/filters-toggled.js +24 -0
- package/build-module/components/dataviews-filters/filters-toggled.js.map +1 -0
- package/build-module/components/dataviews-filters/filters.js +65 -0
- package/build-module/components/dataviews-filters/filters.js.map +1 -0
- package/build-module/components/dataviews-filters/index.js +4 -186
- package/build-module/components/dataviews-filters/index.js.map +1 -1
- package/build-module/components/dataviews-filters/toggle.js +91 -0
- package/build-module/components/dataviews-filters/toggle.js.map +1 -0
- package/build-module/components/dataviews-filters/use-filters.js +56 -0
- package/build-module/components/dataviews-filters/use-filters.js.map +1 -0
- package/build-module/components/dataviews-picker/index.js +5 -7
- package/build-module/components/dataviews-picker/index.js.map +1 -1
- package/build-module/components/dataviews-view-config/index.js +22 -3
- package/build-module/components/dataviews-view-config/index.js.map +1 -1
- package/build-module/dataform-controls/array.js +112 -26
- package/build-module/dataform-controls/array.js.map +1 -1
- package/build-module/dataviews-layouts/picker-grid/index.js +4 -1
- package/build-module/dataviews-layouts/picker-grid/index.js.map +1 -1
- package/build-module/field-types/array.js +0 -6
- package/build-module/field-types/array.js.map +1 -1
- package/build-module/index.js +1 -0
- package/build-module/index.js.map +1 -1
- package/build-module/types.js.map +1 -1
- package/build-module/validation.js +18 -1
- package/build-module/validation.js.map +1 -1
- package/build-types/components/dataform/stories/index.story.d.ts.map +1 -1
- package/build-types/components/dataviews/index.d.ts +3 -2
- package/build-types/components/dataviews/index.d.ts.map +1 -1
- package/build-types/components/dataviews-filters/filters-toggled.d.ts +5 -0
- package/build-types/components/dataviews-filters/filters-toggled.d.ts.map +1 -0
- package/build-types/components/dataviews-filters/filters.d.ts +6 -0
- package/build-types/components/dataviews-filters/filters.d.ts.map +1 -0
- package/build-types/components/dataviews-filters/index.d.ts +4 -8
- package/build-types/components/dataviews-filters/index.d.ts.map +1 -1
- package/build-types/components/dataviews-filters/toggle.d.ts +3 -0
- package/build-types/components/dataviews-filters/toggle.d.ts.map +1 -0
- package/build-types/components/dataviews-filters/use-filters.d.ts +4 -0
- package/build-types/components/dataviews-filters/use-filters.d.ts.map +1 -0
- package/build-types/components/dataviews-picker/index.d.ts +3 -2
- package/build-types/components/dataviews-picker/index.d.ts.map +1 -1
- package/build-types/components/dataviews-view-config/index.d.ts.map +1 -1
- package/build-types/dataform-controls/array.d.ts.map +1 -1
- package/build-types/dataviews-layouts/picker-grid/index.d.ts.map +1 -1
- package/build-types/field-types/array.d.ts.map +1 -1
- package/build-types/index.d.ts +1 -0
- package/build-types/index.d.ts.map +1 -1
- package/build-types/types.d.ts +2 -1
- package/build-types/types.d.ts.map +1 -1
- package/build-types/validation.d.ts.map +1 -1
- package/build-wp/index.js +899 -408
- package/package.json +15 -15
- package/src/components/dataform/stories/index.story.tsx +73 -1
- package/src/components/dataviews/index.tsx +8 -14
- package/src/components/dataviews/stories/index.story.tsx +1 -1
- package/src/components/dataviews-filters/filters-toggled.tsx +20 -0
- package/src/components/dataviews-filters/filters.tsx +73 -0
- package/src/components/dataviews-filters/index.tsx +4 -246
- package/src/components/dataviews-filters/toggle.tsx +118 -0
- package/src/components/dataviews-filters/use-filters.ts +73 -0
- package/src/components/dataviews-picker/index.tsx +8 -14
- package/src/components/dataviews-view-config/index.tsx +18 -3
- package/src/dataform-controls/array.tsx +137 -40
- package/src/dataviews-layouts/picker-grid/index.tsx +15 -8
- package/src/field-types/array.tsx +0 -8
- package/src/index.ts +1 -0
- package/src/test/validation.ts +192 -0
- package/src/types.ts +2 -1
- package/src/validation.ts +30 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WordPress dependencies
|
|
3
|
+
*/
|
|
4
|
+
import { useMemo } from '@wordpress/element';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Internal dependencies
|
|
8
|
+
*/
|
|
9
|
+
import { ALL_OPERATORS, SINGLE_SELECTION_OPERATORS } from '../../constants';
|
|
10
|
+
import type { NormalizedFilter, NormalizedField, View } from '../../types';
|
|
11
|
+
|
|
12
|
+
function useFilters( fields: NormalizedField< any >[], view: View ) {
|
|
13
|
+
return useMemo( () => {
|
|
14
|
+
const filters: NormalizedFilter[] = [];
|
|
15
|
+
fields.forEach( ( field ) => {
|
|
16
|
+
if (
|
|
17
|
+
field.filterBy === false ||
|
|
18
|
+
( ! field.elements?.length && ! field.Edit )
|
|
19
|
+
) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const operators = field.filterBy.operators;
|
|
24
|
+
const isPrimary = !! field.filterBy?.isPrimary;
|
|
25
|
+
const isLocked =
|
|
26
|
+
view.filters?.some(
|
|
27
|
+
( f ) => f.field === field.id && !! f.isLocked
|
|
28
|
+
) ?? false;
|
|
29
|
+
filters.push( {
|
|
30
|
+
field: field.id,
|
|
31
|
+
name: field.label,
|
|
32
|
+
elements: field.elements ?? [],
|
|
33
|
+
singleSelection: operators.some( ( op ) =>
|
|
34
|
+
SINGLE_SELECTION_OPERATORS.includes( op )
|
|
35
|
+
),
|
|
36
|
+
operators,
|
|
37
|
+
isVisible:
|
|
38
|
+
isLocked ||
|
|
39
|
+
isPrimary ||
|
|
40
|
+
!! view.filters?.some(
|
|
41
|
+
( f ) =>
|
|
42
|
+
f.field === field.id &&
|
|
43
|
+
ALL_OPERATORS.includes( f.operator )
|
|
44
|
+
),
|
|
45
|
+
isPrimary,
|
|
46
|
+
isLocked,
|
|
47
|
+
} );
|
|
48
|
+
} );
|
|
49
|
+
|
|
50
|
+
// Sort filters by:
|
|
51
|
+
// - locked filters go first
|
|
52
|
+
// - primary filters go next
|
|
53
|
+
// - then, sort by name
|
|
54
|
+
filters.sort( ( a, b ) => {
|
|
55
|
+
if ( a.isLocked && ! b.isLocked ) {
|
|
56
|
+
return -1;
|
|
57
|
+
}
|
|
58
|
+
if ( ! a.isLocked && b.isLocked ) {
|
|
59
|
+
return 1;
|
|
60
|
+
}
|
|
61
|
+
if ( a.isPrimary && ! b.isPrimary ) {
|
|
62
|
+
return -1;
|
|
63
|
+
}
|
|
64
|
+
if ( ! a.isPrimary && b.isPrimary ) {
|
|
65
|
+
return 1;
|
|
66
|
+
}
|
|
67
|
+
return a.name.localeCompare( b.name );
|
|
68
|
+
} );
|
|
69
|
+
return filters;
|
|
70
|
+
}, [ fields, view ] );
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export default useFilters;
|
|
@@ -7,13 +7,7 @@ import type { ReactNode } from 'react';
|
|
|
7
7
|
* WordPress dependencies
|
|
8
8
|
*/
|
|
9
9
|
import { __experimentalHStack as HStack } from '@wordpress/components';
|
|
10
|
-
import {
|
|
11
|
-
useContext,
|
|
12
|
-
useEffect,
|
|
13
|
-
useMemo,
|
|
14
|
-
useRef,
|
|
15
|
-
useState,
|
|
16
|
-
} from '@wordpress/element';
|
|
10
|
+
import { useEffect, useMemo, useRef, useState } from '@wordpress/element';
|
|
17
11
|
import { useResizeObserver, throttle } from '@wordpress/compose';
|
|
18
12
|
|
|
19
13
|
/**
|
|
@@ -22,7 +16,8 @@ import { useResizeObserver, throttle } from '@wordpress/compose';
|
|
|
22
16
|
import DataViewsContext from '../dataviews-context';
|
|
23
17
|
import { VIEW_LAYOUTS } from '../../dataviews-layouts';
|
|
24
18
|
import {
|
|
25
|
-
|
|
19
|
+
Filters,
|
|
20
|
+
FiltersToggled,
|
|
26
21
|
useFilters,
|
|
27
22
|
FiltersToggle,
|
|
28
23
|
} from '../dataviews-filters';
|
|
@@ -84,7 +79,6 @@ function DefaultUI( {
|
|
|
84
79
|
search = true,
|
|
85
80
|
searchLabel = undefined,
|
|
86
81
|
}: DefaultUIProps ) {
|
|
87
|
-
const { isShowingFilter } = useContext( DataViewsContext );
|
|
88
82
|
return (
|
|
89
83
|
<>
|
|
90
84
|
<HStack
|
|
@@ -109,9 +103,7 @@ function DefaultUI( {
|
|
|
109
103
|
<DataViewsViewConfig />
|
|
110
104
|
</HStack>
|
|
111
105
|
</HStack>
|
|
112
|
-
|
|
113
|
-
<DataViewsFilters className="dataviews-filters__container" />
|
|
114
|
-
) }
|
|
106
|
+
<FiltersToggled className="dataviews-filters__container" />
|
|
115
107
|
<DataViewsLayout />
|
|
116
108
|
<DataViewsPickerFooter />
|
|
117
109
|
</>
|
|
@@ -263,7 +255,8 @@ function DataViewsPicker< Item >( {
|
|
|
263
255
|
const DataViewsPickerSubComponents =
|
|
264
256
|
DataViewsPicker as typeof DataViewsPicker & {
|
|
265
257
|
BulkActionToolbar: typeof DataViewsPickerFooter;
|
|
266
|
-
Filters: typeof
|
|
258
|
+
Filters: typeof Filters;
|
|
259
|
+
FiltersToggled: typeof FiltersToggled;
|
|
267
260
|
FiltersToggle: typeof FiltersToggle;
|
|
268
261
|
Layout: typeof DataViewsLayout;
|
|
269
262
|
LayoutSwitcher: typeof ViewTypeMenu;
|
|
@@ -273,7 +266,8 @@ const DataViewsPickerSubComponents =
|
|
|
273
266
|
};
|
|
274
267
|
|
|
275
268
|
DataViewsPickerSubComponents.BulkActionToolbar = DataViewsPickerFooter;
|
|
276
|
-
DataViewsPickerSubComponents.Filters =
|
|
269
|
+
DataViewsPickerSubComponents.Filters = Filters;
|
|
270
|
+
DataViewsPickerSubComponents.FiltersToggled = FiltersToggled;
|
|
277
271
|
DataViewsPickerSubComponents.FiltersToggle = FiltersToggle;
|
|
278
272
|
DataViewsPickerSubComponents.Layout = DataViewsLayout;
|
|
279
273
|
DataViewsPickerSubComponents.LayoutSwitcher = ViewTypeMenu;
|
|
@@ -562,9 +562,10 @@ function FieldControl() {
|
|
|
562
562
|
( f ) =>
|
|
563
563
|
! visibleFieldIds.includes( f.id ) &&
|
|
564
564
|
! togglableFields.includes( f.id ) &&
|
|
565
|
-
f.type !== 'media'
|
|
565
|
+
f.type !== 'media' &&
|
|
566
|
+
f.enableHiding !== false
|
|
566
567
|
);
|
|
567
|
-
|
|
568
|
+
let visibleFields = visibleFieldIds
|
|
568
569
|
.map( ( fieldId ) => fields.find( ( f ) => f.id === fieldId ) )
|
|
569
570
|
.filter( isDefined );
|
|
570
571
|
|
|
@@ -622,7 +623,7 @@ function FieldControl() {
|
|
|
622
623
|
isVisibleFlag: 'showDescription',
|
|
623
624
|
},
|
|
624
625
|
].filter( ( { field } ) => isDefined( field ) );
|
|
625
|
-
|
|
626
|
+
let visibleLockedFields = lockedFields.filter(
|
|
626
627
|
( { field, isVisibleFlag } ) =>
|
|
627
628
|
// @ts-expect-error
|
|
628
629
|
isDefined( field ) && ( view[ isVisibleFlag ] ?? true )
|
|
@@ -631,6 +632,20 @@ function FieldControl() {
|
|
|
631
632
|
isVisibleFlag: string;
|
|
632
633
|
ui?: ReactNode;
|
|
633
634
|
} >;
|
|
635
|
+
|
|
636
|
+
// If only one locked field is visible, prevent it from being hidden.
|
|
637
|
+
if ( visibleLockedFields.length === 1 ) {
|
|
638
|
+
visibleLockedFields = visibleLockedFields.map( ( locked ) => ( {
|
|
639
|
+
...locked,
|
|
640
|
+
field: { ...locked.field, enableHiding: false },
|
|
641
|
+
} ) );
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// If no locked fields are visible but there are visibleFields, lock the last visible field.
|
|
645
|
+
if ( visibleLockedFields.length === 0 && visibleFields.length === 1 ) {
|
|
646
|
+
visibleFields = [ { ...visibleFields[ 0 ], enableHiding: false } ];
|
|
647
|
+
}
|
|
648
|
+
|
|
634
649
|
const hiddenLockedFields = lockedFields.filter(
|
|
635
650
|
( { field, isVisibleFlag } ) =>
|
|
636
651
|
// @ts-expect-error
|
|
@@ -1,13 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* External dependencies
|
|
3
|
+
*/
|
|
4
|
+
import deepMerge from 'deepmerge';
|
|
5
|
+
|
|
1
6
|
/**
|
|
2
7
|
* WordPress dependencies
|
|
3
8
|
*/
|
|
4
|
-
import {
|
|
5
|
-
import { useCallback, useMemo } from '@wordpress/element';
|
|
9
|
+
import { privateApis } from '@wordpress/components';
|
|
10
|
+
import { useCallback, useMemo, useState } from '@wordpress/element';
|
|
11
|
+
import { _n, sprintf } from '@wordpress/i18n';
|
|
6
12
|
|
|
7
13
|
/**
|
|
8
14
|
* Internal dependencies
|
|
9
15
|
*/
|
|
10
16
|
import type { DataFormControlProps } from '../types';
|
|
17
|
+
import { unlock } from '../lock-unlock';
|
|
18
|
+
|
|
19
|
+
const { ValidatedFormTokenField } = unlock( privateApis );
|
|
11
20
|
|
|
12
21
|
export default function ArrayControl< Item >( {
|
|
13
22
|
data,
|
|
@@ -18,66 +27,154 @@ export default function ArrayControl< Item >( {
|
|
|
18
27
|
const { label, placeholder, elements, getValue, setValue } = field;
|
|
19
28
|
const value = getValue( { item: data } );
|
|
20
29
|
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
);
|
|
30
|
+
const [ customValidity, setCustomValidity ] = useState<
|
|
31
|
+
| {
|
|
32
|
+
type: 'validating' | 'valid' | 'invalid';
|
|
33
|
+
message: string;
|
|
34
|
+
}
|
|
35
|
+
| undefined
|
|
36
|
+
>( undefined );
|
|
29
37
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
return elements?.find(
|
|
33
|
-
( suggestion ) => suggestion.label === suggestionLabel
|
|
34
|
-
);
|
|
35
|
-
},
|
|
36
|
-
[ elements ]
|
|
37
|
-
);
|
|
38
|
-
|
|
39
|
-
// Ensure value is an array
|
|
40
|
-
const arrayValue = useMemo(
|
|
38
|
+
// Convert stored values to element objects for the token field
|
|
39
|
+
const arrayValueAsElements = useMemo(
|
|
41
40
|
() =>
|
|
42
41
|
Array.isArray( value )
|
|
43
42
|
? value.map( ( token ) => {
|
|
44
|
-
const
|
|
45
|
-
|
|
43
|
+
const element = elements?.find(
|
|
44
|
+
( suggestion ) => suggestion.value === token
|
|
45
|
+
);
|
|
46
|
+
return element || { value: token, label: token };
|
|
46
47
|
} )
|
|
47
48
|
: [],
|
|
48
|
-
[ value,
|
|
49
|
+
[ value, elements ]
|
|
49
50
|
);
|
|
50
51
|
|
|
51
|
-
const
|
|
52
|
-
( tokens: ( string | { value: string } )[] ) => {
|
|
53
|
-
//
|
|
54
|
-
const
|
|
55
|
-
if ( typeof token
|
|
52
|
+
const validateTokens = useCallback(
|
|
53
|
+
( tokens: ( string | { value: string; label?: string } )[] ) => {
|
|
54
|
+
// Extract actual values from tokens for validation
|
|
55
|
+
const tokenValues = tokens.map( ( token ) => {
|
|
56
|
+
if ( typeof token === 'object' && 'value' in token ) {
|
|
56
57
|
return token.value;
|
|
57
58
|
}
|
|
59
|
+
return token;
|
|
60
|
+
} );
|
|
61
|
+
|
|
62
|
+
// First, check if elements validation is required and any tokens are invalid
|
|
63
|
+
if ( field.isValid?.elements && elements ) {
|
|
64
|
+
const invalidTokens = tokenValues.filter( ( tokenValue ) => {
|
|
65
|
+
return ! elements.some(
|
|
66
|
+
( element ) => element.value === tokenValue
|
|
67
|
+
);
|
|
68
|
+
} );
|
|
69
|
+
|
|
70
|
+
if ( invalidTokens.length > 0 ) {
|
|
71
|
+
setCustomValidity( {
|
|
72
|
+
type: 'invalid',
|
|
73
|
+
message: sprintf(
|
|
74
|
+
/* translators: %s: list of invalid tokens */
|
|
75
|
+
_n(
|
|
76
|
+
'Please select from the available options: %s is invalid.',
|
|
77
|
+
'Please select from the available options: %s are invalid.',
|
|
78
|
+
invalidTokens.length
|
|
79
|
+
),
|
|
80
|
+
invalidTokens.join( ', ' )
|
|
81
|
+
),
|
|
82
|
+
} );
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Then check custom validation if provided.
|
|
88
|
+
if ( field.isValid?.custom ) {
|
|
89
|
+
const result = field.isValid?.custom?.(
|
|
90
|
+
deepMerge(
|
|
91
|
+
data,
|
|
92
|
+
setValue( {
|
|
93
|
+
item: data,
|
|
94
|
+
value: tokenValues,
|
|
95
|
+
} ) as Partial< Item >
|
|
96
|
+
),
|
|
97
|
+
field
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
if ( result ) {
|
|
101
|
+
setCustomValidity( {
|
|
102
|
+
type: 'invalid',
|
|
103
|
+
message: result,
|
|
104
|
+
} );
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
58
108
|
|
|
59
|
-
|
|
109
|
+
// If no validation errors, clear custom validity
|
|
110
|
+
setCustomValidity( undefined );
|
|
111
|
+
},
|
|
112
|
+
[ elements, data, field, setValue ]
|
|
113
|
+
);
|
|
60
114
|
|
|
61
|
-
|
|
115
|
+
const onChangeControl = useCallback(
|
|
116
|
+
( tokens: ( string | { value: string; label?: string } )[] ) => {
|
|
117
|
+
const valueTokens = tokens.map( ( token ) => {
|
|
118
|
+
if ( typeof token === 'object' && 'value' in token ) {
|
|
119
|
+
return token.value;
|
|
120
|
+
}
|
|
121
|
+
// If it's a string, it's either a new suggestion value or user input
|
|
122
|
+
return token;
|
|
62
123
|
} );
|
|
63
124
|
|
|
64
|
-
onChange( setValue( { item: data, value:
|
|
125
|
+
onChange( setValue( { item: data, value: valueTokens } ) );
|
|
65
126
|
},
|
|
66
|
-
[ onChange, setValue, data
|
|
127
|
+
[ onChange, setValue, data ]
|
|
67
128
|
);
|
|
68
129
|
|
|
69
130
|
return (
|
|
70
|
-
<
|
|
131
|
+
<ValidatedFormTokenField
|
|
132
|
+
required={ !! field.isValid?.required }
|
|
133
|
+
onValidate={ validateTokens }
|
|
134
|
+
customValidity={ customValidity }
|
|
71
135
|
label={ hideLabelFromVision ? undefined : label }
|
|
72
|
-
value={
|
|
136
|
+
value={ arrayValueAsElements }
|
|
73
137
|
onChange={ onChangeControl }
|
|
74
138
|
placeholder={ placeholder }
|
|
75
|
-
suggestions={
|
|
76
|
-
|
|
77
|
-
|
|
139
|
+
suggestions={ elements?.map( ( element ) => element.value ) }
|
|
140
|
+
__experimentalValidateInput={ ( token: string ) => {
|
|
141
|
+
// If elements validation is required, check if token is valid
|
|
142
|
+
if ( field.isValid?.elements && elements ) {
|
|
143
|
+
return elements.some(
|
|
144
|
+
( element ) =>
|
|
145
|
+
element.value === token || element.label === token
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// For non-elements validation, allow all tokens
|
|
150
|
+
return true;
|
|
151
|
+
} }
|
|
78
152
|
__experimentalExpandOnFocus={ elements && elements.length > 0 }
|
|
79
|
-
|
|
80
|
-
|
|
153
|
+
__experimentalShowHowTo={ ! field.isValid?.elements }
|
|
154
|
+
displayTransform={ ( token: any ) => {
|
|
155
|
+
// For existing tokens (element objects), display their label
|
|
156
|
+
if ( typeof token === 'object' && 'label' in token ) {
|
|
157
|
+
return token.label;
|
|
158
|
+
}
|
|
159
|
+
// For suggestions (value strings), find the corresponding element and show its label
|
|
160
|
+
if ( typeof token === 'string' && elements ) {
|
|
161
|
+
const element = elements.find(
|
|
162
|
+
( el ) => el.value === token
|
|
163
|
+
);
|
|
164
|
+
return element?.label || token;
|
|
165
|
+
}
|
|
166
|
+
return token;
|
|
167
|
+
} }
|
|
168
|
+
__experimentalRenderItem={ ( { item }: { item: any } ) => {
|
|
169
|
+
// Custom rendering for suggestion items (item is a value string)
|
|
170
|
+
if ( typeof item === 'string' && elements ) {
|
|
171
|
+
const element = elements.find(
|
|
172
|
+
( el ) => el.value === item
|
|
173
|
+
);
|
|
174
|
+
return <span>{ element?.label || item }</span>;
|
|
175
|
+
}
|
|
176
|
+
return <span>{ item }</span>;
|
|
177
|
+
} }
|
|
81
178
|
/>
|
|
82
179
|
);
|
|
83
180
|
}
|
|
@@ -89,6 +89,11 @@ function GridItem< Item >( {
|
|
|
89
89
|
|
|
90
90
|
return (
|
|
91
91
|
<Composite.Item
|
|
92
|
+
aria-label={
|
|
93
|
+
titleField
|
|
94
|
+
? titleField.getValue( { item } ) || __( '(no title)' )
|
|
95
|
+
: undefined
|
|
96
|
+
}
|
|
92
97
|
key={ id }
|
|
93
98
|
render={ ( { children, ...props } ) => (
|
|
94
99
|
<VStack spacing={ 0 } children={ children } { ...props } />
|
|
@@ -130,14 +135,16 @@ function GridItem< Item >( {
|
|
|
130
135
|
tabIndex={ -1 }
|
|
131
136
|
/>
|
|
132
137
|
) }
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
138
|
+
{ showTitle && (
|
|
139
|
+
<HStack
|
|
140
|
+
justify="space-between"
|
|
141
|
+
className="dataviews-view-picker-grid__title-actions"
|
|
142
|
+
>
|
|
143
|
+
<div className="dataviews-view-picker-grid__title-field dataviews-title-field">
|
|
144
|
+
{ renderedTitleField }
|
|
145
|
+
</div>
|
|
146
|
+
</HStack>
|
|
147
|
+
) }
|
|
141
148
|
<VStack spacing={ 1 }>
|
|
142
149
|
{ showDescription && descriptionField?.render && (
|
|
143
150
|
<descriptionField.render
|
|
@@ -59,14 +59,6 @@ const arrayFieldType: FieldTypeDefinition< any > = {
|
|
|
59
59
|
return __( 'Every value must be a string.' );
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
if ( field?.elements ) {
|
|
63
|
-
const validValues = field.elements.map( ( f ) => f.value );
|
|
64
|
-
if (
|
|
65
|
-
! value.every( ( v: any ) => validValues.includes( v ) )
|
|
66
|
-
) {
|
|
67
|
-
return __( 'Value must be one of the elements.' );
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
62
|
return null;
|
|
71
63
|
},
|
|
72
64
|
},
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { default as DataViews } from './components/dataviews';
|
|
2
|
+
export { default as DataViewsPicker } from './components/dataviews-picker';
|
|
2
3
|
export { default as DataForm } from './components/dataform';
|
|
3
4
|
export { VIEW_LAYOUTS } from './dataviews-layouts';
|
|
4
5
|
export { filterSortAndPaginate } from './filter-and-sort-data-view';
|
package/src/test/validation.ts
CHANGED
|
@@ -117,4 +117,196 @@ describe( 'validation', () => {
|
|
|
117
117
|
const result = isItemValid( item, fields, form );
|
|
118
118
|
expect( result ).toBe( true );
|
|
119
119
|
} );
|
|
120
|
+
|
|
121
|
+
it( 'array field is invalid when required but empty', () => {
|
|
122
|
+
const item = { id: 1, tags: [] };
|
|
123
|
+
const fields: Field< {} >[] = [
|
|
124
|
+
{
|
|
125
|
+
id: 'tags',
|
|
126
|
+
type: 'array',
|
|
127
|
+
isValid: {
|
|
128
|
+
required: true,
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
];
|
|
132
|
+
const form = { fields: [ 'tags' ] };
|
|
133
|
+
const result = isItemValid( item, fields, form );
|
|
134
|
+
expect( result ).toBe( false );
|
|
135
|
+
} );
|
|
136
|
+
|
|
137
|
+
it( 'array field is invalid when required but not an array', () => {
|
|
138
|
+
const item = { id: 1, tags: null };
|
|
139
|
+
const fields: Field< {} >[] = [
|
|
140
|
+
{
|
|
141
|
+
id: 'tags',
|
|
142
|
+
type: 'array',
|
|
143
|
+
isValid: {
|
|
144
|
+
required: true,
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
];
|
|
148
|
+
const form = { fields: [ 'tags' ] };
|
|
149
|
+
const result = isItemValid( item, fields, form );
|
|
150
|
+
expect( result ).toBe( false );
|
|
151
|
+
} );
|
|
152
|
+
|
|
153
|
+
it( 'array field is valid when required and has values', () => {
|
|
154
|
+
const item = { id: 1, tags: [ 'tag1', 'tag2' ] };
|
|
155
|
+
const fields: Field< {} >[] = [
|
|
156
|
+
{
|
|
157
|
+
id: 'tags',
|
|
158
|
+
type: 'array',
|
|
159
|
+
isValid: {
|
|
160
|
+
required: true,
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
];
|
|
164
|
+
const form = { fields: [ 'tags' ] };
|
|
165
|
+
const result = isItemValid( item, fields, form );
|
|
166
|
+
expect( result ).toBe( true );
|
|
167
|
+
} );
|
|
168
|
+
|
|
169
|
+
it( 'text field with isValid.elements validates against elements', () => {
|
|
170
|
+
const item = { id: 1, status: 'published' };
|
|
171
|
+
const fields: Field< {} >[] = [
|
|
172
|
+
{
|
|
173
|
+
id: 'status',
|
|
174
|
+
type: 'text',
|
|
175
|
+
elements: [
|
|
176
|
+
{ value: 'draft', label: 'Draft' },
|
|
177
|
+
{ value: 'published', label: 'Published' },
|
|
178
|
+
],
|
|
179
|
+
isValid: {
|
|
180
|
+
elements: true,
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
];
|
|
184
|
+
const form = { fields: [ 'status' ] };
|
|
185
|
+
const result = isItemValid( item, fields, form );
|
|
186
|
+
expect( result ).toBe( true );
|
|
187
|
+
} );
|
|
188
|
+
|
|
189
|
+
it( 'text field with isValid.elements rejects invalid values', () => {
|
|
190
|
+
const item = { id: 1, status: 'invalid-status' };
|
|
191
|
+
const fields: Field< {} >[] = [
|
|
192
|
+
{
|
|
193
|
+
id: 'status',
|
|
194
|
+
type: 'text',
|
|
195
|
+
elements: [
|
|
196
|
+
{ value: 'draft', label: 'Draft' },
|
|
197
|
+
{ value: 'published', label: 'Published' },
|
|
198
|
+
],
|
|
199
|
+
isValid: {
|
|
200
|
+
elements: true,
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
];
|
|
204
|
+
const form = { fields: [ 'status' ] };
|
|
205
|
+
const result = isItemValid( item, fields, form );
|
|
206
|
+
expect( result ).toBe( false );
|
|
207
|
+
} );
|
|
208
|
+
|
|
209
|
+
it( 'integer field with isValid.elements validates against elements', () => {
|
|
210
|
+
const item = { id: 1, priority: 2 };
|
|
211
|
+
const fields: Field< {} >[] = [
|
|
212
|
+
{
|
|
213
|
+
id: 'priority',
|
|
214
|
+
type: 'integer',
|
|
215
|
+
elements: [
|
|
216
|
+
{ value: 1, label: 'Low' },
|
|
217
|
+
{ value: 2, label: 'Medium' },
|
|
218
|
+
{ value: 3, label: 'High' },
|
|
219
|
+
],
|
|
220
|
+
isValid: {
|
|
221
|
+
elements: true,
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
];
|
|
225
|
+
const form = { fields: [ 'priority' ] };
|
|
226
|
+
const result = isItemValid( item, fields, form );
|
|
227
|
+
expect( result ).toBe( true );
|
|
228
|
+
} );
|
|
229
|
+
|
|
230
|
+
it( 'integer field with isValid.elements rejects invalid values', () => {
|
|
231
|
+
const item = { id: 1, priority: 5 };
|
|
232
|
+
const fields: Field< {} >[] = [
|
|
233
|
+
{
|
|
234
|
+
id: 'priority',
|
|
235
|
+
type: 'integer',
|
|
236
|
+
elements: [
|
|
237
|
+
{ value: 1, label: 'Low' },
|
|
238
|
+
{ value: 2, label: 'Medium' },
|
|
239
|
+
{ value: 3, label: 'High' },
|
|
240
|
+
],
|
|
241
|
+
isValid: {
|
|
242
|
+
elements: true,
|
|
243
|
+
},
|
|
244
|
+
},
|
|
245
|
+
];
|
|
246
|
+
const form = { fields: [ 'priority' ] };
|
|
247
|
+
const result = isItemValid( item, fields, form );
|
|
248
|
+
expect( result ).toBe( false );
|
|
249
|
+
} );
|
|
250
|
+
|
|
251
|
+
it( 'array field with isValid.elements validates all items against elements', () => {
|
|
252
|
+
const item = { id: 1, tags: [ 'red', 'blue' ] };
|
|
253
|
+
const fields: Field< {} >[] = [
|
|
254
|
+
{
|
|
255
|
+
id: 'tags',
|
|
256
|
+
type: 'array',
|
|
257
|
+
elements: [
|
|
258
|
+
{ value: 'red', label: 'Red' },
|
|
259
|
+
{ value: 'blue', label: 'Blue' },
|
|
260
|
+
{ value: 'green', label: 'Green' },
|
|
261
|
+
],
|
|
262
|
+
isValid: {
|
|
263
|
+
elements: true,
|
|
264
|
+
},
|
|
265
|
+
},
|
|
266
|
+
];
|
|
267
|
+
const form = { fields: [ 'tags' ] };
|
|
268
|
+
const result = isItemValid( item, fields, form );
|
|
269
|
+
expect( result ).toBe( true );
|
|
270
|
+
} );
|
|
271
|
+
|
|
272
|
+
it( 'array field with isValid.elements rejects arrays with invalid items', () => {
|
|
273
|
+
const item = { id: 1, tags: [ 'red', 'yellow' ] };
|
|
274
|
+
const fields: Field< {} >[] = [
|
|
275
|
+
{
|
|
276
|
+
id: 'tags',
|
|
277
|
+
type: 'array',
|
|
278
|
+
elements: [
|
|
279
|
+
{ value: 'red', label: 'Red' },
|
|
280
|
+
{ value: 'blue', label: 'Blue' },
|
|
281
|
+
{ value: 'green', label: 'Green' },
|
|
282
|
+
],
|
|
283
|
+
isValid: {
|
|
284
|
+
elements: true,
|
|
285
|
+
},
|
|
286
|
+
},
|
|
287
|
+
];
|
|
288
|
+
const form = { fields: [ 'tags' ] };
|
|
289
|
+
const result = isItemValid( item, fields, form );
|
|
290
|
+
expect( result ).toBe( false );
|
|
291
|
+
} );
|
|
292
|
+
|
|
293
|
+
it( 'array field with isValid.elements handles non-array values', () => {
|
|
294
|
+
const item = { id: 1, tags: 'not-an-array' };
|
|
295
|
+
const fields: Field< {} >[] = [
|
|
296
|
+
{
|
|
297
|
+
id: 'tags',
|
|
298
|
+
type: 'array',
|
|
299
|
+
elements: [
|
|
300
|
+
{ value: 'red', label: 'Red' },
|
|
301
|
+
{ value: 'blue', label: 'Blue' },
|
|
302
|
+
],
|
|
303
|
+
isValid: {
|
|
304
|
+
elements: true,
|
|
305
|
+
},
|
|
306
|
+
},
|
|
307
|
+
];
|
|
308
|
+
const form = { fields: [ 'tags' ] };
|
|
309
|
+
const result = isItemValid( item, fields, form );
|
|
310
|
+
expect( result ).toBe( false );
|
|
311
|
+
} );
|
|
120
312
|
} );
|
package/src/types.ts
CHANGED
|
@@ -164,6 +164,7 @@ export type FieldTypeDefinition< Item > = {
|
|
|
164
164
|
|
|
165
165
|
export type Rules< Item > = {
|
|
166
166
|
required?: boolean;
|
|
167
|
+
elements?: boolean;
|
|
167
168
|
custom?: ( item: Item, field: NormalizedField< Item > ) => null | string;
|
|
168
169
|
};
|
|
169
170
|
|
|
@@ -280,7 +281,7 @@ export type Field< Item > = {
|
|
|
280
281
|
enableGlobalSearch?: boolean;
|
|
281
282
|
|
|
282
283
|
/**
|
|
283
|
-
* Whether the field
|
|
284
|
+
* Whether the field can be hidden in the UI.
|
|
284
285
|
*/
|
|
285
286
|
enableHiding?: boolean;
|
|
286
287
|
|