@wordpress/dataviews 0.4.1 → 0.5.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.
Files changed (77) hide show
  1. package/CHANGELOG.md +2 -0
  2. package/README.md +1 -0
  3. package/build/add-filter.js +25 -108
  4. package/build/add-filter.js.map +1 -1
  5. package/build/constants.js +9 -18
  6. package/build/constants.js.map +1 -1
  7. package/build/dataviews.js +22 -16
  8. package/build/dataviews.js.map +1 -1
  9. package/build/dropdown-menu-helper.js +1 -2
  10. package/build/dropdown-menu-helper.js.map +1 -1
  11. package/build/filter-summary.js +180 -77
  12. package/build/filter-summary.js.map +1 -1
  13. package/build/filters.js +32 -18
  14. package/build/filters.js.map +1 -1
  15. package/build/pagination.js +1 -2
  16. package/build/pagination.js.map +1 -1
  17. package/build/reset-filters.js +4 -1
  18. package/build/reset-filters.js.map +1 -1
  19. package/build/search-widget.js +111 -0
  20. package/build/search-widget.js.map +1 -0
  21. package/build/search.js +2 -3
  22. package/build/search.js.map +1 -1
  23. package/build/single-selection-checkbox.js +54 -0
  24. package/build/single-selection-checkbox.js.map +1 -0
  25. package/build/utils.js +14 -1
  26. package/build/utils.js.map +1 -1
  27. package/build/view-actions.js +2 -3
  28. package/build/view-actions.js.map +1 -1
  29. package/build/view-grid.js +92 -22
  30. package/build/view-grid.js.map +1 -1
  31. package/build/view-list.js +2 -1
  32. package/build/view-list.js.map +1 -1
  33. package/build/view-table.js +43 -132
  34. package/build/view-table.js.map +1 -1
  35. package/build-module/add-filter.js +28 -111
  36. package/build-module/add-filter.js.map +1 -1
  37. package/build-module/dataviews.js +23 -17
  38. package/build-module/dataviews.js.map +1 -1
  39. package/build-module/filter-summary.js +181 -79
  40. package/build-module/filter-summary.js.map +1 -1
  41. package/build-module/filters.js +32 -17
  42. package/build-module/filters.js.map +1 -1
  43. package/build-module/reset-filters.js +4 -1
  44. package/build-module/reset-filters.js.map +1 -1
  45. package/build-module/search-widget.js +101 -0
  46. package/build-module/search-widget.js.map +1 -0
  47. package/build-module/search.js +1 -1
  48. package/build-module/search.js.map +1 -1
  49. package/build-module/single-selection-checkbox.js +47 -0
  50. package/build-module/single-selection-checkbox.js.map +1 -0
  51. package/build-module/utils.js +12 -0
  52. package/build-module/utils.js.map +1 -1
  53. package/build-module/view-actions.js +1 -1
  54. package/build-module/view-actions.js.map +1 -1
  55. package/build-module/view-grid.js +92 -22
  56. package/build-module/view-grid.js.map +1 -1
  57. package/build-module/view-list.js +2 -1
  58. package/build-module/view-list.js.map +1 -1
  59. package/build-module/view-table.js +43 -131
  60. package/build-module/view-table.js.map +1 -1
  61. package/build-style/style-rtl.css +253 -44
  62. package/build-style/style.css +253 -44
  63. package/package.json +12 -11
  64. package/src/add-filter.js +39 -230
  65. package/src/dataviews.js +31 -20
  66. package/src/filter-summary.js +228 -135
  67. package/src/filters.js +42 -29
  68. package/src/reset-filters.js +12 -2
  69. package/src/search-widget.js +128 -0
  70. package/src/search.js +1 -1
  71. package/src/single-selection-checkbox.js +59 -0
  72. package/src/style.scss +259 -44
  73. package/src/utils.js +15 -0
  74. package/src/view-actions.js +1 -2
  75. package/src/view-grid.js +127 -53
  76. package/src/view-list.js +5 -1
  77. package/src/view-table.js +57 -230
package/src/dataviews.js CHANGED
@@ -14,7 +14,7 @@ import Pagination from './pagination';
14
14
  import ViewActions from './view-actions';
15
15
  import Filters from './filters';
16
16
  import Search from './search';
17
- import { VIEW_LAYOUTS, LAYOUT_TABLE } from './constants';
17
+ import { VIEW_LAYOUTS, LAYOUT_TABLE, LAYOUT_GRID } from './constants';
18
18
  import BulkActions from './bulk-actions';
19
19
 
20
20
  const defaultGetItemId = ( item ) => item.id;
@@ -37,30 +37,33 @@ export default function DataViews( {
37
37
  deferredRendering = false,
38
38
  } ) {
39
39
  const [ selection, setSelection ] = useState( [] );
40
+ const [ openedFilter, setOpenedFilter ] = useState( null );
40
41
 
41
42
  useEffect( () => {
42
43
  if (
43
44
  selection.length > 0 &&
44
45
  selection.some(
45
- ( id ) => ! data.some( ( item ) => item.id === id )
46
+ ( id ) => ! data.some( ( item ) => getItemId( item ) === id )
46
47
  )
47
48
  ) {
48
49
  const newSelection = selection.filter( ( id ) =>
49
- data.some( ( item ) => item.id === id )
50
+ data.some( ( item ) => getItemId( item ) === id )
50
51
  );
51
52
  setSelection( newSelection );
52
53
  onSelectionChange(
53
- data.filter( ( item ) => newSelection.includes( item.id ) )
54
+ data.filter( ( item ) =>
55
+ newSelection.includes( getItemId( item ) )
56
+ )
54
57
  );
55
58
  }
56
- }, [ selection, data, onSelectionChange ] );
59
+ }, [ selection, data, getItemId, onSelectionChange ] );
57
60
 
58
61
  const onSetSelection = useCallback(
59
62
  ( items ) => {
60
- setSelection( items.map( ( item ) => item.id ) );
63
+ setSelection( items.map( ( item ) => getItemId( item ) ) );
61
64
  onSelectionChange( items );
62
65
  },
63
- [ setSelection, onSelectionChange ]
66
+ [ setSelection, getItemId, onSelectionChange ]
64
67
  );
65
68
 
66
69
  const ViewComponent = VIEW_LAYOUTS.find(
@@ -74,26 +77,20 @@ export default function DataViews( {
74
77
  }, [ fields ] );
75
78
  return (
76
79
  <div className="dataviews-wrapper">
77
- <VStack spacing={ 0 } justify="flex-start">
80
+ <VStack spacing={ 3 } justify="flex-start">
78
81
  <HStack
79
82
  alignment="flex-start"
83
+ justify="start"
80
84
  className="dataviews-filters__view-actions"
81
85
  >
82
- <HStack justify="start" wrap>
83
- { search && (
84
- <Search
85
- label={ searchLabel }
86
- view={ view }
87
- onChangeView={ onChangeView }
88
- />
89
- ) }
90
- <Filters
91
- fields={ _fields }
86
+ { search && (
87
+ <Search
88
+ label={ searchLabel }
92
89
  view={ view }
93
90
  onChangeView={ onChangeView }
94
91
  />
95
- </HStack>
96
- { view.type === LAYOUT_TABLE && (
92
+ ) }
93
+ { [ LAYOUT_TABLE, LAYOUT_GRID ].includes( view.type ) && (
97
94
  <BulkActions
98
95
  actions={ actions }
99
96
  data={ data }
@@ -109,6 +106,19 @@ export default function DataViews( {
109
106
  supportedLayouts={ supportedLayouts }
110
107
  />
111
108
  </HStack>
109
+ <HStack
110
+ justify="start"
111
+ className="dataviews-filters__container"
112
+ wrap
113
+ >
114
+ <Filters
115
+ fields={ _fields }
116
+ view={ view }
117
+ onChangeView={ onChangeView }
118
+ openedFilter={ openedFilter }
119
+ setOpenedFilter={ setOpenedFilter }
120
+ />
121
+ </HStack>
112
122
  <ViewComponent
113
123
  fields={ _fields }
114
124
  view={ view }
@@ -121,6 +131,7 @@ export default function DataViews( {
121
131
  onDetailsChange={ onDetailsChange }
122
132
  selection={ selection }
123
133
  deferredRendering={ deferredRendering }
134
+ setOpenedFilter={ setOpenedFilter }
124
135
  />
125
136
  <Pagination
126
137
  view={ view }
@@ -1,45 +1,54 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import classnames from 'classnames';
5
+
1
6
  /**
2
7
  * WordPress dependencies
3
8
  */
4
9
  import {
10
+ Dropdown,
5
11
  Button,
6
- privateApis as componentsPrivateApis,
12
+ __experimentalVStack as VStack,
13
+ __experimentalHStack as HStack,
14
+ FlexItem,
15
+ SelectControl,
16
+ Tooltip,
7
17
  Icon,
8
18
  } from '@wordpress/components';
9
- import { chevronDown } from '@wordpress/icons';
10
19
  import { __, sprintf } from '@wordpress/i18n';
11
- import { Children, Fragment } from '@wordpress/element';
20
+ import { useRef, createInterpolateElement } from '@wordpress/element';
21
+ import { closeSmall } from '@wordpress/icons';
22
+ import { ENTER, SPACE } from '@wordpress/keycodes';
12
23
 
13
24
  /**
14
25
  * Internal dependencies
15
26
  */
27
+ import SearchWidget from './search-widget';
16
28
  import { OPERATOR_IN, OPERATOR_NOT_IN, OPERATORS } from './constants';
17
- import { unlock } from './lock-unlock';
18
- import { DropdownMenuRadioItemCustom } from './dropdown-menu-helper';
19
-
20
- const {
21
- DropdownMenuV2: DropdownMenu,
22
- DropdownMenuGroupV2: DropdownMenuGroup,
23
- DropdownMenuItemV2: DropdownMenuItem,
24
- DropdownMenuSeparatorV2: DropdownMenuSeparator,
25
- DropdownMenuItemLabelV2: DropdownMenuItemLabel,
26
- DropdownMenuItemHelpTextV2: DropdownMenuItemHelpText,
27
- } = unlock( componentsPrivateApis );
28
29
 
29
30
  const FilterText = ( { activeElement, filterInView, filter } ) => {
30
31
  if ( activeElement === undefined ) {
31
32
  return filter.name;
32
33
  }
33
34
 
35
+ const filterTextWrappers = {
36
+ Span1: <span className="dataviews-filter-summary__filter-text-name" />,
37
+ Span2: <span className="dataviews-filter-summary__filter-text-value" />,
38
+ };
39
+
34
40
  if (
35
41
  activeElement !== undefined &&
36
42
  filterInView?.operator === OPERATOR_IN
37
43
  ) {
38
- return sprintf(
39
- /* translators: 1: Filter name. 2: Filter value. e.g.: "Author is Admin". */
40
- __( '%1$s is %2$s' ),
41
- filter.name,
42
- activeElement.label
44
+ return createInterpolateElement(
45
+ sprintf(
46
+ /* translators: 1: Filter name. 2: Filter value. e.g.: "Author is Admin". */
47
+ __( '<Span1>%1$s </Span1><Span2>is %2$s</Span2>' ),
48
+ filter.name,
49
+ activeElement.label
50
+ ),
51
+ filterTextWrappers
43
52
  );
44
53
  }
45
54
 
@@ -47,11 +56,14 @@ const FilterText = ( { activeElement, filterInView, filter } ) => {
47
56
  activeElement !== undefined &&
48
57
  filterInView?.operator === OPERATOR_NOT_IN
49
58
  ) {
50
- return sprintf(
51
- /* translators: 1: Filter name. 2: Filter value. e.g.: "Author is not Admin". */
52
- __( '%1$s is not %2$s' ),
53
- filter.name,
54
- activeElement.label
59
+ return createInterpolateElement(
60
+ sprintf(
61
+ /* translators: 1: Filter name. 2: Filter value. e.g.: "Author is not Admin". */
62
+ __( '<Span1>%1$s </Span1><Span2>is not %2$s</Span2>' ),
63
+ filter.name,
64
+ activeElement.label
65
+ ),
66
+ filterTextWrappers
55
67
  );
56
68
  }
57
69
 
@@ -62,127 +74,208 @@ const FilterText = ( { activeElement, filterInView, filter } ) => {
62
74
  );
63
75
  };
64
76
 
65
- function WithSeparators( { children } ) {
66
- return Children.toArray( children )
67
- .filter( Boolean )
68
- .map( ( child, i ) => (
69
- <Fragment key={ i }>
70
- { i > 0 && <DropdownMenuSeparator /> }
71
- { child }
72
- </Fragment>
73
- ) );
77
+ function OperatorSelector( { filter, view, onChangeView } ) {
78
+ const operatorOptions = filter.operators?.map( ( operator ) => ( {
79
+ value: operator,
80
+ label: OPERATORS[ operator ]?.label,
81
+ } ) );
82
+ const currentFilter = view.filters.find(
83
+ ( _filter ) => _filter.field === filter.field
84
+ );
85
+ const value = currentFilter?.operator || filter.operators[ 0 ];
86
+ return (
87
+ operatorOptions.length > 1 && (
88
+ <HStack
89
+ spacing={ 2 }
90
+ justify="flex-start"
91
+ className="dataviews-filter-summary__operators-container"
92
+ >
93
+ <FlexItem className="dataviews-filter-summary__operators-filter-name">
94
+ { filter.name }
95
+ </FlexItem>
96
+
97
+ <SelectControl
98
+ label={ __( 'Conditions' ) }
99
+ value={ value }
100
+ options={ operatorOptions }
101
+ onChange={ ( newValue ) => {
102
+ const newFilters = currentFilter
103
+ ? [
104
+ ...view.filters.map( ( _filter ) => {
105
+ if ( _filter.field === filter.field ) {
106
+ return {
107
+ ..._filter,
108
+ operator: newValue,
109
+ };
110
+ }
111
+ return _filter;
112
+ } ),
113
+ ]
114
+ : [
115
+ ...view.filters,
116
+ {
117
+ field: filter.field,
118
+ operator: newValue,
119
+ },
120
+ ];
121
+ onChangeView( {
122
+ ...view,
123
+ page: 1,
124
+ filters: newFilters,
125
+ } );
126
+ } }
127
+ size="small"
128
+ __nextHasNoMarginBottom
129
+ hideLabelFromVision
130
+ />
131
+ </HStack>
132
+ )
133
+ );
74
134
  }
75
135
 
76
- export default function FilterSummary( { filter, view, onChangeView } ) {
77
- const filterInView = view.filters.find( ( f ) => f.field === filter.field );
78
- const otherFilters = view.filters.filter(
79
- ( f ) => f.field !== filter.field
136
+ function ResetFilter( { filter, view, onChangeView, addFilterRef } ) {
137
+ const isDisabled =
138
+ filter.isPrimary &&
139
+ view.filters.find( ( _filter ) => _filter.field === filter.field )
140
+ ?.value === undefined;
141
+ return (
142
+ <div className="dataviews-filter-summary__reset">
143
+ <Button
144
+ disabled={ isDisabled }
145
+ __experimentalIsFocusable
146
+ size="compact"
147
+ variant="tertiary"
148
+ style={ { justifyContent: 'center', width: '100%' } }
149
+ onClick={ () => {
150
+ onChangeView( {
151
+ ...view,
152
+ page: 1,
153
+ filters: view.filters.filter(
154
+ ( _filter ) => _filter.field !== filter.field
155
+ ),
156
+ } );
157
+ // If the filter is not primary and can be removed, it will be added
158
+ // back to the available filters from `Add filter` component.
159
+ if ( ! filter.isPrimary ) {
160
+ addFilterRef.current?.focus();
161
+ }
162
+ } }
163
+ >
164
+ { filter.isPrimary ? __( 'Reset' ) : __( 'Remove' ) }
165
+ </Button>
166
+ </div>
80
167
  );
168
+ }
169
+
170
+ export default function FilterSummary( {
171
+ addFilterRef,
172
+ openedFilter,
173
+ ...commonProps
174
+ } ) {
175
+ const toggleRef = useRef();
176
+ const { filter, view, onChangeView } = commonProps;
177
+ const filterInView = view.filters.find( ( f ) => f.field === filter.field );
81
178
  const activeElement = filter.elements.find(
82
179
  ( element ) => element.value === filterInView?.value
83
180
  );
84
- const activeOperator = filterInView?.operator || filter.operators[ 0 ];
85
-
181
+ const isPrimary = filter.isPrimary;
182
+ const hasValues = filterInView?.value !== undefined;
183
+ const canResetOrRemove = ! isPrimary || hasValues;
86
184
  return (
87
- <DropdownMenu
88
- key={ filter.field }
89
- trigger={
90
- <Button variant="tertiary" size="compact" label={ filter.name }>
91
- <FilterText
92
- activeElement={ activeElement }
93
- filterInView={ filterInView }
94
- filter={ filter }
95
- />
96
- <Icon icon={ chevronDown } style={ { flexShrink: 0 } } />
97
- </Button>
98
- }
99
- >
100
- <WithSeparators>
101
- <DropdownMenuGroup>
102
- { filter.elements.map( ( element ) => {
103
- const isActive = activeElement?.value === element.value;
104
- return (
105
- <DropdownMenuRadioItemCustom
106
- key={ element.value }
107
- name={ `filter-summary-${ filter.field }` }
108
- value={ element.value }
109
- checked={ isActive }
110
- onClick={ () =>
185
+ <Dropdown
186
+ defaultOpen={ openedFilter === filter.field }
187
+ contentClassName="dataviews-filter-summary__popover"
188
+ popoverProps={ { placement: 'bottom-start', role: 'dialog' } }
189
+ onClose={ () => {
190
+ toggleRef.current?.focus();
191
+ } }
192
+ renderToggle={ ( { isOpen, onToggle } ) => (
193
+ <div className="dataviews-filter-summary__chip-container">
194
+ <Tooltip
195
+ text={ sprintf(
196
+ /* translators: 1: Filter name. */
197
+ __( 'Filter by: %1$s' ),
198
+ filter.name.toLowerCase()
199
+ ) }
200
+ placement="top"
201
+ >
202
+ <div
203
+ className={ classnames(
204
+ 'dataviews-filter-summary__chip',
205
+ {
206
+ 'has-reset': canResetOrRemove,
207
+ 'has-values': hasValues,
208
+ }
209
+ ) }
210
+ role="button"
211
+ tabIndex={ 0 }
212
+ onClick={ onToggle }
213
+ onKeyDown={ ( event ) => {
214
+ if (
215
+ [ ENTER, SPACE ].includes( event.keyCode )
216
+ ) {
217
+ onToggle();
218
+ event.preventDefault();
219
+ }
220
+ } }
221
+ aria-pressed={ isOpen }
222
+ aria-expanded={ isOpen }
223
+ ref={ toggleRef }
224
+ >
225
+ <FilterText
226
+ activeElement={ activeElement }
227
+ filterInView={ filterInView }
228
+ filter={ filter }
229
+ />
230
+ </div>
231
+ </Tooltip>
232
+ { canResetOrRemove && (
233
+ <Tooltip
234
+ text={ isPrimary ? __( 'Reset' ) : __( 'Remove' ) }
235
+ placement="top"
236
+ >
237
+ <button
238
+ className={ classnames(
239
+ 'dataviews-filter-summary__chip-remove',
240
+ { 'has-values': hasValues }
241
+ ) }
242
+ onClick={ () => {
111
243
  onChangeView( {
112
244
  ...view,
113
245
  page: 1,
114
- filters: [
115
- ...otherFilters,
116
- {
117
- field: filter.field,
118
- operator: activeOperator,
119
- value: isActive
120
- ? undefined
121
- : element.value,
122
- },
123
- ],
124
- } )
125
- }
126
- >
127
- <DropdownMenuItemLabel>
128
- { element.label }
129
- </DropdownMenuItemLabel>
130
- { !! element.description && (
131
- <DropdownMenuItemHelpText>
132
- { element.description }
133
- </DropdownMenuItemHelpText>
134
- ) }
135
- </DropdownMenuRadioItemCustom>
136
- );
137
- } ) }
138
- </DropdownMenuGroup>
139
- { filter.operators.length > 1 && (
140
- <DropdownMenu
141
- trigger={
142
- <DropdownMenuItem
143
- suffix={
144
- <span aria-hidden="true">
145
- { OPERATORS[ activeOperator ]?.label }
146
- </span>
147
- }
246
+ filters: view.filters.filter(
247
+ ( _filter ) =>
248
+ _filter.field !== filter.field
249
+ ),
250
+ } );
251
+ // If the filter is not primary and can be removed, it will be added
252
+ // back to the available filters from `Add filter` component.
253
+ if ( ! isPrimary ) {
254
+ addFilterRef.current?.focus();
255
+ } else {
256
+ // If is primary, focus the toggle button.
257
+ toggleRef.current?.focus();
258
+ }
259
+ } }
148
260
  >
149
- <DropdownMenuItemLabel>
150
- { __( 'Conditions' ) }
151
- </DropdownMenuItemLabel>
152
- </DropdownMenuItem>
153
- }
154
- >
155
- { Object.entries( OPERATORS ).map(
156
- ( [ operator, { label, key } ] ) => (
157
- <DropdownMenuRadioItemCustom
158
- key={ key }
159
- name={ `filter-summary-${ filter.field }-conditions` }
160
- value={ operator }
161
- checked={ activeOperator === operator }
162
- onChange={ ( e ) => {
163
- onChangeView( {
164
- ...view,
165
- page: 1,
166
- filters: [
167
- ...otherFilters,
168
- {
169
- field: filter.field,
170
- operator: e.target.value,
171
- value: filterInView?.value,
172
- },
173
- ],
174
- } );
175
- } }
176
- >
177
- <DropdownMenuItemLabel>
178
- { label }
179
- </DropdownMenuItemLabel>
180
- </DropdownMenuRadioItemCustom>
181
- )
182
- ) }
183
- </DropdownMenu>
184
- ) }
185
- </WithSeparators>
186
- </DropdownMenu>
261
+ <Icon icon={ closeSmall } />
262
+ </button>
263
+ </Tooltip>
264
+ ) }
265
+ </div>
266
+ ) }
267
+ renderContent={ () => {
268
+ return (
269
+ <VStack spacing={ 0 } justify="flex-start">
270
+ <OperatorSelector { ...commonProps } />
271
+ <SearchWidget { ...commonProps } />
272
+ <ResetFilter
273
+ { ...commonProps }
274
+ addFilterRef={ addFilterRef }
275
+ />
276
+ </VStack>
277
+ );
278
+ } }
279
+ />
187
280
  );
188
281
  }
package/src/filters.js CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * WordPress dependencies
3
3
  */
4
- import { memo } from '@wordpress/element';
4
+ import { memo, useRef } from '@wordpress/element';
5
5
 
6
6
  /**
7
7
  * Internal dependencies
@@ -9,24 +9,17 @@ import { memo } from '@wordpress/element';
9
9
  import FilterSummary from './filter-summary';
10
10
  import AddFilter from './add-filter';
11
11
  import ResetFilters from './reset-filters';
12
- import {
13
- ENUMERATION_TYPE,
14
- OPERATOR_IN,
15
- OPERATOR_NOT_IN,
16
- LAYOUT_LIST,
17
- } from './constants';
12
+ import { sanitizeOperators } from './utils';
13
+ import { ENUMERATION_TYPE, OPERATOR_IN, OPERATOR_NOT_IN } from './constants';
18
14
 
19
- const sanitizeOperators = ( field ) => {
20
- let operators = field.filterBy?.operators;
21
- if ( ! operators || ! Array.isArray( operators ) ) {
22
- operators = [ OPERATOR_IN, OPERATOR_NOT_IN ];
23
- }
24
- return operators.filter( ( operator ) =>
25
- [ OPERATOR_IN, OPERATOR_NOT_IN ].includes( operator )
26
- );
27
- };
28
-
29
- const Filters = memo( function Filters( { fields, view, onChangeView } ) {
15
+ const Filters = memo( function Filters( {
16
+ fields,
17
+ view,
18
+ onChangeView,
19
+ openedFilter,
20
+ setOpenedFilter,
21
+ } ) {
22
+ const addFilterRef = useRef();
30
23
  const filters = [];
31
24
  fields.forEach( ( field ) => {
32
25
  if ( ! field.type ) {
@@ -43,34 +36,50 @@ const Filters = memo( function Filters( { fields, view, onChangeView } ) {
43
36
  if ( ! field.elements?.length ) {
44
37
  return;
45
38
  }
39
+
40
+ const isPrimary = !! field.filterBy?.isPrimary;
46
41
  filters.push( {
47
42
  field: field.id,
48
43
  name: field.header,
49
44
  elements: field.elements,
50
45
  operators,
51
- isVisible: view.filters.some(
52
- ( f ) =>
53
- f.field === field.id &&
54
- [ OPERATOR_IN, OPERATOR_NOT_IN ].includes(
55
- f.operator
56
- )
57
- ),
46
+ isVisible:
47
+ isPrimary ||
48
+ view.filters.some(
49
+ ( f ) =>
50
+ f.field === field.id &&
51
+ [ OPERATOR_IN, OPERATOR_NOT_IN ].includes(
52
+ f.operator
53
+ )
54
+ ),
55
+ isPrimary,
58
56
  } );
59
57
  }
60
58
  } );
61
-
59
+ // Sort filters by primary property. We need the primary filters to be first.
60
+ // Then we sort by name.
61
+ filters.sort( ( a, b ) => {
62
+ if ( a.isPrimary && ! b.isPrimary ) {
63
+ return -1;
64
+ }
65
+ if ( ! a.isPrimary && b.isPrimary ) {
66
+ return 1;
67
+ }
68
+ return a.name.localeCompare( b.name );
69
+ } );
62
70
  const addFilter = (
63
71
  <AddFilter
64
72
  key="add-filter"
65
73
  filters={ filters }
66
74
  view={ view }
67
75
  onChangeView={ onChangeView }
76
+ ref={ addFilterRef }
77
+ setOpenedFilter={ setOpenedFilter }
68
78
  />
69
79
  );
70
80
  const filterComponents = [
71
- addFilter,
72
81
  ...filters.map( ( filter ) => {
73
- if ( ! filter.isVisible || view.type === LAYOUT_LIST ) {
82
+ if ( ! filter.isVisible ) {
74
83
  return null;
75
84
  }
76
85
 
@@ -80,15 +89,19 @@ const Filters = memo( function Filters( { fields, view, onChangeView } ) {
80
89
  filter={ filter }
81
90
  view={ view }
82
91
  onChangeView={ onChangeView }
92
+ addFilterRef={ addFilterRef }
93
+ openedFilter={ openedFilter }
83
94
  />
84
95
  );
85
96
  } ),
97
+ addFilter,
86
98
  ];
87
99
 
88
- if ( filterComponents.length > 1 && view.type !== LAYOUT_LIST ) {
100
+ if ( filterComponents.length > 1 ) {
89
101
  filterComponents.push(
90
102
  <ResetFilters
91
103
  key="reset-filters"
104
+ filters={ filters }
92
105
  view={ view }
93
106
  onChangeView={ onChangeView }
94
107
  />
@@ -4,10 +4,20 @@
4
4
  import { Button } from '@wordpress/components';
5
5
  import { __ } from '@wordpress/i18n';
6
6
 
7
- export default function ResetFilter( { view, onChangeView } ) {
7
+ export default function ResetFilter( { filters, view, onChangeView } ) {
8
+ const isPrimary = ( field ) =>
9
+ filters.some(
10
+ ( _filter ) => _filter.field === field && _filter.isPrimary
11
+ );
12
+ const isDisabled =
13
+ ! view.search &&
14
+ ! view.filters?.some(
15
+ ( _filter ) =>
16
+ _filter.value !== undefined || ! isPrimary( _filter.field )
17
+ );
8
18
  return (
9
19
  <Button
10
- disabled={ view.search === '' && view.filters?.length === 0 }
20
+ disabled={ isDisabled }
11
21
  __experimentalIsFocusable
12
22
  size="compact"
13
23
  variant="tertiary"