@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/view-grid.js CHANGED
@@ -1,3 +1,8 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import classnames from 'classnames';
5
+
1
6
  /**
2
7
  * WordPress dependencies
3
8
  */
@@ -8,11 +13,115 @@ import {
8
13
  Tooltip,
9
14
  } from '@wordpress/components';
10
15
  import { useAsyncList } from '@wordpress/compose';
16
+ import { useState } from '@wordpress/element';
11
17
 
12
18
  /**
13
19
  * Internal dependencies
14
20
  */
15
21
  import ItemActions from './item-actions';
22
+ import SingleSelectionCheckbox from './single-selection-checkbox';
23
+
24
+ function GridItem( {
25
+ selection,
26
+ data,
27
+ onSelectionChange,
28
+ getItemId,
29
+ item,
30
+ actions,
31
+ mediaField,
32
+ primaryField,
33
+ visibleFields,
34
+ } ) {
35
+ const [ hasNoPointerEvents, setHasNoPointerEvents ] = useState( false );
36
+ const id = getItemId( item );
37
+ const isSelected = selection.includes( id );
38
+ return (
39
+ <VStack
40
+ spacing={ 0 }
41
+ key={ id }
42
+ className={ classnames( 'dataviews-view-grid__card', {
43
+ 'is-selected': isSelected,
44
+ 'has-no-pointer-events': hasNoPointerEvents,
45
+ } ) }
46
+ onMouseDown={ ( event ) => {
47
+ if ( event.ctrlKey || event.metaKey ) {
48
+ setHasNoPointerEvents( true );
49
+ if ( ! isSelected ) {
50
+ onSelectionChange(
51
+ data.filter( ( _item ) => {
52
+ const itemId = getItemId?.( _item );
53
+ return (
54
+ itemId === id ||
55
+ selection.includes( itemId )
56
+ );
57
+ } )
58
+ );
59
+ } else {
60
+ onSelectionChange(
61
+ data.filter( ( _item ) => {
62
+ const itemId = getItemId?.( _item );
63
+ return (
64
+ itemId !== id &&
65
+ selection.includes( itemId )
66
+ );
67
+ } )
68
+ );
69
+ }
70
+ }
71
+ } }
72
+ onClick={ () => {
73
+ if ( hasNoPointerEvents ) {
74
+ setHasNoPointerEvents( false );
75
+ }
76
+ } }
77
+ >
78
+ <div className="dataviews-view-grid__media">
79
+ { mediaField?.render( { item } ) }
80
+ </div>
81
+ <HStack
82
+ justify="space-between"
83
+ className="dataviews-view-grid__title-actions"
84
+ >
85
+ <SingleSelectionCheckbox
86
+ id={ id }
87
+ item={ item }
88
+ selection={ selection }
89
+ onSelectionChange={ onSelectionChange }
90
+ getItemId={ getItemId }
91
+ data={ data }
92
+ primaryField={ primaryField }
93
+ />
94
+ <HStack className="dataviews-view-grid__primary-field">
95
+ { primaryField?.render( { item } ) }
96
+ </HStack>
97
+ <ItemActions item={ item } actions={ actions } isCompact />
98
+ </HStack>
99
+ <VStack className="dataviews-view-grid__fields" spacing={ 3 }>
100
+ { visibleFields.map( ( field ) => {
101
+ const renderedValue = field.render( {
102
+ item,
103
+ } );
104
+ if ( ! renderedValue ) {
105
+ return null;
106
+ }
107
+ return (
108
+ <VStack
109
+ className="dataviews-view-grid__field"
110
+ key={ field.id }
111
+ spacing={ 1 }
112
+ >
113
+ <Tooltip text={ field.header } placement="left">
114
+ <div className="dataviews-view-grid__field-value">
115
+ { renderedValue }
116
+ </div>
117
+ </Tooltip>
118
+ </VStack>
119
+ );
120
+ } ) }
121
+ </VStack>
122
+ </VStack>
123
+ );
124
+ }
16
125
 
17
126
  export default function ViewGrid( {
18
127
  data,
@@ -21,6 +130,8 @@ export default function ViewGrid( {
21
130
  actions,
22
131
  getItemId,
23
132
  deferredRendering,
133
+ selection,
134
+ onSelectionChange,
24
135
  } ) {
25
136
  const mediaField = fields.find(
26
137
  ( field ) => field.id === view.layout.mediaField
@@ -44,59 +155,22 @@ export default function ViewGrid( {
44
155
  alignment="top"
45
156
  className="dataviews-view-grid"
46
157
  >
47
- { usedData.map( ( item ) => (
48
- <VStack
49
- spacing={ 0 }
50
- key={ getItemId( item ) }
51
- className="dataviews-view-grid__card"
52
- >
53
- <div className="dataviews-view-grid__media">
54
- { mediaField?.render( { item } ) }
55
- </div>
56
- <HStack
57
- justify="space-between"
58
- className="dataviews-view-grid__title-actions"
59
- >
60
- <HStack className="dataviews-view-grid__primary-field">
61
- { primaryField?.render( { item } ) }
62
- </HStack>
63
- <ItemActions
64
- item={ item }
65
- actions={ actions }
66
- isCompact
67
- />
68
- </HStack>
69
- <VStack
70
- className="dataviews-view-grid__fields"
71
- spacing={ 3 }
72
- >
73
- { visibleFields.map( ( field ) => {
74
- const renderedValue = field.render( {
75
- item,
76
- } );
77
- if ( ! renderedValue ) {
78
- return null;
79
- }
80
- return (
81
- <VStack
82
- className="dataviews-view-grid__field"
83
- key={ field.id }
84
- spacing={ 1 }
85
- >
86
- <Tooltip
87
- text={ field.header }
88
- placement="left"
89
- >
90
- <div className="dataviews-view-grid__field-value">
91
- { renderedValue }
92
- </div>
93
- </Tooltip>
94
- </VStack>
95
- );
96
- } ) }
97
- </VStack>
98
- </VStack>
99
- ) ) }
158
+ { usedData.map( ( item ) => {
159
+ return (
160
+ <GridItem
161
+ key={ getItemId( item ) }
162
+ selection={ selection }
163
+ data={ data }
164
+ onSelectionChange={ onSelectionChange }
165
+ getItemId={ getItemId }
166
+ item={ item }
167
+ actions={ actions }
168
+ mediaField={ mediaField }
169
+ primaryField={ primaryField }
170
+ visibleFields={ visibleFields }
171
+ />
172
+ );
173
+ } ) }
100
174
  </Grid>
101
175
  );
102
176
  }
package/src/view-list.js CHANGED
@@ -85,7 +85,11 @@ export default function ViewList( {
85
85
  className="dataviews-view-list__item"
86
86
  onClick={ () => onSelectionChange( [ item ] ) }
87
87
  >
88
- <HStack spacing={ 3 } justify="start">
88
+ <HStack
89
+ spacing={ 3 }
90
+ justify="start"
91
+ alignment="flex-start"
92
+ >
89
93
  <div className="dataviews-view-list__media-wrapper">
90
94
  { mediaField?.render( { item } ) || (
91
95
  <div className="dataviews-view-list__media-placeholder"></div>
package/src/view-table.js CHANGED
@@ -6,7 +6,7 @@ import classnames from 'classnames';
6
6
  /**
7
7
  * WordPress dependencies
8
8
  */
9
- import { __, sprintf } from '@wordpress/i18n';
9
+ import { __ } from '@wordpress/i18n';
10
10
  import { useAsyncList } from '@wordpress/compose';
11
11
  import { unseen, funnel } from '@wordpress/icons';
12
12
  import {
@@ -16,75 +16,66 @@ import {
16
16
  CheckboxControl,
17
17
  } from '@wordpress/components';
18
18
  import {
19
- Children,
20
- Fragment,
21
19
  forwardRef,
22
20
  useEffect,
23
21
  useId,
24
22
  useRef,
25
23
  useState,
24
+ Children,
25
+ Fragment,
26
26
  } from '@wordpress/element';
27
27
 
28
28
  /**
29
29
  * Internal dependencies
30
30
  */
31
+ import SingleSelectionCheckbox from './single-selection-checkbox';
31
32
  import { unlock } from './lock-unlock';
32
33
  import ItemActions from './item-actions';
33
- import { ENUMERATION_TYPE, OPERATORS, SORTING_DIRECTIONS } from './constants';
34
- import { DropdownMenuRadioItemCustom } from './dropdown-menu-helper';
34
+ import { sanitizeOperators } from './utils';
35
+ import { ENUMERATION_TYPE, SORTING_DIRECTIONS } from './constants';
35
36
 
36
37
  const {
37
38
  DropdownMenuV2: DropdownMenu,
38
39
  DropdownMenuGroupV2: DropdownMenuGroup,
39
40
  DropdownMenuItemV2: DropdownMenuItem,
40
41
  DropdownMenuRadioItemV2: DropdownMenuRadioItem,
41
- DropdownMenuSeparatorV2: DropdownMenuSeparator,
42
42
  DropdownMenuItemLabelV2: DropdownMenuItemLabel,
43
- DropdownMenuItemHelpTextV2: DropdownMenuItemHelpText,
43
+ DropdownMenuSeparatorV2: DropdownMenuSeparator,
44
44
  } = unlock( componentsPrivateApis );
45
45
 
46
- const sortArrows = { asc: '↑', desc: '↓' };
46
+ function WithSeparators( { children } ) {
47
+ return Children.toArray( children )
48
+ .filter( Boolean )
49
+ .map( ( child, i ) => (
50
+ <Fragment key={ i }>
51
+ { i > 0 && <DropdownMenuSeparator /> }
52
+ { child }
53
+ </Fragment>
54
+ ) );
55
+ }
47
56
 
48
- const sanitizeOperators = ( field ) => {
49
- let operators = field.filterBy?.operators;
50
- if ( ! operators || ! Array.isArray( operators ) ) {
51
- operators = Object.keys( OPERATORS );
52
- }
53
- return operators.filter( ( operator ) =>
54
- Object.keys( OPERATORS ).includes( operator )
55
- );
56
- };
57
+ const sortArrows = { asc: '↑', desc: '↓' };
57
58
 
58
59
  const HeaderMenu = forwardRef( function HeaderMenu(
59
- { field, view, onChangeView, onHide },
60
+ { field, view, onChangeView, onHide, setOpenedFilter },
60
61
  ref
61
62
  ) {
62
63
  const isHidable = field.enableHiding !== false;
63
-
64
64
  const isSortable = field.enableSorting !== false;
65
65
  const isSorted = view.sort?.field === field.id;
66
-
67
- let filter, filterInView, activeElement, activeOperator, otherFilters;
68
66
  const operators = sanitizeOperators( field );
69
- if ( field.type === ENUMERATION_TYPE && operators.length > 0 ) {
70
- filter = {
71
- field: field.id,
72
- operators,
73
- elements: field.elements || [],
74
- };
75
- filterInView = view.filters.find( ( f ) => f.field === filter.field );
76
- otherFilters = view.filters.filter( ( f ) => f.field !== filter.field );
77
- activeElement = filter.elements.find(
78
- ( element ) => element.value === filterInView?.value
79
- );
80
- activeOperator = filterInView?.operator || filter.operators[ 0 ];
81
- }
82
- const isFilterable = !! filter;
83
-
84
- if ( ! isSortable && ! isHidable && ! isFilterable ) {
67
+ // Filter can be added:
68
+ // 1. If the field is not already part of a view's filters.
69
+ // 2. If the field meets the type and operator requirements.
70
+ // 3. If it's not primary. If it is, it should be already visible.
71
+ const canAddFilter =
72
+ ! view.filters?.some( ( _filter ) => field.id === _filter.field ) &&
73
+ field.type === ENUMERATION_TYPE &&
74
+ !! operators.length &&
75
+ ! field.filterBy?.isPrimary;
76
+ if ( ! isSortable && ! isHidable && ! canAddFilter ) {
85
77
  return field.header;
86
78
  }
87
-
88
79
  return (
89
80
  <DropdownMenu
90
81
  align="start"
@@ -146,6 +137,32 @@ const HeaderMenu = forwardRef( function HeaderMenu(
146
137
  ) }
147
138
  </DropdownMenuGroup>
148
139
  ) }
140
+ { canAddFilter && (
141
+ <DropdownMenuGroup>
142
+ <DropdownMenuItem
143
+ prefix={ <Icon icon={ funnel } /> }
144
+ onClick={ () => {
145
+ setOpenedFilter( field.id );
146
+ onChangeView( {
147
+ ...view,
148
+ page: 1,
149
+ filters: [
150
+ ...( view.filters || [] ),
151
+ {
152
+ field: field.id,
153
+ value: undefined,
154
+ operator: operators[ 0 ],
155
+ },
156
+ ],
157
+ } );
158
+ } }
159
+ >
160
+ <DropdownMenuItemLabel>
161
+ { __( 'Add filter' ) }
162
+ </DropdownMenuItemLabel>
163
+ </DropdownMenuItem>
164
+ </DropdownMenuGroup>
165
+ ) }
149
166
  { isHidable && (
150
167
  <DropdownMenuItem
151
168
  prefix={ <Icon icon={ unseen } /> }
@@ -164,149 +181,11 @@ const HeaderMenu = forwardRef( function HeaderMenu(
164
181
  </DropdownMenuItemLabel>
165
182
  </DropdownMenuItem>
166
183
  ) }
167
- { isFilterable && (
168
- <DropdownMenuGroup>
169
- <DropdownMenu
170
- key={ filter.field }
171
- trigger={
172
- <DropdownMenuItem
173
- prefix={ <Icon icon={ funnel } /> }
174
- suffix={
175
- activeElement && (
176
- <span aria-hidden="true">
177
- { activeOperator in OPERATORS &&
178
- `${ OPERATORS[ activeOperator ].label } ` }
179
- { activeElement?.label }
180
- </span>
181
- )
182
- }
183
- >
184
- <DropdownMenuItemLabel>
185
- { __( 'Filter by' ) }
186
- </DropdownMenuItemLabel>
187
- </DropdownMenuItem>
188
- }
189
- >
190
- <WithSeparators>
191
- <DropdownMenuGroup>
192
- { filter.elements.map( ( element ) => {
193
- const isActive =
194
- activeElement?.value ===
195
- element.value;
196
- return (
197
- <DropdownMenuRadioItemCustom
198
- key={ element.value }
199
- name={ `view-table-${ filter.field }` }
200
- value={ element.value }
201
- checked={ isActive }
202
- onClick={ () => {
203
- onChangeView( {
204
- ...view,
205
- page: 1,
206
- filters: [
207
- ...otherFilters,
208
- {
209
- field: filter.field,
210
- operator:
211
- activeOperator,
212
- value: isActive
213
- ? undefined
214
- : element.value,
215
- },
216
- ],
217
- } );
218
- } }
219
- >
220
- <DropdownMenuItemLabel>
221
- { element.label }
222
- </DropdownMenuItemLabel>
223
- { !! element.description && (
224
- <DropdownMenuItemHelpText>
225
- { element.description }
226
- </DropdownMenuItemHelpText>
227
- ) }
228
- </DropdownMenuRadioItemCustom>
229
- );
230
- } ) }
231
- </DropdownMenuGroup>
232
- { filter.operators.length > 1 && (
233
- <DropdownMenu
234
- trigger={
235
- <DropdownMenuItem
236
- suffix={
237
- <span aria-hidden="true">
238
- {
239
- OPERATORS[
240
- activeOperator
241
- ]?.label
242
- }
243
- </span>
244
- }
245
- >
246
- <DropdownMenuItemLabel>
247
- { __( 'Conditions' ) }
248
- </DropdownMenuItemLabel>
249
- </DropdownMenuItem>
250
- }
251
- >
252
- { Object.entries( OPERATORS ).map(
253
- ( [
254
- operator,
255
- { label, key },
256
- ] ) => (
257
- <DropdownMenuRadioItem
258
- key={ key }
259
- name={ `view-table-${ filter.field }-conditions` }
260
- value={ operator }
261
- checked={
262
- activeOperator ===
263
- operator
264
- }
265
- onChange={ ( e ) =>
266
- onChangeView( {
267
- ...view,
268
- page: 1,
269
- filters: [
270
- ...otherFilters,
271
- {
272
- field: filter.field,
273
- operator:
274
- e.target
275
- .value,
276
- value: filterInView?.value,
277
- },
278
- ],
279
- } )
280
- }
281
- >
282
- <DropdownMenuItemLabel>
283
- { label }
284
- </DropdownMenuItemLabel>
285
- </DropdownMenuRadioItem>
286
- )
287
- ) }
288
- </DropdownMenu>
289
- ) }
290
- </WithSeparators>
291
- </DropdownMenu>
292
- </DropdownMenuGroup>
293
- ) }
294
184
  </WithSeparators>
295
185
  </DropdownMenu>
296
186
  );
297
187
  } );
298
188
 
299
- function WithSeparators( { children } ) {
300
- return Children.toArray( children )
301
- .filter( Boolean )
302
- .map( ( child, i ) => (
303
- <Fragment key={ i }>
304
- { i > 0 && <DropdownMenuSeparator /> }
305
- { child }
306
- </Fragment>
307
- ) );
308
- }
309
-
310
189
  function BulkSelectionCheckbox( { selection, onSelectionChange, data } ) {
311
190
  const areAllSelected = selection.length === data.length;
312
191
  return (
@@ -327,60 +206,6 @@ function BulkSelectionCheckbox( { selection, onSelectionChange, data } ) {
327
206
  );
328
207
  }
329
208
 
330
- function SingleSelectionCheckbox( {
331
- selection,
332
- onSelectionChange,
333
- item,
334
- data,
335
- getItemId,
336
- primaryField,
337
- } ) {
338
- const id = getItemId( item );
339
- const isSelected = selection.includes( id );
340
- let selectionLabel;
341
- if ( primaryField?.getValue && item ) {
342
- // eslint-disable-next-line @wordpress/valid-sprintf
343
- selectionLabel = sprintf(
344
- /* translators: %s: item title. */
345
- isSelected ? __( 'Deselect item: %s' ) : __( 'Select item: %s' ),
346
- primaryField.getValue( { item } )
347
- );
348
- } else {
349
- selectionLabel = isSelected
350
- ? __( 'Select a new item' )
351
- : __( 'Deselect item' );
352
- }
353
- return (
354
- <CheckboxControl
355
- className="dataviews-view-table-selection-checkbox"
356
- __nextHasNoMarginBottom
357
- checked={ isSelected }
358
- label={ selectionLabel }
359
- onChange={ () => {
360
- if ( ! isSelected ) {
361
- onSelectionChange(
362
- data.filter( ( _item ) => {
363
- const itemId = getItemId?.( _item );
364
- return (
365
- itemId === id || selection.includes( itemId )
366
- );
367
- } )
368
- );
369
- } else {
370
- onSelectionChange(
371
- data.filter( ( _item ) => {
372
- const itemId = getItemId?.( _item );
373
- return (
374
- itemId !== id && selection.includes( itemId )
375
- );
376
- } )
377
- );
378
- }
379
- } }
380
- />
381
- );
382
- }
383
-
384
209
  function ViewTable( {
385
210
  view,
386
211
  onChangeView,
@@ -392,6 +217,7 @@ function ViewTable( {
392
217
  deferredRendering,
393
218
  selection,
394
219
  onSelectionChange,
220
+ setOpenedFilter,
395
221
  } ) {
396
222
  const hasBulkActions = actions?.some( ( action ) => action.supportsBulk );
397
223
  const headerMenuRefs = useRef( new Map() );
@@ -502,6 +328,7 @@ function ViewTable( {
502
328
  view={ view }
503
329
  onChangeView={ onChangeView }
504
330
  onHide={ onHide }
331
+ setOpenedFilter={ setOpenedFilter }
505
332
  />
506
333
  </th>
507
334
  ) ) }