@wordpress/dataviews 0.2.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 (84) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/LICENSE.md +788 -0
  3. package/README.md +224 -0
  4. package/build/add-filter.js +90 -0
  5. package/build/add-filter.js.map +1 -0
  6. package/build/constants.js +55 -0
  7. package/build/constants.js.map +1 -0
  8. package/build/dataviews.js +93 -0
  9. package/build/dataviews.js.map +1 -0
  10. package/build/filter-summary.js +137 -0
  11. package/build/filter-summary.js.map +1 -0
  12. package/build/filters.js +75 -0
  13. package/build/filters.js.map +1 -0
  14. package/build/index.js +21 -0
  15. package/build/index.js.map +1 -0
  16. package/build/item-actions.js +185 -0
  17. package/build/item-actions.js.map +1 -0
  18. package/build/lock-unlock.js +18 -0
  19. package/build/lock-unlock.js.map +1 -0
  20. package/build/pagination.js +123 -0
  21. package/build/pagination.js.map +1 -0
  22. package/build/reset-filters.js +33 -0
  23. package/build/reset-filters.js.map +1 -0
  24. package/build/search.js +46 -0
  25. package/build/search.js.map +1 -0
  26. package/build/view-actions.js +223 -0
  27. package/build/view-actions.js.map +1 -0
  28. package/build/view-grid.js +80 -0
  29. package/build/view-grid.js.map +1 -0
  30. package/build/view-list.js +83 -0
  31. package/build/view-list.js.map +1 -0
  32. package/build/view-table.js +286 -0
  33. package/build/view-table.js.map +1 -0
  34. package/build-module/add-filter.js +83 -0
  35. package/build-module/add-filter.js.map +1 -0
  36. package/build-module/constants.js +41 -0
  37. package/build-module/constants.js.map +1 -0
  38. package/build-module/dataviews.js +85 -0
  39. package/build-module/dataviews.js.map +1 -0
  40. package/build-module/filter-summary.js +130 -0
  41. package/build-module/filter-summary.js.map +1 -0
  42. package/build-module/filters.js +67 -0
  43. package/build-module/filters.js.map +1 -0
  44. package/build-module/index.js +3 -0
  45. package/build-module/index.js.map +1 -0
  46. package/build-module/item-actions.js +178 -0
  47. package/build-module/item-actions.js.map +1 -0
  48. package/build-module/lock-unlock.js +9 -0
  49. package/build-module/lock-unlock.js.map +1 -0
  50. package/build-module/pagination.js +115 -0
  51. package/build-module/pagination.js.map +1 -0
  52. package/build-module/reset-filters.js +26 -0
  53. package/build-module/reset-filters.js.map +1 -0
  54. package/build-module/search.js +39 -0
  55. package/build-module/search.js.map +1 -0
  56. package/build-module/view-actions.js +216 -0
  57. package/build-module/view-actions.js.map +1 -0
  58. package/build-module/view-grid.js +72 -0
  59. package/build-module/view-grid.js.map +1 -0
  60. package/build-module/view-list.js +75 -0
  61. package/build-module/view-list.js.map +1 -0
  62. package/build-module/view-table.js +277 -0
  63. package/build-module/view-table.js.map +1 -0
  64. package/build-style/style-rtl.css +325 -0
  65. package/build-style/style.css +325 -0
  66. package/package.json +49 -0
  67. package/src/add-filter.js +106 -0
  68. package/src/constants.js +50 -0
  69. package/src/dataviews.js +99 -0
  70. package/src/filter-summary.js +221 -0
  71. package/src/filters.js +84 -0
  72. package/src/index.js +2 -0
  73. package/src/item-actions.js +211 -0
  74. package/src/lock-unlock.js +10 -0
  75. package/src/pagination.js +144 -0
  76. package/src/reset-filters.js +26 -0
  77. package/src/search.js +38 -0
  78. package/src/stories/fixtures.js +126 -0
  79. package/src/stories/index.story.js +137 -0
  80. package/src/style.scss +245 -0
  81. package/src/view-actions.js +298 -0
  82. package/src/view-grid.js +100 -0
  83. package/src/view-list.js +99 -0
  84. package/src/view-table.js +425 -0
@@ -0,0 +1,425 @@
1
+ /**
2
+ * WordPress dependencies
3
+ */
4
+ import { __ } from '@wordpress/i18n';
5
+ import { useAsyncList } from '@wordpress/compose';
6
+ import {
7
+ chevronDown,
8
+ chevronUp,
9
+ unseen,
10
+ check,
11
+ arrowUp,
12
+ arrowDown,
13
+ chevronRightSmall,
14
+ funnel,
15
+ } from '@wordpress/icons';
16
+ import {
17
+ Button,
18
+ Icon,
19
+ privateApis as componentsPrivateApis,
20
+ } from '@wordpress/components';
21
+ import { Children, Fragment } from '@wordpress/element';
22
+
23
+ /**
24
+ * Internal dependencies
25
+ */
26
+ import { unlock } from './lock-unlock';
27
+ import ItemActions from './item-actions';
28
+ import { ENUMERATION_TYPE, OPERATOR_IN, OPERATOR_NOT_IN } from './constants';
29
+
30
+ const {
31
+ DropdownMenuV2: DropdownMenu,
32
+ DropdownMenuGroupV2: DropdownMenuGroup,
33
+ DropdownMenuItemV2: DropdownMenuItem,
34
+ DropdownMenuSeparatorV2: DropdownMenuSeparator,
35
+ DropdownSubMenuV2: DropdownSubMenu,
36
+ DropdownSubMenuTriggerV2: DropdownSubMenuTrigger,
37
+ } = unlock( componentsPrivateApis );
38
+
39
+ const sortingItemsInfo = {
40
+ asc: { icon: arrowUp, label: __( 'Sort ascending' ) },
41
+ desc: { icon: arrowDown, label: __( 'Sort descending' ) },
42
+ };
43
+ const sortIcons = { asc: chevronUp, desc: chevronDown };
44
+
45
+ function HeaderMenu( { field, view, onChangeView } ) {
46
+ const isSortable = field.enableSorting !== false;
47
+ const isHidable = field.enableHiding !== false;
48
+ if ( ! isSortable && ! isHidable ) {
49
+ return field.header;
50
+ }
51
+ const isSorted = view.sort?.field === field.id;
52
+ let filter, filterInView;
53
+ const otherFilters = [];
54
+ if ( field.type === ENUMERATION_TYPE ) {
55
+ let columnOperators = field.filterBy?.operators;
56
+ if ( ! columnOperators || ! Array.isArray( columnOperators ) ) {
57
+ columnOperators = [ OPERATOR_IN, OPERATOR_NOT_IN ];
58
+ }
59
+ const operators = columnOperators.filter( ( operator ) =>
60
+ [ OPERATOR_IN, OPERATOR_NOT_IN ].includes( operator )
61
+ );
62
+ if ( operators.length >= 0 ) {
63
+ filter = {
64
+ field: field.id,
65
+ operators,
66
+ elements: field.elements || [],
67
+ };
68
+ filterInView = {
69
+ field: filter.field,
70
+ operator: filter.operators[ 0 ],
71
+ value: undefined,
72
+ };
73
+ }
74
+ }
75
+ const isFilterable = !! filter;
76
+
77
+ if ( isFilterable ) {
78
+ const columnFilters = view.filters;
79
+ columnFilters.forEach( ( columnFilter ) => {
80
+ if ( columnFilter.field === filter.field ) {
81
+ filterInView = {
82
+ ...columnFilter,
83
+ };
84
+ } else {
85
+ otherFilters.push( columnFilter );
86
+ }
87
+ } );
88
+ }
89
+ return (
90
+ <DropdownMenu
91
+ align="start"
92
+ trigger={
93
+ <Button
94
+ icon={ isSorted && sortIcons[ view.sort.direction ] }
95
+ iconPosition="right"
96
+ text={ field.header }
97
+ style={ { padding: 0 } }
98
+ size="compact"
99
+ />
100
+ }
101
+ >
102
+ <WithSeparators>
103
+ { isSortable && (
104
+ <DropdownMenuGroup>
105
+ { Object.entries( sortingItemsInfo ).map(
106
+ ( [ direction, info ] ) => {
107
+ const isActive =
108
+ isSorted &&
109
+ view.sort.direction === direction;
110
+ return (
111
+ <DropdownMenuItem
112
+ key={ direction }
113
+ role="menuitemradio"
114
+ aria-checked={ isActive }
115
+ prefix={ <Icon icon={ info.icon } /> }
116
+ suffix={
117
+ isActive && <Icon icon={ check } />
118
+ }
119
+ onSelect={ ( event ) => {
120
+ event.preventDefault();
121
+ onChangeView( {
122
+ ...view,
123
+ sort: {
124
+ field: field.id,
125
+ direction,
126
+ },
127
+ } );
128
+ } }
129
+ >
130
+ { info.label }
131
+ </DropdownMenuItem>
132
+ );
133
+ }
134
+ ) }
135
+ </DropdownMenuGroup>
136
+ ) }
137
+ { isHidable && (
138
+ <DropdownMenuItem
139
+ role="menuitemradio"
140
+ aria-checked={ false }
141
+ prefix={ <Icon icon={ unseen } /> }
142
+ onSelect={ ( event ) => {
143
+ event.preventDefault();
144
+ onChangeView( {
145
+ ...view,
146
+ hiddenFields: view.hiddenFields.concat(
147
+ field.id
148
+ ),
149
+ } );
150
+ } }
151
+ >
152
+ { __( 'Hide' ) }
153
+ </DropdownMenuItem>
154
+ ) }
155
+ { isFilterable && (
156
+ <DropdownMenuGroup>
157
+ <DropdownSubMenu
158
+ key={ filter.field }
159
+ trigger={
160
+ <DropdownSubMenuTrigger
161
+ prefix={ <Icon icon={ funnel } /> }
162
+ suffix={
163
+ <Icon icon={ chevronRightSmall } />
164
+ }
165
+ >
166
+ { __( 'Filter by' ) }
167
+ </DropdownSubMenuTrigger>
168
+ }
169
+ >
170
+ <WithSeparators>
171
+ <DropdownMenuGroup>
172
+ { filter.elements.map( ( element ) => {
173
+ let isActive = false;
174
+ if ( filterInView ) {
175
+ // Intentionally use loose comparison, so it does type conversion.
176
+ // This covers the case where a top-level filter for the same field converts a number into a string.
177
+ /* eslint-disable eqeqeq */
178
+ isActive =
179
+ element.value ==
180
+ filterInView.value;
181
+ /* eslint-enable eqeqeq */
182
+ }
183
+
184
+ return (
185
+ <DropdownMenuItem
186
+ key={ element.value }
187
+ role="menuitemradio"
188
+ aria-checked={ isActive }
189
+ prefix={
190
+ isActive && (
191
+ <Icon icon={ check } />
192
+ )
193
+ }
194
+ onSelect={ () => {
195
+ onChangeView( {
196
+ ...view,
197
+ filters: [
198
+ ...otherFilters,
199
+ {
200
+ field: filter.field,
201
+ operator:
202
+ filterInView?.operator,
203
+ value: isActive
204
+ ? undefined
205
+ : element.value,
206
+ },
207
+ ],
208
+ } );
209
+ } }
210
+ >
211
+ { element.label }
212
+ </DropdownMenuItem>
213
+ );
214
+ } ) }
215
+ </DropdownMenuGroup>
216
+ { filter.operators.length > 1 && (
217
+ <DropdownSubMenu
218
+ trigger={
219
+ <DropdownSubMenuTrigger
220
+ suffix={
221
+ <>
222
+ { filterInView.operator ===
223
+ OPERATOR_IN
224
+ ? __( 'Is' )
225
+ : __( 'Is not' ) }
226
+ <Icon
227
+ icon={
228
+ chevronRightSmall
229
+ }
230
+ />{ ' ' }
231
+ </>
232
+ }
233
+ >
234
+ { __( 'Conditions' ) }
235
+ </DropdownSubMenuTrigger>
236
+ }
237
+ >
238
+ <DropdownMenuItem
239
+ key="in-filter"
240
+ role="menuitemradio"
241
+ aria-checked={
242
+ filterInView?.operator ===
243
+ OPERATOR_IN
244
+ }
245
+ prefix={
246
+ filterInView?.operator ===
247
+ OPERATOR_IN && (
248
+ <Icon icon={ check } />
249
+ )
250
+ }
251
+ onSelect={ () =>
252
+ onChangeView( {
253
+ ...view,
254
+ filters: [
255
+ ...otherFilters,
256
+ {
257
+ field: filter.field,
258
+ operator:
259
+ OPERATOR_IN,
260
+ value: filterInView?.value,
261
+ },
262
+ ],
263
+ } )
264
+ }
265
+ >
266
+ { __( 'Is' ) }
267
+ </DropdownMenuItem>
268
+ <DropdownMenuItem
269
+ key="not-in-filter"
270
+ role="menuitemradio"
271
+ aria-checked={
272
+ filterInView?.operator ===
273
+ OPERATOR_NOT_IN
274
+ }
275
+ prefix={
276
+ filterInView?.operator ===
277
+ OPERATOR_NOT_IN && (
278
+ <Icon icon={ check } />
279
+ )
280
+ }
281
+ onSelect={ () =>
282
+ onChangeView( {
283
+ ...view,
284
+ filters: [
285
+ ...otherFilters,
286
+ {
287
+ field: filter.field,
288
+ operator:
289
+ OPERATOR_NOT_IN,
290
+ value: filterInView?.value,
291
+ },
292
+ ],
293
+ } )
294
+ }
295
+ >
296
+ { __( 'Is not' ) }
297
+ </DropdownMenuItem>
298
+ </DropdownSubMenu>
299
+ ) }
300
+ </WithSeparators>
301
+ </DropdownSubMenu>
302
+ </DropdownMenuGroup>
303
+ ) }
304
+ </WithSeparators>
305
+ </DropdownMenu>
306
+ );
307
+ }
308
+
309
+ function WithSeparators( { children } ) {
310
+ return Children.toArray( children )
311
+ .filter( Boolean )
312
+ .map( ( child, i ) => (
313
+ <Fragment key={ i }>
314
+ { i > 0 && <DropdownMenuSeparator /> }
315
+ { child }
316
+ </Fragment>
317
+ ) );
318
+ }
319
+
320
+ function ViewTable( {
321
+ view,
322
+ onChangeView,
323
+ fields,
324
+ actions,
325
+ data,
326
+ getItemId,
327
+ isLoading = false,
328
+ deferredRendering,
329
+ } ) {
330
+ const visibleFields = fields.filter(
331
+ ( field ) =>
332
+ ! view.hiddenFields.includes( field.id ) &&
333
+ ! [ view.layout.mediaField, view.layout.primaryField ].includes(
334
+ field.id
335
+ )
336
+ );
337
+ const shownData = useAsyncList( data );
338
+ const usedData = deferredRendering ? shownData : data;
339
+ const hasData = !! usedData?.length;
340
+ if ( isLoading ) {
341
+ // TODO:Add spinner or progress bar..
342
+ return (
343
+ <div className="dataviews-loading">
344
+ <h3>{ __( 'Loading' ) }</h3>
345
+ </div>
346
+ );
347
+ }
348
+ const sortValues = { asc: 'ascending', desc: 'descending' };
349
+ return (
350
+ <div className="dataviews-table-view-wrapper">
351
+ { hasData && (
352
+ <table className="dataviews-table-view">
353
+ <thead>
354
+ <tr>
355
+ { visibleFields.map( ( field ) => (
356
+ <th
357
+ key={ field.id }
358
+ style={ {
359
+ width: field.width || undefined,
360
+ minWidth: field.minWidth || undefined,
361
+ maxWidth: field.maxWidth || undefined,
362
+ } }
363
+ data-field-id={ field.id }
364
+ aria-sort={
365
+ view.sort?.field === field.id &&
366
+ sortValues[ view.sort.direction ]
367
+ }
368
+ scope="col"
369
+ >
370
+ <HeaderMenu
371
+ field={ field }
372
+ view={ view }
373
+ onChangeView={ onChangeView }
374
+ />
375
+ </th>
376
+ ) ) }
377
+ { !! actions?.length && (
378
+ <th data-field-id="actions">
379
+ { __( 'Actions' ) }
380
+ </th>
381
+ ) }
382
+ </tr>
383
+ </thead>
384
+ <tbody>
385
+ { usedData.map( ( item, index ) => (
386
+ <tr key={ getItemId?.( item ) || index }>
387
+ { visibleFields.map( ( field ) => (
388
+ <td
389
+ key={ field.id }
390
+ style={ {
391
+ width: field.width || undefined,
392
+ minWidth:
393
+ field.minWidth || undefined,
394
+ maxWidth:
395
+ field.maxWidth || undefined,
396
+ } }
397
+ >
398
+ { field.render( {
399
+ item,
400
+ } ) }
401
+ </td>
402
+ ) ) }
403
+ { !! actions?.length && (
404
+ <td>
405
+ <ItemActions
406
+ item={ item }
407
+ actions={ actions }
408
+ />
409
+ </td>
410
+ ) }
411
+ </tr>
412
+ ) ) }
413
+ </tbody>
414
+ </table>
415
+ ) }
416
+ { ! hasData && (
417
+ <div className="dataviews-no-results">
418
+ <p>{ __( 'No results' ) }</p>
419
+ </div>
420
+ ) }
421
+ </div>
422
+ );
423
+ }
424
+
425
+ export default ViewTable;