Skip to content

Commit 8ea262d

Browse files
authored
DataViews: Add user input filter (#103276)
* Add userInput property to FilterByConfig and NormalizedFilter interfaces This update introduces a new optional property, userInput, to both the FilterByConfig and NormalizedFilter interfaces, allowing filters to accept user input. Additionally, a type property is added to NormalizedFilter for better field type specification. * Add UserInputWidget component for filter input handling This commit introduces the UserInputWidget component, which allows users to input values for filters in the dataviews. It includes a SingleInputSummary for managing single selection operators and utilizes a debounced input for better performance. The component integrates with existing filter logic to update the view's filters based on user input. * Enhance FilterSummary to support UserInputWidget for dynamic filtering This update modifies the FilterSummary component to conditionally render the UserInputWidget based on the userInput property of filters. It also adjusts the logic for determining active elements to accommodate user input, ensuring a seamless integration with existing filter functionalities. Additionally, the useFilters hook is updated to handle the new userInput property, and styles are added for the UserInputWidget. * Refine operator filtering logic in sanitizeOperators function This update enhances the sanitizeOperators function to specifically filter operators for integer fields. Additionally, the logic in the column header menu is adjusted to allow adding filters based on user input or existing elements, improving the overall filtering capabilities in the dataviews. * Add filterBy configuration for integer fields to support user input This update introduces a filterBy property for integer fields in the fixtures, allowing the use of specific operators and enabling user input for filtering. This enhancement aligns with previous updates to improve filtering capabilities in the dataviews. * Refactor UserInputWidget to streamline filter handling This update refines the UserInputWidget component by renaming the currentFilter to activeFilter for clarity and consistency. The applyFilterChange function is simplified to directly handle the new value logic, enhancing readability. Additionally, the SingleInputSummary component is updated to ensure it only renders when an active filter is present and valid, improving the overall user experience in managing filters. * Refactor filtering components to enhance user input handling * Update SearchWidget to refine integer filter handling This change modifies the SearchWidget component to ensure that the InputWidget is only rendered for integer filters when no elements are present. This adjustment enhances the filtering logic and improves user experience by preventing unnecessary input fields from appearing. * Add changelog * Enhance filter functionality by adding user input support based on the Edit Update changelog to reflect this change. * Refactor input and search widgets to utilize fields prop for improved functionality * Fix type * Fix types * Rename FilterSummary component to Filter * Fix types * Pass all filter data to Edit * Revert filter.elements type and check length instead * Update filterBy property to be required and initialize with default operators in field types * Change variable name * Fix lint
1 parent bcdb9ab commit 8ea262d

File tree

18 files changed

+213
-32
lines changed

18 files changed

+213
-32
lines changed

client/sites/components/sites-dataviews/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import SiteField from './dataviews-fields/site-field';
1717
import SiteIcon from './site-icon';
1818
import { SiteStats } from './sites-site-stats';
1919
import { SiteStatus } from './sites-site-status';
20-
import type { View } from '@wordpress/dataviews';
20+
import type { View } from '@automattic/dataviews';
2121
import './style.scss';
2222
import './dataview-style.scss';
2323

packages/dataviews/CHANGELOG.automattic.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
- Add `renderItemLink` prop support in the `DataViews` component. It replaces `onClickItem`prop and allows integration with router libraries.
1111
- Enhance filter component styles.
1212
- Adds new story that combines DataViews and DataForm.
13+
- Add user input filter support based on the `Edit` property of the field type definitions.
1314

1415
## 0.1.1
1516

packages/dataviews/src/components/dataviews-filters/filter-summary.tsx renamed to packages/dataviews/src/components/dataviews-filters/filter.tsx

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ const SPACE = ' ';
2727
* Internal dependencies
2828
*/
2929
import SearchWidget from './search-widget';
30+
import InputWidget from './input-widget';
3031
import {
3132
OPERATORS,
3233
OPERATOR_IS,
@@ -42,6 +43,7 @@ import type {
4243
Operator,
4344
Option,
4445
View,
46+
NormalizedField,
4547
} from '../../types';
4648

4749
interface FilterTextProps {
@@ -56,9 +58,10 @@ interface OperatorSelectorProps {
5658
onChangeView: ( view: View ) => void;
5759
}
5860

59-
interface FilterSummaryProps extends OperatorSelectorProps {
61+
interface FilterProps extends OperatorSelectorProps {
6062
addFilterRef: RefObject< HTMLButtonElement >;
6163
openedFilter: string | null;
64+
fields: NormalizedField< any >[];
6265
}
6366

6467
const FilterText = ( {
@@ -226,22 +229,36 @@ function OperatorSelector( {
226229
);
227230
}
228231

229-
export default function FilterSummary( {
232+
export default function Filter( {
230233
addFilterRef,
231234
openedFilter,
235+
fields,
232236
...commonProps
233-
}: FilterSummaryProps ) {
237+
}: FilterProps ) {
234238
const toggleRef = useRef< HTMLDivElement >( null );
235239
const { filter, view, onChangeView } = commonProps;
236240
const filterInView = view.filters?.find(
237241
( f ) => f.field === filter.field
238242
);
239-
const activeElements = filter.elements.filter( ( element ) => {
240-
if ( filter.singleSelection ) {
241-
return element.value === filterInView?.value;
242-
}
243-
return filterInView?.value?.includes( element.value );
244-
} );
243+
244+
let activeElements: Option[] = [];
245+
246+
if ( filter.elements.length > 0 ) {
247+
activeElements = filter.elements.filter( ( element ) => {
248+
if ( filter.singleSelection ) {
249+
return element.value === filterInView?.value;
250+
}
251+
return filterInView?.value?.includes( element.value );
252+
} );
253+
} else if ( filterInView?.value !== undefined ) {
254+
activeElements = [
255+
{
256+
value: filterInView.value,
257+
label: filterInView.value,
258+
},
259+
];
260+
}
261+
245262
const isPrimary = filter.isPrimary;
246263
const hasValues = filterInView?.value !== undefined;
247264
const canResetOrRemove = ! isPrimary || hasValues;
@@ -330,7 +347,17 @@ export default function FilterSummary( {
330347
return (
331348
<VStack spacing={ 0 } justify="flex-start">
332349
<OperatorSelector { ...commonProps } />
333-
<SearchWidget { ...commonProps } />
350+
{ commonProps.filter.elements.length > 0 ? (
351+
<SearchWidget
352+
{ ...commonProps }
353+
filter={ {
354+
...commonProps.filter,
355+
elements: commonProps.filter.elements,
356+
} }
357+
/>
358+
) : (
359+
<InputWidget { ...commonProps } fields={ fields } />
360+
) }
334361
</VStack>
335362
);
336363
} }

packages/dataviews/src/components/dataviews-filters/index.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { __, _x } from '@wordpress/i18n';
1616
/**
1717
* Internal dependencies
1818
*/
19-
import FilterSummary from './filter-summary';
19+
import Filter from './filter';
2020
import { default as AddFilter, AddFilterMenu } from './add-filter';
2121
import ResetFilters from './reset-filters';
2222
import DataViewsContext from '../dataviews-context';
@@ -28,7 +28,7 @@ export function useFilters( fields: NormalizedField< any >[], view: View ) {
2828
return useMemo( () => {
2929
const filters: NormalizedFilter[] = [];
3030
fields.forEach( ( field ) => {
31-
if ( ! field.elements?.length ) {
31+
if ( ! field.elements?.length && ! field.Edit ) {
3232
return;
3333
}
3434

@@ -41,7 +41,7 @@ export function useFilters( fields: NormalizedField< any >[], view: View ) {
4141
filters.push( {
4242
field: field.id,
4343
name: field.label,
44-
elements: field.elements,
44+
elements: field.elements ?? [],
4545
singleSelection: operators.some( ( op ) =>
4646
[ OPERATOR_IS, OPERATOR_IS_NOT ].includes( op )
4747
),
@@ -195,10 +195,11 @@ function Filters( { className }: { className?: string } ) {
195195
const filterComponents = [
196196
...visibleFilters.map( ( filter ) => {
197197
return (
198-
<FilterSummary
198+
<Filter
199199
key={ filter.field }
200200
filter={ filter }
201201
view={ view }
202+
fields={ fields }
202203
onChangeView={ onChangeView }
203204
addFilterRef={ addFilterRef }
204205
openedFilter={ openedFilter }
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/**
2+
* WordPress dependencies
3+
*/
4+
import { __ } from '@wordpress/i18n';
5+
import { useCallback, useMemo } from '@wordpress/element';
6+
7+
/**
8+
* Internal dependencies
9+
*/
10+
import type { View, NormalizedFilter, NormalizedField } from '../../types';
11+
12+
interface UserInputWidgetProps {
13+
view: View;
14+
filter: NormalizedFilter;
15+
onChangeView: ( view: View ) => void;
16+
fields: NormalizedField< any >[];
17+
}
18+
19+
export default function InputWidget( {
20+
filter,
21+
view,
22+
onChangeView,
23+
fields,
24+
}: UserInputWidgetProps ) {
25+
const currentFilter = view.filters?.find(
26+
( f ) => f.field === filter.field
27+
);
28+
if ( ! currentFilter ) {
29+
return null;
30+
}
31+
32+
const field = fields.find( ( f ) => f.id === filter.field );
33+
if ( ! field || ! field.Edit ) {
34+
return null;
35+
}
36+
37+
const currentValue = currentFilter.value;
38+
39+
const data = useMemo( () => {
40+
return ( view.filters ?? [] ).reduce(
41+
( acc, f ) => {
42+
acc[ f.field ] = f.value;
43+
return acc;
44+
},
45+
{} as Record< string, any >
46+
);
47+
}, [ view.filters ] );
48+
49+
const handleChange = useCallback(
50+
( data: Record< string, any > ) => {
51+
const nextValue = data[ field.id ];
52+
if ( nextValue === currentValue ) {
53+
return;
54+
}
55+
56+
onChangeView( {
57+
...view,
58+
filters: ( view.filters ?? [] ).map( ( _filter ) =>
59+
_filter.field === filter.field
60+
? {
61+
..._filter,
62+
operator:
63+
currentFilter.operator ||
64+
filter.operators[ 0 ],
65+
value: nextValue,
66+
}
67+
: _filter
68+
),
69+
} );
70+
},
71+
[ currentValue, field, onChangeView, view, filter, currentFilter ]
72+
);
73+
74+
return (
75+
<div className="dataviews-filters__user-input-widget">
76+
<field.Edit
77+
data={ data }
78+
field={ field }
79+
onChange={ handleChange }
80+
/>
81+
</div>
82+
);
83+
}

packages/dataviews/src/components/dataviews-filters/search-widget.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,19 @@ import { search, check } from '@wordpress/icons';
1818
/**
1919
* Internal dependencies
2020
*/
21-
import type { Filter, NormalizedFilter, View } from '../../types';
21+
import type {
22+
Filter,
23+
NormalizedFilter,
24+
View,
25+
NormalizedField,
26+
Option,
27+
} from '../../types';
2228

2329
interface SearchWidgetProps {
2430
view: View;
25-
filter: NormalizedFilter;
31+
filter: NormalizedFilter & {
32+
elements: Option[];
33+
};
2634
onChangeView: ( view: View ) => void;
2735
}
2836

packages/dataviews/src/components/dataviews-filters/style.scss

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@
3636
.dataviews-filters__summary-operators-container {
3737
padding: $grid-unit-10 $grid-unit-20;
3838

39-
&:has(+ .dataviews-filters__search-widget-listbox) {
39+
&:has(+ .dataviews-filters__search-widget-listbox),
40+
&:has(+ .dataviews-filters__user-input-widget) {
4041
border-bottom: 1px solid $gray-200;
4142
}
4243

@@ -375,3 +376,11 @@
375376
.dataviews-search {
376377
width: fit-content;
377378
}
379+
380+
.dataviews-filters__user-input-widget {
381+
padding: $grid-unit-20;
382+
383+
.components-input-control__prefix {
384+
padding-left: $grid-unit-10;
385+
}
386+
}

packages/dataviews/src/components/dataviews-selection-checkbox/index.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,15 @@ import { __ } from '@wordpress/i18n';
77
/**
88
* Internal dependencies
99
*/
10-
import type { Field } from '../../types';
1110
import type { SetSelection } from '../../private-types';
11+
import type { NormalizedField } from '../../types';
1212

1313
interface DataViewsSelectionCheckboxProps< Item > {
1414
selection: string[];
1515
onChangeSelection: SetSelection;
1616
item: Item;
1717
getItemId: ( item: Item ) => string;
18-
titleField?: Field< Item >;
18+
titleField?: NormalizedField< Item >;
1919
disabled: boolean;
2020
}
2121

packages/dataviews/src/dataforms-layouts/regular/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ export default function FormRegularField< Item >( {
7979
( fieldDef ) => fieldDef.id === field.id
8080
);
8181

82-
if ( ! fieldDefinition ) {
82+
if ( ! fieldDefinition || ! fieldDefinition.Edit ) {
8383
return null;
8484
}
8585
if ( labelPosition === 'side' ) {

packages/dataviews/src/dataviews-layouts/table/column-header-menu.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -84,11 +84,12 @@ const _HeaderMenu = forwardRef( function HeaderMenu< Item >(
8484
operators = sanitizeOperators( field );
8585
// Filter can be added:
8686
// 1. If the field is not already part of a view's filters.
87-
// 2. If the field meets the type and operator requirements.
88-
// 3. If it's not primary. If it is, it should be already visible.
87+
// 2. If the field has elements or Edit property.
88+
// 3. If the field meets the type and operator requirements.
89+
// 4. If it's not primary. If it is, it should be already visible.
8990
canAddFilter =
9091
! view.filters?.some( ( _filter ) => fieldId === _filter.field ) &&
91-
!! field.elements?.length &&
92+
!! ( field.elements?.length || field.Edit ) &&
9293
!! operators.length &&
9394
! field.filterBy?.isPrimary;
9495

packages/dataviews/src/field-types/boolean.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,11 @@ import { __ } from '@wordpress/i18n';
99
import type {
1010
DataViewRenderFieldProps,
1111
SortDirection,
12+
Operator,
1213
ValidationContext,
1314
} from '../types';
1415
import { renderFromElements } from '../utils';
16+
import { OPERATOR_IS, OPERATOR_IS_NOT } from '../constants';
1517

1618
function sort( a: any, b: any, direction: SortDirection ) {
1719
const boolA = Boolean( a );
@@ -38,6 +40,8 @@ function isValid( value: any, context?: ValidationContext ) {
3840
return true;
3941
}
4042

43+
const operators: Operator[] = [ OPERATOR_IS, OPERATOR_IS_NOT ];
44+
4145
export default {
4246
sort,
4347
isValid,
@@ -58,4 +62,7 @@ export default {
5862
return null;
5963
},
6064
enableSorting: true,
65+
filterBy: {
66+
operators,
67+
},
6168
};

packages/dataviews/src/field-types/datetime.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@ import type {
55
DataViewRenderFieldProps,
66
SortDirection,
77
ValidationContext,
8+
Operator,
89
} from '../types';
910
import { renderFromElements } from '../utils';
11+
import { OPERATOR_IS, OPERATOR_IS_NOT } from '../constants';
1012

1113
function sort( a: any, b: any, direction: SortDirection ) {
1214
const timeA = new Date( a ).getTime();
@@ -26,6 +28,8 @@ function isValid( value: any, context?: ValidationContext ) {
2628
return true;
2729
}
2830

31+
const operators: Operator[] = [ OPERATOR_IS, OPERATOR_IS_NOT ];
32+
2933
export default {
3034
sort,
3135
isValid,
@@ -36,4 +40,7 @@ export default {
3640
: field.getValue( { item } );
3741
},
3842
enableSorting: true,
43+
filterBy: {
44+
operators,
45+
},
3946
};

0 commit comments

Comments
 (0)