@wordpress/dataviews 5.0.1-next.719a03cbe.0 → 6.0.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 (254) hide show
  1. package/CHANGELOG.md +34 -4
  2. package/README.md +55 -26
  3. package/build/components/dataviews/index.js +13 -4
  4. package/build/components/dataviews/index.js.map +1 -1
  5. package/build/components/dataviews-context/index.js +3 -1
  6. package/build/components/dataviews-context/index.js.map +1 -1
  7. package/build/components/dataviews-filters/filter.js +15 -8
  8. package/build/components/dataviews-filters/filter.js.map +1 -1
  9. package/build/components/dataviews-filters/index.js +16 -5
  10. package/build/components/dataviews-filters/index.js.map +1 -1
  11. package/build/components/dataviews-filters/input-widget.js +7 -1
  12. package/build/components/dataviews-filters/input-widget.js.map +1 -1
  13. package/build/components/dataviews-filters/reset-filters.js +2 -2
  14. package/build/components/dataviews-filters/reset-filters.js.map +1 -1
  15. package/build/components/dataviews-layout/index.js +5 -2
  16. package/build/components/dataviews-layout/index.js.map +1 -1
  17. package/build/components/dataviews-view-config/index.js +4 -3
  18. package/build/components/dataviews-view-config/index.js.map +1 -1
  19. package/build/dataform-controls/boolean.js +15 -1
  20. package/build/dataform-controls/boolean.js.map +1 -1
  21. package/build/dataform-controls/date.js +385 -0
  22. package/build/dataform-controls/date.js.map +1 -0
  23. package/build/dataform-controls/datetime.js +5 -84
  24. package/build/dataform-controls/datetime.js.map +1 -1
  25. package/build/dataform-controls/email.js +15 -1
  26. package/build/dataform-controls/email.js.map +1 -1
  27. package/build/dataform-controls/index.js +2 -0
  28. package/build/dataform-controls/index.js.map +1 -1
  29. package/build/dataform-controls/integer.js +23 -4
  30. package/build/dataform-controls/integer.js.map +1 -1
  31. package/build/dataform-controls/relative-date-control.js +109 -0
  32. package/build/dataform-controls/relative-date-control.js.map +1 -0
  33. package/build/dataform-controls/select.js +12 -5
  34. package/build/dataform-controls/select.js.map +1 -1
  35. package/build/dataform-controls/text.js +15 -1
  36. package/build/dataform-controls/text.js.map +1 -1
  37. package/build/dataviews-layouts/grid/index.js +91 -18
  38. package/build/dataviews-layouts/grid/index.js.map +1 -1
  39. package/build/dataviews-layouts/grid/preview-size-picker.js +39 -85
  40. package/build/dataviews-layouts/grid/preview-size-picker.js.map +1 -1
  41. package/build/dataviews-layouts/list/index.js +7 -3
  42. package/build/dataviews-layouts/list/index.js.map +1 -1
  43. package/build/dataviews-layouts/table/column-primary.js +18 -3
  44. package/build/dataviews-layouts/table/column-primary.js.map +1 -1
  45. package/build/dataviews-layouts/table/index.js +57 -5
  46. package/build/dataviews-layouts/table/index.js.map +1 -1
  47. package/build/field-types/array.js +27 -18
  48. package/build/field-types/array.js.map +1 -1
  49. package/build/field-types/boolean.js +11 -7
  50. package/build/field-types/boolean.js.map +1 -1
  51. package/build/field-types/date.js +66 -0
  52. package/build/field-types/date.js.map +1 -0
  53. package/build/field-types/datetime.js +19 -10
  54. package/build/field-types/datetime.js.map +1 -1
  55. package/build/field-types/email.js +22 -18
  56. package/build/field-types/email.js.map +1 -1
  57. package/build/field-types/index.js +20 -6
  58. package/build/field-types/index.js.map +1 -1
  59. package/build/field-types/integer.js +22 -17
  60. package/build/field-types/integer.js.map +1 -1
  61. package/build/field-types/media.js +19 -10
  62. package/build/field-types/media.js.map +1 -1
  63. package/build/field-types/text.js +19 -10
  64. package/build/field-types/text.js.map +1 -1
  65. package/build/filter-and-sort-data-view.js +28 -14
  66. package/build/filter-and-sort-data-view.js.map +1 -1
  67. package/build/normalize-fields.js +4 -5
  68. package/build/normalize-fields.js.map +1 -1
  69. package/build/types.js.map +1 -1
  70. package/build/validation.js +15 -2
  71. package/build/validation.js.map +1 -1
  72. package/build-module/components/dataviews/index.js +15 -6
  73. package/build-module/components/dataviews/index.js.map +1 -1
  74. package/build-module/components/dataviews-context/index.js +3 -1
  75. package/build-module/components/dataviews-context/index.js.map +1 -1
  76. package/build-module/components/dataviews-filters/filter.js +15 -8
  77. package/build-module/components/dataviews-filters/filter.js.map +1 -1
  78. package/build-module/components/dataviews-filters/index.js +16 -5
  79. package/build-module/components/dataviews-filters/index.js.map +1 -1
  80. package/build-module/components/dataviews-filters/input-widget.js +7 -1
  81. package/build-module/components/dataviews-filters/input-widget.js.map +1 -1
  82. package/build-module/components/dataviews-filters/reset-filters.js +2 -2
  83. package/build-module/components/dataviews-filters/reset-filters.js.map +1 -1
  84. package/build-module/components/dataviews-layout/index.js +5 -2
  85. package/build-module/components/dataviews-layout/index.js.map +1 -1
  86. package/build-module/components/dataviews-view-config/index.js +4 -3
  87. package/build-module/components/dataviews-view-config/index.js.map +1 -1
  88. package/build-module/dataform-controls/boolean.js +17 -2
  89. package/build-module/dataform-controls/boolean.js.map +1 -1
  90. package/build-module/dataform-controls/date.js +376 -0
  91. package/build-module/dataform-controls/date.js.map +1 -0
  92. package/build-module/dataform-controls/datetime.js +3 -84
  93. package/build-module/dataform-controls/datetime.js.map +1 -1
  94. package/build-module/dataform-controls/email.js +17 -2
  95. package/build-module/dataform-controls/email.js.map +1 -1
  96. package/build-module/dataform-controls/index.js +2 -0
  97. package/build-module/dataform-controls/index.js.map +1 -1
  98. package/build-module/dataform-controls/integer.js +24 -5
  99. package/build-module/dataform-controls/integer.js.map +1 -1
  100. package/build-module/dataform-controls/relative-date-control.js +100 -0
  101. package/build-module/dataform-controls/relative-date-control.js.map +1 -0
  102. package/build-module/dataform-controls/select.js +12 -5
  103. package/build-module/dataform-controls/select.js.map +1 -1
  104. package/build-module/dataform-controls/text.js +17 -2
  105. package/build-module/dataform-controls/text.js.map +1 -1
  106. package/build-module/dataviews-layouts/grid/index.js +93 -20
  107. package/build-module/dataviews-layouts/grid/index.js.map +1 -1
  108. package/build-module/dataviews-layouts/grid/preview-size-picker.js +40 -85
  109. package/build-module/dataviews-layouts/grid/preview-size-picker.js.map +1 -1
  110. package/build-module/dataviews-layouts/list/index.js +7 -3
  111. package/build-module/dataviews-layouts/list/index.js.map +1 -1
  112. package/build-module/dataviews-layouts/table/column-primary.js +18 -3
  113. package/build-module/dataviews-layouts/table/column-primary.js.map +1 -1
  114. package/build-module/dataviews-layouts/table/index.js +58 -6
  115. package/build-module/dataviews-layouts/table/index.js.map +1 -1
  116. package/build-module/field-types/array.js +27 -18
  117. package/build-module/field-types/array.js.map +1 -1
  118. package/build-module/field-types/boolean.js +11 -7
  119. package/build-module/field-types/boolean.js.map +1 -1
  120. package/build-module/field-types/date.js +60 -0
  121. package/build-module/field-types/date.js.map +1 -0
  122. package/build-module/field-types/datetime.js +19 -10
  123. package/build-module/field-types/datetime.js.map +1 -1
  124. package/build-module/field-types/email.js +22 -18
  125. package/build-module/field-types/email.js.map +1 -1
  126. package/build-module/field-types/index.js +20 -6
  127. package/build-module/field-types/index.js.map +1 -1
  128. package/build-module/field-types/integer.js +22 -17
  129. package/build-module/field-types/integer.js.map +1 -1
  130. package/build-module/field-types/media.js +19 -10
  131. package/build-module/field-types/media.js.map +1 -1
  132. package/build-module/field-types/text.js +19 -10
  133. package/build-module/field-types/text.js.map +1 -1
  134. package/build-module/filter-and-sort-data-view.js +28 -14
  135. package/build-module/filter-and-sort-data-view.js.map +1 -1
  136. package/build-module/normalize-fields.js +4 -5
  137. package/build-module/normalize-fields.js.map +1 -1
  138. package/build-module/types.js.map +1 -1
  139. package/build-module/validation.js +15 -2
  140. package/build-module/validation.js.map +1 -1
  141. package/build-style/style-rtl.css +84 -41
  142. package/build-style/style.css +84 -41
  143. package/build-types/components/dataform/stories/index.story.d.ts +21 -0
  144. package/build-types/components/dataform/stories/index.story.d.ts.map +1 -1
  145. package/build-types/components/dataviews/index.d.ts +3 -2
  146. package/build-types/components/dataviews/index.d.ts.map +1 -1
  147. package/build-types/components/dataviews/stories/fixtures.d.ts +1 -0
  148. package/build-types/components/dataviews/stories/fixtures.d.ts.map +1 -1
  149. package/build-types/components/dataviews/stories/index.story.d.ts +16 -2
  150. package/build-types/components/dataviews/stories/index.story.d.ts.map +1 -1
  151. package/build-types/components/dataviews-context/index.d.ts +4 -2
  152. package/build-types/components/dataviews-context/index.d.ts.map +1 -1
  153. package/build-types/components/dataviews-filters/filter.d.ts.map +1 -1
  154. package/build-types/components/dataviews-filters/index.d.ts.map +1 -1
  155. package/build-types/components/dataviews-filters/input-widget.d.ts.map +1 -1
  156. package/build-types/components/dataviews-filters/reset-filters.d.ts.map +1 -1
  157. package/build-types/components/dataviews-layout/index.d.ts.map +1 -1
  158. package/build-types/components/dataviews-view-config/index.d.ts.map +1 -1
  159. package/build-types/components/stories/index.story.d.ts +4 -0
  160. package/build-types/components/stories/index.story.d.ts.map +1 -1
  161. package/build-types/constants.d.ts +2 -2
  162. package/build-types/dataform-controls/boolean.d.ts.map +1 -1
  163. package/build-types/dataform-controls/date.d.ts +3 -0
  164. package/build-types/dataform-controls/date.d.ts.map +1 -0
  165. package/build-types/dataform-controls/datetime.d.ts.map +1 -1
  166. package/build-types/dataform-controls/email.d.ts.map +1 -1
  167. package/build-types/dataform-controls/index.d.ts.map +1 -1
  168. package/build-types/dataform-controls/integer.d.ts.map +1 -1
  169. package/build-types/dataform-controls/relative-date-control.d.ts +46 -0
  170. package/build-types/dataform-controls/relative-date-control.d.ts.map +1 -0
  171. package/build-types/dataform-controls/select.d.ts.map +1 -1
  172. package/build-types/dataform-controls/text.d.ts.map +1 -1
  173. package/build-types/dataviews-layouts/grid/index.d.ts +1 -1
  174. package/build-types/dataviews-layouts/grid/index.d.ts.map +1 -1
  175. package/build-types/dataviews-layouts/grid/preview-size-picker.d.ts +0 -1
  176. package/build-types/dataviews-layouts/grid/preview-size-picker.d.ts.map +1 -1
  177. package/build-types/dataviews-layouts/index.d.ts +3 -3
  178. package/build-types/dataviews-layouts/list/index.d.ts.map +1 -1
  179. package/build-types/dataviews-layouts/table/column-primary.d.ts.map +1 -1
  180. package/build-types/dataviews-layouts/table/index.d.ts +1 -1
  181. package/build-types/dataviews-layouts/table/index.d.ts.map +1 -1
  182. package/build-types/field-types/array.d.ts.map +1 -1
  183. package/build-types/field-types/boolean.d.ts +5 -4
  184. package/build-types/field-types/boolean.d.ts.map +1 -1
  185. package/build-types/field-types/date.d.ts +20 -0
  186. package/build-types/field-types/date.d.ts.map +1 -0
  187. package/build-types/field-types/datetime.d.ts +4 -3
  188. package/build-types/field-types/datetime.d.ts.map +1 -1
  189. package/build-types/field-types/email.d.ts +4 -3
  190. package/build-types/field-types/email.d.ts.map +1 -1
  191. package/build-types/field-types/index.d.ts.map +1 -1
  192. package/build-types/field-types/integer.d.ts +4 -3
  193. package/build-types/field-types/integer.d.ts.map +1 -1
  194. package/build-types/field-types/media.d.ts +4 -3
  195. package/build-types/field-types/media.d.ts.map +1 -1
  196. package/build-types/field-types/text.d.ts +4 -3
  197. package/build-types/field-types/text.d.ts.map +1 -1
  198. package/build-types/filter-and-sort-data-view.d.ts.map +1 -1
  199. package/build-types/normalize-fields.d.ts.map +1 -1
  200. package/build-types/types.d.ts +25 -8
  201. package/build-types/types.d.ts.map +1 -1
  202. package/build-types/validation.d.ts.map +1 -1
  203. package/build-wp/index.js +2196 -739
  204. package/package.json +15 -14
  205. package/src/components/dataform/stories/index.story.tsx +229 -2
  206. package/src/components/dataviews/index.tsx +30 -10
  207. package/src/components/dataviews/stories/fixtures.tsx +82 -59
  208. package/src/components/dataviews/stories/index.story.tsx +65 -8
  209. package/src/components/dataviews/stories/style.css +6 -0
  210. package/src/components/dataviews-context/index.ts +8 -2
  211. package/src/components/dataviews-filters/filter.tsx +17 -7
  212. package/src/components/dataviews-filters/index.tsx +17 -2
  213. package/src/components/dataviews-filters/input-widget.tsx +7 -1
  214. package/src/components/dataviews-filters/reset-filters.tsx +4 -2
  215. package/src/components/dataviews-filters/style.scss +8 -2
  216. package/src/components/dataviews-layout/index.tsx +3 -0
  217. package/src/components/dataviews-view-config/index.tsx +5 -3
  218. package/src/components/stories/index.story.tsx +21 -0
  219. package/src/dataform-controls/boolean.tsx +19 -2
  220. package/src/dataform-controls/date.tsx +499 -0
  221. package/src/dataform-controls/datetime.tsx +5 -91
  222. package/src/dataform-controls/email.tsx +19 -2
  223. package/src/dataform-controls/index.tsx +2 -0
  224. package/src/dataform-controls/integer.tsx +30 -4
  225. package/src/dataform-controls/relative-date-control.tsx +106 -0
  226. package/src/dataform-controls/select.tsx +23 -13
  227. package/src/dataform-controls/style.scss +19 -2
  228. package/src/dataform-controls/text.tsx +19 -2
  229. package/src/dataviews-layouts/grid/index.tsx +168 -55
  230. package/src/dataviews-layouts/grid/preview-size-picker.tsx +48 -73
  231. package/src/dataviews-layouts/grid/style.scss +21 -26
  232. package/src/dataviews-layouts/list/index.tsx +7 -4
  233. package/src/dataviews-layouts/list/style.scss +3 -3
  234. package/src/dataviews-layouts/table/column-primary.tsx +29 -5
  235. package/src/dataviews-layouts/table/index.tsx +134 -42
  236. package/src/dataviews-layouts/table/style.scss +45 -1
  237. package/src/field-types/array.tsx +33 -21
  238. package/src/field-types/boolean.tsx +15 -9
  239. package/src/field-types/date.ts +92 -0
  240. package/src/field-types/datetime.tsx +19 -13
  241. package/src/field-types/email.tsx +26 -21
  242. package/src/field-types/index.tsx +23 -8
  243. package/src/field-types/integer.tsx +26 -22
  244. package/src/field-types/media.tsx +19 -13
  245. package/src/field-types/text.tsx +19 -13
  246. package/src/filter-and-sort-data-view.ts +38 -13
  247. package/src/normalize-fields.ts +4 -8
  248. package/src/test/dataviews.tsx +129 -0
  249. package/src/test/filter-and-sort-data-view.js +150 -31
  250. package/src/test/validation.ts +4 -15
  251. package/src/types.ts +34 -8
  252. package/src/validation.ts +30 -1
  253. package/tsconfig.json +1 -0
  254. package/tsconfig.tsbuildinfo +1 -1
@@ -285,6 +285,71 @@ describe( 'DataViews component', () => {
285
285
  await user.click( titleField );
286
286
  expect( onClickItemCallback ).toHaveBeenCalledWith( data[ 0 ] );
287
287
  } );
288
+
289
+ it( 'accepts click for single selection', async () => {
290
+ render(
291
+ <DataViewWrapper
292
+ view={ {
293
+ ...DEFAULT_VIEW,
294
+ fields: [ 'author' ],
295
+ titleField: 'title',
296
+ } }
297
+ // A bulk action is required for the dataview to be multi-selectable.
298
+ actions={ actions }
299
+ />
300
+ );
301
+ const firstItemElement = screen.getByText( data[ 0 ].title );
302
+ const thirdItemElement = screen.getByText( data[ 2 ].title );
303
+ const user = userEvent.setup();
304
+ await user.click( firstItemElement );
305
+
306
+ // First item should be selected.
307
+ expect(
308
+ screen.getByRole( 'checkbox', { name: data[ 0 ].title } )
309
+ ).toBeChecked();
310
+ await user.click( thirdItemElement );
311
+
312
+ // Third item should be selected. First item was deselected.
313
+ expect(
314
+ screen.getByRole( 'checkbox', { name: data[ 2 ].title } )
315
+ ).toBeChecked();
316
+ } );
317
+
318
+ it( 'accepts ctrl/cmd key and click for non-consecutive multi-selection', async () => {
319
+ render(
320
+ <DataViewWrapper
321
+ view={ {
322
+ ...DEFAULT_VIEW,
323
+ fields: [ 'author' ],
324
+ titleField: 'title',
325
+ } }
326
+ // A bulk action is required for the dataview to be multi-selectable.
327
+ actions={ actions }
328
+ />
329
+ );
330
+ const firstItemElement = screen.getByText( data[ 0 ].title );
331
+ const thirdItemElement = screen.getByText( data[ 2 ].title );
332
+ const user = userEvent.setup();
333
+ await user.click( firstItemElement );
334
+
335
+ // First item should be selected.
336
+ expect(
337
+ screen.getByRole( 'checkbox', { name: data[ 0 ].title } )
338
+ ).toBeChecked();
339
+ await user.keyboard( '{Control>}' );
340
+ await user.click( thirdItemElement );
341
+
342
+ // Both items should be selected.
343
+ expect(
344
+ screen.getByRole( 'checkbox', { name: data[ 0 ].title } )
345
+ ).toBeChecked();
346
+ expect(
347
+ screen.getByRole( 'checkbox', { name: data[ 2 ].title } )
348
+ ).toBeChecked();
349
+
350
+ // Don't keep the modifier pressed down, that's just mean.
351
+ await user.keyboard( '{/Control}' );
352
+ } );
288
353
  } );
289
354
 
290
355
  describe( 'in grid view', () => {
@@ -363,6 +428,70 @@ describe( 'DataViews component', () => {
363
428
  await user.click( imageField );
364
429
  expect( mediaClickItemCallback ).toHaveBeenCalledWith( data[ 0 ] );
365
430
  } );
431
+
432
+ it( 'accepts click for single selection', async () => {
433
+ render(
434
+ <DataViewWrapper
435
+ view={ {
436
+ ...DEFAULT_VIEW,
437
+ fields: [ 'author' ],
438
+ titleField: 'title',
439
+ } }
440
+ // A bulk action is required for the dataview to be multi-selectable.
441
+ actions={ actions }
442
+ />
443
+ );
444
+ const firstItemElement = screen.getByText( data[ 0 ].title );
445
+ const thirdItemElement = screen.getByText( data[ 2 ].title );
446
+ const user = userEvent.setup();
447
+ await user.click( firstItemElement );
448
+
449
+ // First item should be selected.
450
+ expect(
451
+ screen.getByRole( 'checkbox', { name: data[ 0 ].title } )
452
+ ).toBeChecked();
453
+ await user.click( thirdItemElement );
454
+
455
+ // Third item should be selected. First item was deselected.
456
+ expect(
457
+ screen.getByRole( 'checkbox', { name: data[ 2 ].title } )
458
+ ).toBeChecked();
459
+ } );
460
+
461
+ it( 'accepts ctrl/cmd key and click for non-consecutive multi-selection', async () => {
462
+ render(
463
+ <DataViewWrapper
464
+ view={ {
465
+ ...DEFAULT_VIEW,
466
+ fields: [ 'author' ],
467
+ titleField: 'title',
468
+ } }
469
+ // A bulk action is required for the dataview to be multi-selectable.
470
+ actions={ actions }
471
+ />
472
+ );
473
+ const firstItemElement = screen.getByText( data[ 0 ].title );
474
+ const thirdItemElement = screen.getByText( data[ 2 ].title );
475
+ const user = userEvent.setup();
476
+ await user.click( firstItemElement );
477
+
478
+ // First item should be selected.
479
+ expect(
480
+ screen.getByRole( 'checkbox', { name: data[ 0 ].title } )
481
+ ).toBeChecked();
482
+ await user.keyboard( '{Control>}' );
483
+ await user.click( thirdItemElement );
484
+
485
+ // Both items should be selected.
486
+ expect(
487
+ screen.getByRole( 'checkbox', { name: data[ 0 ].title } )
488
+ ).toBeChecked();
489
+ expect(
490
+ screen.getByRole( 'checkbox', { name: data[ 2 ].title } )
491
+ ).toBeChecked();
492
+
493
+ await user.keyboard( '{/Control}' );
494
+ } );
366
495
  } );
367
496
 
368
497
  describe( 'in list view', () => {
@@ -49,13 +49,15 @@ describe( 'filters', () => {
49
49
  const { data: result } = filterSortAndPaginate(
50
50
  data,
51
51
  {
52
- search: 'photo',
52
+ search: 'earth',
53
53
  filters: [],
54
54
  },
55
55
  fields
56
56
  );
57
- expect( result ).toHaveLength( 1 );
58
- expect( result[ 0 ].description ).toBe( 'NASA photo' );
57
+ expect( result ).toHaveLength( 2 );
58
+ expect( result[ 0 ].description ).toBe(
59
+ 'The Moon is Earth’s only natural satellite, orbiting at an average distance of 384,400 kilometers with a synchronous rotation that leads to fixed lunar phases as seen from Earth. Its cratered surface and subtle glow define night skies, inspiring exploration missions and influencing tides and biological rhythms worldwide.'
60
+ );
59
61
  } );
60
62
 
61
63
  it( 'should perform case-insensitive and accent-insensitive search', () => {
@@ -71,6 +73,54 @@ describe( 'filters', () => {
71
73
  expect( result[ 0 ].description ).toBe( 'La planète Vénus' );
72
74
  } );
73
75
 
76
+ it( 'should search over array fields when enableGlobalSearch is true', () => {
77
+ const fieldsWithArraySearch = fields.map( ( field ) =>
78
+ field.id === 'categories'
79
+ ? { ...field, enableGlobalSearch: true }
80
+ : field
81
+ );
82
+
83
+ const { data: result } = filterSortAndPaginate(
84
+ data,
85
+ {
86
+ search: 'Moon',
87
+ filters: [],
88
+ },
89
+ fieldsWithArraySearch
90
+ );
91
+
92
+ // Should find items with "satellite" in categories
93
+ expect( result ).toHaveLength( 3 );
94
+ expect( result.map( ( r ) => r.title ).sort() ).toEqual( [
95
+ 'Europa',
96
+ 'Io',
97
+ 'Moon',
98
+ ] );
99
+ } );
100
+
101
+ it( 'should search over array fields case-insensitively', () => {
102
+ const fieldsWithArraySearch = fields.map( ( field ) =>
103
+ field.id === 'categories'
104
+ ? { ...field, enableGlobalSearch: true }
105
+ : field
106
+ );
107
+
108
+ const { data: result } = filterSortAndPaginate(
109
+ data,
110
+ {
111
+ search: 'planet',
112
+ filters: [],
113
+ },
114
+ fieldsWithArraySearch
115
+ );
116
+
117
+ // Should find items with "Planet" in categories (case-insensitive)
118
+ expect( result ).toHaveLength( 8 );
119
+ expect( result.map( ( r ) => r.title ) ).toContain( 'Neptune' );
120
+ expect( result.map( ( r ) => r.title ) ).toContain( 'Mercury' );
121
+ expect( result.map( ( r ) => r.title ) ).toContain( 'Earth' );
122
+ } );
123
+
74
124
  it( 'should search using IS filter', () => {
75
125
  const { data: result } = filterSortAndPaginate(
76
126
  data,
@@ -105,9 +155,9 @@ describe( 'filters', () => {
105
155
  fields
106
156
  );
107
157
  expect( result ).toHaveLength( 9 );
108
- expect( result[ 0 ].title ).toBe( 'Apollo' );
109
- expect( result[ 1 ].title ).toBe( 'Space' );
110
- expect( result[ 2 ].title ).toBe( 'NASA' );
158
+ expect( result[ 0 ].title ).toBe( 'Moon' );
159
+ expect( result[ 1 ].title ).toBe( 'Io' );
160
+ expect( result[ 2 ].title ).toBe( 'Europa' );
111
161
  expect( result[ 3 ].title ).toBe( 'Mercury' );
112
162
  expect( result[ 4 ].title ).toBe( 'Venus' );
113
163
  expect( result[ 5 ].title ).toBe( 'Earth' );
@@ -150,9 +200,9 @@ describe( 'filters', () => {
150
200
  fields
151
201
  );
152
202
  expect( result ).toHaveLength( 3 );
153
- expect( result[ 0 ].title ).toBe( 'Apollo' );
154
- expect( result[ 1 ].title ).toBe( 'Space' );
155
- expect( result[ 2 ].title ).toBe( 'NASA' );
203
+ expect( result[ 0 ].title ).toBe( 'Moon' );
204
+ expect( result[ 1 ].title ).toBe( 'Io' );
205
+ expect( result[ 2 ].title ).toBe( 'Europa' );
156
206
  } );
157
207
 
158
208
  it( 'should search using IS ANY filter for ARRAY values', () => {
@@ -163,15 +213,15 @@ describe( 'filters', () => {
163
213
  {
164
214
  field: 'categories',
165
215
  operator: 'isAny',
166
- value: [ 'NASA' ],
216
+ value: [ 'Earth' ],
167
217
  },
168
218
  ],
169
219
  },
170
220
  fields
171
221
  );
172
222
  expect( result ).toHaveLength( 2 );
173
- expect( result[ 0 ].title ).toBe( 'Apollo' );
174
- expect( result[ 1 ].title ).toBe( 'NASA' );
223
+ expect( result[ 0 ].title ).toBe( 'Moon' );
224
+ expect( result[ 1 ].title ).toBe( 'Earth' );
175
225
  } );
176
226
 
177
227
  it( 'should search using IS NONE filter for ARRAY values', () => {
@@ -182,14 +232,20 @@ describe( 'filters', () => {
182
232
  {
183
233
  field: 'categories',
184
234
  operator: 'isNone',
185
- value: [ 'Space' ],
235
+ value: [ 'Terrestrial' ],
186
236
  },
187
237
  ],
188
238
  },
189
239
  fields
190
240
  );
191
- expect( result ).toHaveLength( 1 );
192
- expect( result[ 0 ].title ).toBe( 'NASA' );
241
+ expect( result ).toHaveLength( 7 );
242
+ expect( result[ 0 ].title ).toBe( 'Moon' );
243
+ expect( result[ 1 ].title ).toBe( 'Io' );
244
+ expect( result[ 2 ].title ).toBe( 'Europa' );
245
+ expect( result[ 3 ].title ).toBe( 'Neptune' );
246
+ expect( result[ 4 ].title ).toBe( 'Jupiter' );
247
+ expect( result[ 5 ].title ).toBe( 'Saturn' );
248
+ expect( result[ 6 ].title ).toBe( 'Uranus' );
193
249
  } );
194
250
 
195
251
  it( 'should search using IS ALL filter', () => {
@@ -206,7 +262,7 @@ describe( 'filters', () => {
206
262
  },
207
263
  fields
208
264
  );
209
- expect( result ).toHaveLength( 7 );
265
+ expect( result ).toHaveLength( 8 );
210
266
  expect( result[ 0 ].title ).toBe( 'Neptune' );
211
267
  expect( result[ 1 ].title ).toBe( 'Mercury' );
212
268
  expect( result[ 2 ].title ).toBe( 'Venus' );
@@ -214,6 +270,7 @@ describe( 'filters', () => {
214
270
  expect( result[ 4 ].title ).toBe( 'Mars' );
215
271
  expect( result[ 5 ].title ).toBe( 'Jupiter' );
216
272
  expect( result[ 6 ].title ).toBe( 'Saturn' );
273
+ expect( result[ 7 ].title ).toBe( 'Uranus' );
217
274
  } );
218
275
 
219
276
  it( 'should search using IS NOT ALL filter', () => {
@@ -224,16 +281,16 @@ describe( 'filters', () => {
224
281
  {
225
282
  field: 'categories',
226
283
  operator: 'isNotAll',
227
- value: [ 'Planet', 'Solar system' ],
284
+ value: [ 'Planet' ],
228
285
  },
229
286
  ],
230
287
  },
231
288
  fields
232
289
  );
233
290
  expect( result ).toHaveLength( 3 );
234
- expect( result[ 0 ].title ).toBe( 'Apollo' );
235
- expect( result[ 1 ].title ).toBe( 'Space' );
236
- expect( result[ 2 ].title ).toBe( 'NASA' );
291
+ expect( result[ 0 ].title ).toBe( 'Moon' );
292
+ expect( result[ 1 ].title ).toBe( 'Io' );
293
+ expect( result[ 2 ].title ).toBe( 'Europa' );
237
294
  } );
238
295
 
239
296
  it( 'should search using IS filter and return all values if filter.value is undefined', () => {
@@ -251,9 +308,9 @@ describe( 'filters', () => {
251
308
  fields
252
309
  );
253
310
  expect( result ).toHaveLength( 11 );
254
- expect( result[ 0 ].title ).toBe( 'Apollo' );
255
- expect( result[ 1 ].title ).toBe( 'Space' );
256
- expect( result[ 2 ].title ).toBe( 'NASA' );
311
+ expect( result[ 0 ].title ).toBe( 'Moon' );
312
+ expect( result[ 1 ].title ).toBe( 'Io' );
313
+ expect( result[ 2 ].title ).toBe( 'Europa' );
257
314
  expect( result[ 3 ].title ).toBe( 'Neptune' );
258
315
  expect( result[ 4 ].title ).toBe( 'Mercury' );
259
316
  expect( result[ 5 ].title ).toBe( 'Venus' );
@@ -360,15 +417,16 @@ describe( 'filters', () => {
360
417
  {
361
418
  field: 'description',
362
419
  operator: 'notContains',
363
- value: 'description',
420
+ value: 'Solar system',
364
421
  },
365
422
  ],
366
423
  },
367
424
  fields
368
425
  );
369
- // Only 'NASA photo' and 'La planète Vénus' do not contain 'description'
370
426
  expect( result.map( ( r ) => r.description ) ).toEqual( [
371
- 'NASA photo',
427
+ 'The Moon is Earth’s only natural satellite, orbiting at an average distance of 384,400 kilometers with a synchronous rotation that leads to fixed lunar phases as seen from Earth. Its cratered surface and subtle glow define night skies, inspiring exploration missions and influencing tides and biological rhythms worldwide.',
428
+ 'Moon of Jupiter',
429
+ 'Moon of Jupiter',
372
430
  'La planète Vénus',
373
431
  ] );
374
432
  } );
@@ -740,6 +798,45 @@ describe( 'filters', () => {
740
798
  } );
741
799
 
742
800
  describe( 'sorting', () => {
801
+ it( 'should sort by groupByField first, then by sort.field', () => {
802
+ const { data: result } = filterSortAndPaginate(
803
+ data,
804
+ {
805
+ sort: { field: 'title', direction: 'desc' },
806
+ groupByField: 'type',
807
+ },
808
+ fields
809
+ );
810
+
811
+ expect( result ).toHaveLength( 11 );
812
+
813
+ expect( result[ 0 ].type ).toBe( 'Gas giant' );
814
+ expect( result[ 0 ].title ).toBe( 'Saturn' );
815
+ expect( result[ 1 ].type ).toBe( 'Gas giant' );
816
+ expect( result[ 1 ].title ).toBe( 'Jupiter' );
817
+
818
+ expect( result[ 2 ].type ).toBe( 'Ice giant' );
819
+ expect( result[ 2 ].title ).toBe( 'Uranus' );
820
+ expect( result[ 3 ].type ).toBe( 'Ice giant' );
821
+ expect( result[ 3 ].title ).toBe( 'Neptune' );
822
+
823
+ expect( result[ 4 ].type ).toBe( 'Satellite' );
824
+ expect( result[ 4 ].title ).toBe( 'Moon' );
825
+ expect( result[ 5 ].type ).toBe( 'Satellite' );
826
+ expect( result[ 5 ].title ).toBe( 'Io' );
827
+ expect( result[ 6 ].type ).toBe( 'Satellite' );
828
+ expect( result[ 6 ].title ).toBe( 'Europa' );
829
+
830
+ expect( result[ 7 ].type ).toBe( 'Terrestrial' );
831
+ expect( result[ 7 ].title ).toBe( 'Venus' );
832
+ expect( result[ 8 ].type ).toBe( 'Terrestrial' );
833
+ expect( result[ 8 ].title ).toBe( 'Mercury' );
834
+ expect( result[ 9 ].type ).toBe( 'Terrestrial' );
835
+ expect( result[ 9 ].title ).toBe( 'Mars' );
836
+ expect( result[ 10 ].type ).toBe( 'Terrestrial' );
837
+ expect( result[ 10 ].title ).toBe( 'Earth' );
838
+ } );
839
+
743
840
  it( 'should sort integer field types', () => {
744
841
  const { data: result } = filterSortAndPaginate(
745
842
  data,
@@ -784,9 +881,9 @@ describe( 'sorting', () => {
784
881
  fields
785
882
  );
786
883
  expect( resultDesc ).toHaveLength( 11 );
787
- expect( resultDesc[ 0 ].title ).toBe( 'NASA' );
884
+ expect( resultDesc[ 0 ].title ).toBe( 'Europa' );
788
885
  expect( resultDesc[ 1 ].title ).toBe( 'Earth' );
789
- expect( resultDesc[ 9 ].title ).toBe( 'Space' );
886
+ expect( resultDesc[ 9 ].title ).toBe( 'Io' );
790
887
  expect( resultDesc[ 10 ].title ).toBe( 'Jupiter' );
791
888
 
792
889
  const { data: resultAsc } = filterSortAndPaginate(
@@ -798,9 +895,9 @@ describe( 'sorting', () => {
798
895
  );
799
896
  expect( resultAsc ).toHaveLength( 11 );
800
897
  expect( resultAsc[ 0 ].title ).toBe( 'Jupiter' );
801
- expect( resultAsc[ 1 ].title ).toBe( 'Space' );
898
+ expect( resultAsc[ 1 ].title ).toBe( 'Io' );
802
899
  expect( resultAsc[ 9 ].title ).toBe( 'Earth' );
803
- expect( resultAsc[ 10 ].title ).toBe( 'NASA' );
900
+ expect( resultAsc[ 10 ].title ).toBe( 'Europa' );
804
901
  } );
805
902
 
806
903
  it( 'should sort untyped fields if the value is a number', () => {
@@ -845,6 +942,28 @@ describe( 'sorting', () => {
845
942
  expect( result[ 0 ].title ).toBe( 'Uranus' );
846
943
  expect( result[ 1 ].title ).toBe( 'Neptune' );
847
944
  } );
945
+
946
+ it( 'should sort only by groupByField when sort is not specified', () => {
947
+ const { data: result } = filterSortAndPaginate(
948
+ data,
949
+ {
950
+ groupByField: 'type',
951
+ },
952
+ fields
953
+ );
954
+
955
+ let currentType = result[ 0 ].type;
956
+ let groupCount = 1;
957
+
958
+ for ( let i = 1; i < result.length; i++ ) {
959
+ if ( result[ i ].type !== currentType ) {
960
+ currentType = result[ i ].type;
961
+ groupCount++;
962
+ }
963
+ }
964
+
965
+ expect( groupCount ).toBe( 4 );
966
+ } );
848
967
  } );
849
968
 
850
969
  describe( 'pagination', () => {
@@ -859,7 +978,7 @@ describe( 'pagination', () => {
859
978
  fields
860
979
  );
861
980
  expect( result ).toHaveLength( 2 );
862
- expect( result[ 0 ].title ).toBe( 'NASA' );
981
+ expect( result[ 0 ].title ).toBe( 'Europa' );
863
982
  expect( result[ 1 ].title ).toBe( 'Neptune' );
864
983
  expect( paginationInfo ).toStrictEqual( {
865
984
  totalItems: data.length,
@@ -35,7 +35,7 @@ describe( 'validation', () => {
35
35
  expect( result ).toBe( true );
36
36
  } );
37
37
 
38
- it( 'integer field is invalid if value is not integer', () => {
38
+ it( 'integer field is invalid if value is not integer when not empty', () => {
39
39
  const item = { id: 1, order: 'd' };
40
40
  const fields: Field< {} >[] = [
41
41
  {
@@ -48,19 +48,6 @@ describe( 'validation', () => {
48
48
  expect( result ).toBe( false );
49
49
  } );
50
50
 
51
- it( 'integer field is invalid if value is empty', () => {
52
- const item = { id: 1, order: '' };
53
- const fields: Field< {} >[] = [
54
- {
55
- id: 'order',
56
- type: 'integer',
57
- },
58
- ];
59
- const form = { fields: [ 'order' ] };
60
- const result = isItemValid( item, fields, form );
61
- expect( result ).toBe( false );
62
- } );
63
-
64
51
  it( 'integer field is invalid if value is not one of the elements', () => {
65
52
  const item = { id: 1, author: 3 };
66
53
  const fields: Field< {} >[] = [
@@ -121,7 +108,9 @@ describe( 'validation', () => {
121
108
  { value: 'a', label: 'A' },
122
109
  { value: 'b', label: 'B' },
123
110
  ],
124
- isValid: () => true, // Overrides the validation provided for integer types.
111
+ isValid: {
112
+ custom: () => null, // Overrides the validation provided for integer types.
113
+ },
125
114
  },
126
115
  ];
127
116
  const form = { fields: [ 'order' ] };
package/src/types.ts CHANGED
@@ -1,7 +1,12 @@
1
1
  /**
2
2
  * External dependencies
3
3
  */
4
- import type { ReactElement, ComponentType, ComponentProps } from 'react';
4
+ import type {
5
+ ReactElement,
6
+ ReactNode,
7
+ ComponentType,
8
+ ComponentProps,
9
+ } from 'react';
5
10
 
6
11
  /**
7
12
  * Internal dependencies
@@ -94,15 +99,12 @@ export type FieldType =
94
99
  | 'text'
95
100
  | 'integer'
96
101
  | 'datetime'
102
+ | 'date'
97
103
  | 'media'
98
104
  | 'boolean'
99
105
  | 'email'
100
106
  | 'array';
101
107
 
102
- export type ValidationContext = {
103
- elements?: Option[];
104
- };
105
-
106
108
  /**
107
109
  * An abstract interface for Field based on the field type.
108
110
  */
@@ -115,7 +117,7 @@ export type FieldTypeDefinition< Item > = {
115
117
  /**
116
118
  * Callback used to validate the field.
117
119
  */
118
- isValid: ( item: Item, context?: ValidationContext ) => boolean;
120
+ isValid: Rules< Item >;
119
121
 
120
122
  /**
121
123
  * Callback used to render an edit control for the field or control name.
@@ -144,6 +146,11 @@ export type FieldTypeDefinition< Item > = {
144
146
  enableSorting: boolean;
145
147
  };
146
148
 
149
+ export type Rules< Item > = {
150
+ required?: boolean;
151
+ custom?: ( item: Item, field: NormalizedField< Item > ) => null | string;
152
+ };
153
+
147
154
  /**
148
155
  * A dataview field for a specific property of a data type.
149
156
  */
@@ -197,7 +204,7 @@ export type Field< Item > = {
197
204
  /**
198
205
  * Callback used to validate the field.
199
206
  */
200
- isValid?: ( item: Item, context?: ValidationContext ) => boolean;
207
+ isValid?: Rules< Item >;
201
208
 
202
209
  /**
203
210
  * Callback used to decide if a field should be displayed.
@@ -249,7 +256,7 @@ export type NormalizedField< Item > = Omit< Field< Item >, 'Edit' > & {
249
256
  render: ComponentType< DataViewRenderFieldProps< Item > >;
250
257
  Edit: ComponentType< DataFormControlProps< Item > > | null;
251
258
  sort: ( a: Item, b: Item, direction: SortDirection ) => number;
252
- isValid: ( item: Item, context?: ValidationContext ) => boolean;
259
+ isValid: Rules< Item >;
253
260
  enableHiding: boolean;
254
261
  enableSorting: boolean;
255
262
  filterBy: NormalizedFilterByConfig | false;
@@ -279,6 +286,9 @@ export type DataFormControlProps< Item > = {
279
286
  export type DataViewRenderFieldProps< Item > = {
280
287
  item: Item;
281
288
  field: NormalizedField< Item >;
289
+ config?: {
290
+ sizes: string;
291
+ };
282
292
  };
283
293
 
284
294
  /**
@@ -299,6 +309,11 @@ export interface Filter {
299
309
  * The value to filter by.
300
310
  */
301
311
  value: any;
312
+
313
+ /**
314
+ * Whether the filter can be edited by the user.
315
+ */
316
+ isLocked?: boolean;
302
317
  }
303
318
 
304
319
  export interface NormalizedFilter {
@@ -336,6 +351,11 @@ export interface NormalizedFilter {
336
351
  * Whether it is a primary filter.
337
352
  */
338
353
  isPrimary: boolean;
354
+
355
+ /**
356
+ * Whether the filter can be edited by the user.
357
+ */
358
+ isLocked: boolean;
339
359
  }
340
360
 
341
361
  interface ViewBase {
@@ -418,6 +438,11 @@ interface ViewBase {
418
438
  * Whether to show the hierarchical levels.
419
439
  */
420
440
  showLevels?: boolean;
441
+
442
+ /**
443
+ * The field to group by.
444
+ */
445
+ groupByField?: string;
421
446
  }
422
447
 
423
448
  export interface ColumnStyle {
@@ -610,6 +635,7 @@ export interface ViewBaseProps< Item > {
610
635
  ) => ReactElement;
611
636
  isItemClickable: ( item: Item ) => boolean;
612
637
  view: View;
638
+ empty: ReactNode;
613
639
  }
614
640
 
615
641
  export interface ViewTableProps< Item > extends ViewBaseProps< Item > {
package/src/validation.ts CHANGED
@@ -21,7 +21,36 @@ export function isItemValid< Item >(
21
21
  const _fields = normalizeFields(
22
22
  fields.filter( ( { id } ) => !! form.fields?.includes( id ) )
23
23
  );
24
+
25
+ const isEmptyNullOrUndefined = ( value: any ) =>
26
+ [ undefined, '', null ].includes( value );
27
+
24
28
  return _fields.every( ( field ) => {
25
- return field.isValid( item, { elements: field.elements } );
29
+ const value = field.getValue( { item } );
30
+
31
+ if ( field.isValid.required ) {
32
+ if (
33
+ ( field.type === 'text' && isEmptyNullOrUndefined( value ) ) ||
34
+ ( field.type === 'email' && isEmptyNullOrUndefined( value ) ) ||
35
+ ( field.type === 'integer' &&
36
+ isEmptyNullOrUndefined( value ) ) ||
37
+ ( field.type === undefined && isEmptyNullOrUndefined( value ) )
38
+ ) {
39
+ return false;
40
+ }
41
+
42
+ if ( field.type === 'boolean' && value !== true ) {
43
+ return false;
44
+ }
45
+ }
46
+
47
+ if (
48
+ typeof field.isValid.custom === 'function' &&
49
+ field.isValid.custom( item, field ) !== null
50
+ ) {
51
+ return false;
52
+ }
53
+
54
+ return true;
26
55
  } );
27
56
  }
package/tsconfig.json CHANGED
@@ -17,6 +17,7 @@
17
17
  { "path": "../element" },
18
18
  { "path": "../i18n" },
19
19
  { "path": "../icons" },
20
+ { "path": "../keycodes" },
20
21
  { "path": "../primitives" },
21
22
  { "path": "../private-apis" },
22
23
  { "path": "../url" },