@wordpress/dataviews 0.2.0 → 0.3.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 (87) hide show
  1. package/CHANGELOG.md +2 -0
  2. package/LICENSE.md +1 -1
  3. package/README.md +30 -6
  4. package/build/add-filter.js +109 -49
  5. package/build/add-filter.js.map +1 -1
  6. package/build/constants.js +24 -2
  7. package/build/constants.js.map +1 -1
  8. package/build/dataviews.js +12 -9
  9. package/build/dataviews.js.map +1 -1
  10. package/build/dropdown-menu-helper.js +72 -0
  11. package/build/dropdown-menu-helper.js.map +1 -0
  12. package/build/filter-summary.js +43 -54
  13. package/build/filter-summary.js.map +1 -1
  14. package/build/filters.js +27 -17
  15. package/build/filters.js.map +1 -1
  16. package/build/index.js +13 -0
  17. package/build/index.js.map +1 -1
  18. package/build/item-actions.js +12 -12
  19. package/build/item-actions.js.map +1 -1
  20. package/build/pagination.js +31 -65
  21. package/build/pagination.js.map +1 -1
  22. package/build/reset-filters.js +8 -8
  23. package/build/reset-filters.js.map +1 -1
  24. package/build/search.js +8 -6
  25. package/build/search.js.map +1 -1
  26. package/build/utils.js +71 -0
  27. package/build/utils.js.map +1 -0
  28. package/build/view-actions.js +72 -95
  29. package/build/view-actions.js.map +1 -1
  30. package/build/view-grid.js +4 -6
  31. package/build/view-grid.js.map +1 -1
  32. package/build/view-list.js +26 -13
  33. package/build/view-list.js.map +1 -1
  34. package/build/view-table.js +153 -154
  35. package/build/view-table.js.map +1 -1
  36. package/build-module/add-filter.js +113 -53
  37. package/build-module/add-filter.js.map +1 -1
  38. package/build-module/constants.js +20 -0
  39. package/build-module/constants.js.map +1 -1
  40. package/build-module/dataviews.js +13 -10
  41. package/build-module/dataviews.js.map +1 -1
  42. package/build-module/dropdown-menu-helper.js +64 -0
  43. package/build-module/dropdown-menu-helper.js.map +1 -0
  44. package/build-module/filter-summary.js +45 -56
  45. package/build-module/filter-summary.js.map +1 -1
  46. package/build-module/filters.js +26 -17
  47. package/build-module/filters.js.map +1 -1
  48. package/build-module/index.js +1 -0
  49. package/build-module/index.js.map +1 -1
  50. package/build-module/item-actions.js +12 -12
  51. package/build-module/item-actions.js.map +1 -1
  52. package/build-module/pagination.js +35 -69
  53. package/build-module/pagination.js.map +1 -1
  54. package/build-module/reset-filters.js +6 -6
  55. package/build-module/reset-filters.js.map +1 -1
  56. package/build-module/search.js +7 -6
  57. package/build-module/search.js.map +1 -1
  58. package/build-module/utils.js +63 -0
  59. package/build-module/utils.js.map +1 -0
  60. package/build-module/view-actions.js +73 -97
  61. package/build-module/view-actions.js.map +1 -1
  62. package/build-module/view-grid.js +4 -6
  63. package/build-module/view-grid.js.map +1 -1
  64. package/build-module/view-list.js +27 -14
  65. package/build-module/view-list.js.map +1 -1
  66. package/build-module/view-table.js +156 -157
  67. package/build-module/view-table.js.map +1 -1
  68. package/build-style/style-rtl.css +180 -70
  69. package/build-style/style.css +180 -70
  70. package/package.json +11 -10
  71. package/src/add-filter.js +227 -68
  72. package/src/constants.js +16 -0
  73. package/src/dataviews.js +19 -12
  74. package/src/dropdown-menu-helper.js +61 -0
  75. package/src/filter-summary.js +70 -103
  76. package/src/filters.js +41 -24
  77. package/src/index.js +1 -0
  78. package/src/item-actions.js +30 -25
  79. package/src/pagination.js +75 -123
  80. package/src/reset-filters.js +5 -5
  81. package/src/search.js +8 -6
  82. package/src/style.scss +182 -48
  83. package/src/utils.js +51 -0
  84. package/src/view-actions.js +113 -114
  85. package/src/view-grid.js +4 -4
  86. package/src/view-list.js +42 -28
  87. package/src/view-table.js +280 -238
package/src/add-filter.js CHANGED
@@ -4,103 +4,262 @@
4
4
  import {
5
5
  privateApis as componentsPrivateApis,
6
6
  Button,
7
- Icon,
8
7
  } from '@wordpress/components';
9
- import { chevronRightSmall, plus } from '@wordpress/icons';
10
- import { __ } from '@wordpress/i18n';
8
+ import { funnel } from '@wordpress/icons';
9
+ import { __, sprintf } from '@wordpress/i18n';
10
+ import { Children, Fragment } from '@wordpress/element';
11
11
 
12
12
  /**
13
13
  * Internal dependencies
14
14
  */
15
15
  import { unlock } from './lock-unlock';
16
- import { ENUMERATION_TYPE, OPERATOR_IN } from './constants';
16
+ import { LAYOUT_LIST, OPERATORS } from './constants';
17
+ import { DropdownMenuRadioItemCustom } from './dropdown-menu-helper';
17
18
 
18
19
  const {
19
20
  DropdownMenuV2: DropdownMenu,
20
- DropdownSubMenuV2: DropdownSubMenu,
21
- DropdownSubMenuTriggerV2: DropdownSubMenuTrigger,
21
+ DropdownMenuGroupV2: DropdownMenuGroup,
22
22
  DropdownMenuItemV2: DropdownMenuItem,
23
+ DropdownMenuRadioItemV2: DropdownMenuRadioItem,
24
+ DropdownMenuSeparatorV2: DropdownMenuSeparator,
25
+ DropdownMenuItemLabelV2: DropdownMenuItemLabel,
26
+ DropdownMenuItemHelpTextV2: DropdownMenuItemHelpText,
23
27
  } = unlock( componentsPrivateApis );
24
28
 
25
- export default function AddFilter( { fields, view, onChangeView } ) {
26
- const filters = [];
27
- fields.forEach( ( field ) => {
28
- if ( ! field.type ) {
29
- return;
30
- }
31
-
32
- switch ( field.type ) {
33
- case ENUMERATION_TYPE:
34
- filters.push( {
35
- field: field.id,
36
- name: field.header,
37
- elements: field.elements || [],
38
- isVisible: view.filters.some(
39
- ( f ) => f.field === field.id
40
- ),
41
- } );
42
- }
43
- } );
29
+ function WithSeparators( { children } ) {
30
+ return Children.toArray( children )
31
+ .filter( Boolean )
32
+ .map( ( child, i ) => (
33
+ <Fragment key={ i }>
34
+ { i > 0 && <DropdownMenuSeparator /> }
35
+ { child }
36
+ </Fragment>
37
+ ) );
38
+ }
44
39
 
40
+ export default function AddFilter( { filters, view, onChangeView } ) {
45
41
  if ( filters.length === 0 ) {
46
42
  return null;
47
43
  }
48
44
 
45
+ const filterCount = view.filters.reduce( ( acc, filter ) => {
46
+ if ( filter.value !== undefined ) {
47
+ return acc + 1;
48
+ }
49
+ return acc;
50
+ }, 0 );
51
+
49
52
  return (
50
53
  <DropdownMenu
51
- label={ __( 'Add filter' ) }
52
54
  trigger={
53
55
  <Button
54
- disabled={ filters.length === view.filters?.length }
55
56
  __experimentalIsFocusable
56
- variant="tertiary"
57
+ label={ __( 'Filters' ) }
57
58
  size="compact"
59
+ icon={ funnel }
60
+ className="dataviews-filters-button"
58
61
  >
59
- <Icon icon={ plus } style={ { flexShrink: 0 } } />
60
- { __( 'Add filter' ) }
62
+ { view.type === LAYOUT_LIST && filterCount > 0 ? (
63
+ <span className="dataviews-filters-count">
64
+ { filterCount }
65
+ </span>
66
+ ) : null }
61
67
  </Button>
62
68
  }
69
+ style={ {
70
+ minWidth: '230px',
71
+ } }
63
72
  >
64
- { filters.map( ( filter ) => {
65
- if ( filter.isVisible ) {
66
- return null;
67
- }
68
-
69
- return (
70
- <DropdownSubMenu
71
- key={ filter.field }
72
- trigger={
73
- <DropdownSubMenuTrigger
74
- suffix={ <Icon icon={ chevronRightSmall } /> }
75
- >
76
- { filter.name }
77
- </DropdownSubMenuTrigger>
78
- }
79
- >
80
- { filter.elements.map( ( element ) => (
81
- <DropdownMenuItem
82
- key={ element.value }
83
- onSelect={ () => {
84
- onChangeView( ( currentView ) => ( {
85
- ...currentView,
86
- page: 1,
87
- filters: [
88
- ...currentView.filters,
89
- {
90
- field: filter.field,
91
- operator: OPERATOR_IN,
92
- value: element.value,
93
- },
94
- ],
95
- } ) );
73
+ <WithSeparators>
74
+ <DropdownMenuGroup>
75
+ { filters.map( ( filter ) => {
76
+ const filterInView = view.filters.find(
77
+ ( f ) => f.field === filter.field
78
+ );
79
+ const otherFilters = view.filters.filter(
80
+ ( f ) => f.field !== filter.field
81
+ );
82
+ const activeElement = filter.elements.find(
83
+ ( element ) => element.value === filterInView?.value
84
+ );
85
+ const activeOperator =
86
+ filterInView?.operator || filter.operators[ 0 ];
87
+ return (
88
+ <DropdownMenu
89
+ key={ filter.field }
90
+ trigger={
91
+ <DropdownMenuItem
92
+ suffix={
93
+ activeElement && (
94
+ <span aria-hidden="true">
95
+ { activeOperator in
96
+ OPERATORS &&
97
+ `${ OPERATORS[ activeOperator ].label } ` }
98
+ { activeElement.label }
99
+ </span>
100
+ )
101
+ }
102
+ >
103
+ <DropdownMenuItemLabel>
104
+ { filter.name }
105
+ </DropdownMenuItemLabel>
106
+ </DropdownMenuItem>
107
+ }
108
+ style={ {
109
+ minWidth: '200px',
96
110
  } }
97
111
  >
98
- { element.label }
99
- </DropdownMenuItem>
100
- ) ) }
101
- </DropdownSubMenu>
102
- );
103
- } ) }
112
+ <WithSeparators>
113
+ <DropdownMenuGroup>
114
+ { filter.elements.map( ( element ) => {
115
+ const isActive =
116
+ activeElement?.value ===
117
+ element.value;
118
+ return (
119
+ <DropdownMenuRadioItemCustom
120
+ key={ element.value }
121
+ name={ `add-filter-${ filter.field }` }
122
+ value={ element.value }
123
+ checked={ isActive }
124
+ onChange={ ( e ) => {
125
+ onChangeView( {
126
+ ...view,
127
+ page: 1,
128
+ filters: [
129
+ ...otherFilters,
130
+ {
131
+ field: filter.field,
132
+ operator:
133
+ activeOperator,
134
+ value: isActive
135
+ ? undefined
136
+ : e
137
+ .target
138
+ .value,
139
+ },
140
+ ],
141
+ } );
142
+ } }
143
+ >
144
+ <DropdownMenuItemLabel>
145
+ { element.label }
146
+ </DropdownMenuItemLabel>
147
+ { !! element.description && (
148
+ <DropdownMenuItemHelpText>
149
+ {
150
+ element.description
151
+ }
152
+ </DropdownMenuItemHelpText>
153
+ ) }
154
+ </DropdownMenuRadioItemCustom>
155
+ );
156
+ } ) }
157
+ </DropdownMenuGroup>
158
+ { filter.operators.length > 1 && (
159
+ <DropdownMenu
160
+ trigger={
161
+ <DropdownMenuItem
162
+ suffix={
163
+ <span aria-hidden="true">
164
+ {
165
+ OPERATORS[
166
+ activeOperator
167
+ ]?.label
168
+ }
169
+ </span>
170
+ }
171
+ >
172
+ <DropdownMenuItemLabel>
173
+ { __( 'Conditions' ) }
174
+ </DropdownMenuItemLabel>
175
+ </DropdownMenuItem>
176
+ }
177
+ >
178
+ { Object.entries( OPERATORS ).map(
179
+ ( [
180
+ operator,
181
+ { label, key },
182
+ ] ) => (
183
+ <DropdownMenuRadioItem
184
+ key={ key }
185
+ name={ `add-filter-${ filter.field }-conditions` }
186
+ value={ operator }
187
+ checked={
188
+ activeOperator ===
189
+ operator
190
+ }
191
+ onChange={ ( e ) => {
192
+ onChangeView( {
193
+ ...view,
194
+ page: 1,
195
+ filters: [
196
+ ...otherFilters,
197
+ {
198
+ field: filter.field,
199
+ operator:
200
+ e
201
+ .target
202
+ .value,
203
+ value: filterInView?.value,
204
+ },
205
+ ],
206
+ } );
207
+ } }
208
+ >
209
+ <DropdownMenuItemLabel>
210
+ { label }
211
+ </DropdownMenuItemLabel>
212
+ </DropdownMenuRadioItem>
213
+ )
214
+ ) }
215
+ </DropdownMenu>
216
+ ) }
217
+ <DropdownMenuItem
218
+ key={ 'reset-filter-' + filter.name }
219
+ disabled={ ! activeElement }
220
+ hideOnClick={ false }
221
+ onClick={ () => {
222
+ onChangeView( {
223
+ ...view,
224
+ page: 1,
225
+ filters: view.filters.filter(
226
+ ( f ) =>
227
+ f.field !== filter.field
228
+ ),
229
+ } );
230
+ } }
231
+ >
232
+ <DropdownMenuItemLabel>
233
+ { sprintf(
234
+ /* translators: 1: Filter name. e.g.: "Reset Author". */
235
+ __( 'Reset %1$s' ),
236
+ filter.name.toLowerCase()
237
+ ) }
238
+ </DropdownMenuItemLabel>
239
+ </DropdownMenuItem>
240
+ </WithSeparators>
241
+ </DropdownMenu>
242
+ );
243
+ } ) }
244
+ </DropdownMenuGroup>
245
+ <DropdownMenuItem
246
+ disabled={
247
+ view.search === '' && view.filters?.length === 0
248
+ }
249
+ hideOnClick={ false }
250
+ onClick={ () => {
251
+ onChangeView( {
252
+ ...view,
253
+ page: 1,
254
+ filters: [],
255
+ } );
256
+ } }
257
+ >
258
+ <DropdownMenuItemLabel>
259
+ { __( 'Reset filters' ) }
260
+ </DropdownMenuItemLabel>
261
+ </DropdownMenuItem>
262
+ </WithSeparators>
104
263
  </DropdownMenu>
105
264
  );
106
265
  }
package/src/constants.js CHANGED
@@ -22,6 +22,22 @@ export const ENUMERATION_TYPE = 'enumeration';
22
22
  // Filter operators.
23
23
  export const OPERATOR_IN = 'in';
24
24
  export const OPERATOR_NOT_IN = 'notIn';
25
+ export const OPERATORS = {
26
+ [ OPERATOR_IN ]: {
27
+ key: 'in-filter',
28
+ label: __( 'Is' ),
29
+ },
30
+ [ OPERATOR_NOT_IN ]: {
31
+ key: 'not-in-filter',
32
+ label: __( 'Is not' ),
33
+ },
34
+ };
35
+
36
+ // Sorting
37
+ export const SORTING_DIRECTIONS = {
38
+ asc: { label: __( 'Sort ascending' ) },
39
+ desc: { label: __( 'Sort descending' ) },
40
+ };
25
41
 
26
42
  // View layouts.
27
43
  export const LAYOUT_TABLE = 'table';
package/src/dataviews.js CHANGED
@@ -5,7 +5,7 @@ import {
5
5
  __experimentalVStack as VStack,
6
6
  __experimentalHStack as HStack,
7
7
  } from '@wordpress/components';
8
- import { useMemo, useState } from '@wordpress/element';
8
+ import { useMemo, useState, useCallback } from '@wordpress/element';
9
9
 
10
10
  /**
11
11
  * Internal dependencies
@@ -16,6 +16,9 @@ import Filters from './filters';
16
16
  import Search from './search';
17
17
  import { VIEW_LAYOUTS } from './constants';
18
18
 
19
+ const defaultGetItemId = ( item ) => item.id;
20
+ const defaultOnSelectionChange = () => {};
21
+
19
22
  export default function DataViews( {
20
23
  view,
21
24
  onChangeView,
@@ -24,19 +27,23 @@ export default function DataViews( {
24
27
  searchLabel = undefined,
25
28
  actions,
26
29
  data,
27
- getItemId,
30
+ getItemId = defaultGetItemId,
28
31
  isLoading = false,
29
32
  paginationInfo,
30
33
  supportedLayouts,
31
- onSelectionChange,
32
- deferredRendering,
34
+ onSelectionChange = defaultOnSelectionChange,
35
+ onDetailsChange = null,
36
+ deferredRendering = false,
33
37
  } ) {
34
38
  const [ selection, setSelection ] = useState( [] );
35
39
 
36
- const onSetSelection = ( items ) => {
37
- setSelection( items.map( ( item ) => item.id ) );
38
- onSelectionChange( items );
39
- };
40
+ const onSetSelection = useCallback(
41
+ ( items ) => {
42
+ setSelection( items.map( ( item ) => item.id ) );
43
+ onSelectionChange( items );
44
+ },
45
+ [ setSelection, onSelectionChange ]
46
+ );
40
47
 
41
48
  const ViewComponent = VIEW_LAYOUTS.find(
42
49
  ( v ) => v.type === view.type
@@ -52,7 +59,7 @@ export default function DataViews( {
52
59
  <VStack spacing={ 0 } justify="flex-start">
53
60
  <HStack
54
61
  alignment="flex-start"
55
- className="dataviews__filters-view-actions"
62
+ className="dataviews-filters__view-actions"
56
63
  >
57
64
  <HStack justify="start" wrap>
58
65
  { search && (
@@ -63,13 +70,13 @@ export default function DataViews( {
63
70
  />
64
71
  ) }
65
72
  <Filters
66
- fields={ fields }
73
+ fields={ _fields }
67
74
  view={ view }
68
75
  onChangeView={ onChangeView }
69
76
  />
70
77
  </HStack>
71
78
  <ViewActions
72
- fields={ fields }
79
+ fields={ _fields }
73
80
  view={ view }
74
81
  onChangeView={ onChangeView }
75
82
  supportedLayouts={ supportedLayouts }
@@ -79,12 +86,12 @@ export default function DataViews( {
79
86
  fields={ _fields }
80
87
  view={ view }
81
88
  onChangeView={ onChangeView }
82
- paginationInfo={ paginationInfo }
83
89
  actions={ actions }
84
90
  data={ data }
85
91
  getItemId={ getItemId }
86
92
  isLoading={ isLoading }
87
93
  onSelectionChange={ onSetSelection }
94
+ onDetailsChange={ onDetailsChange }
88
95
  selection={ selection }
89
96
  deferredRendering={ deferredRendering }
90
97
  />
@@ -0,0 +1,61 @@
1
+ /**
2
+ * WordPress dependencies
3
+ */
4
+ import {
5
+ Icon,
6
+ privateApis as componentsPrivateApis,
7
+ } from '@wordpress/components';
8
+ import { forwardRef } from '@wordpress/element';
9
+ import { SVG, Circle } from '@wordpress/primitives';
10
+
11
+ /**
12
+ * Internal dependencies
13
+ */
14
+ import { unlock } from './lock-unlock';
15
+
16
+ const { DropdownMenuItemV2: DropdownMenuItem } = unlock(
17
+ componentsPrivateApis
18
+ );
19
+
20
+ const radioCheck = (
21
+ <SVG xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
22
+ <Circle cx={ 12 } cy={ 12 } r={ 3 }></Circle>
23
+ </SVG>
24
+ );
25
+
26
+ /**
27
+ * A custom implementation of a radio menu item using the standard menu item
28
+ * component, which allows deselecting selected values.
29
+ */
30
+ export const DropdownMenuRadioItemCustom = forwardRef(
31
+ function DropdownMenuRadioItemCustom(
32
+ { checked, name, value, hideOnClick, onChange, onClick, ...props },
33
+ ref
34
+ ) {
35
+ const onClickHandler = ( e ) => {
36
+ onClick?.( e );
37
+ onChange?.( { ...e, target: { ...e.target, value } } );
38
+ };
39
+ return (
40
+ <DropdownMenuItem
41
+ ref={ ref }
42
+ role="menuitemradio"
43
+ name={ name }
44
+ aria-checked={ checked }
45
+ hideOnClick={ !! hideOnClick }
46
+ prefix={
47
+ checked ? (
48
+ <Icon icon={ radioCheck } />
49
+ ) : (
50
+ <span
51
+ className="dataviews-filters__custom-menu-radio-item-prefix"
52
+ aria-hidden="true"
53
+ ></span>
54
+ )
55
+ }
56
+ onClick={ onClickHandler }
57
+ { ...props }
58
+ />
59
+ );
60
+ }
61
+ );