@wordpress/dataviews 5.0.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 (250) hide show
  1. package/CHANGELOG.md +31 -1
  2. package/README.md +55 -27
  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 +40 -23
  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 +21 -12
  52. package/build/field-types/date.js.map +1 -1
  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 +16 -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 +6 -4
  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 +41 -24
  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 +21 -12
  121. package/build-module/field-types/date.js.map +1 -1
  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 +16 -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 +6 -4
  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 +78 -43
  142. package/build-style/style.css +78 -43
  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.map +1 -1
  148. package/build-types/components/dataviews/stories/index.story.d.ts +16 -3
  149. package/build-types/components/dataviews/stories/index.story.d.ts.map +1 -1
  150. package/build-types/components/dataviews-context/index.d.ts +4 -2
  151. package/build-types/components/dataviews-context/index.d.ts.map +1 -1
  152. package/build-types/components/dataviews-filters/filter.d.ts.map +1 -1
  153. package/build-types/components/dataviews-filters/index.d.ts.map +1 -1
  154. package/build-types/components/dataviews-filters/input-widget.d.ts.map +1 -1
  155. package/build-types/components/dataviews-filters/reset-filters.d.ts.map +1 -1
  156. package/build-types/components/dataviews-layout/index.d.ts.map +1 -1
  157. package/build-types/components/dataviews-view-config/index.d.ts.map +1 -1
  158. package/build-types/constants.d.ts +2 -2
  159. package/build-types/dataform-controls/boolean.d.ts.map +1 -1
  160. package/build-types/dataform-controls/date.d.ts +3 -0
  161. package/build-types/dataform-controls/date.d.ts.map +1 -0
  162. package/build-types/dataform-controls/datetime.d.ts.map +1 -1
  163. package/build-types/dataform-controls/email.d.ts.map +1 -1
  164. package/build-types/dataform-controls/index.d.ts.map +1 -1
  165. package/build-types/dataform-controls/integer.d.ts.map +1 -1
  166. package/build-types/dataform-controls/relative-date-control.d.ts +46 -0
  167. package/build-types/dataform-controls/relative-date-control.d.ts.map +1 -0
  168. package/build-types/dataform-controls/select.d.ts.map +1 -1
  169. package/build-types/dataform-controls/text.d.ts.map +1 -1
  170. package/build-types/dataviews-layouts/grid/index.d.ts +1 -1
  171. package/build-types/dataviews-layouts/grid/index.d.ts.map +1 -1
  172. package/build-types/dataviews-layouts/grid/preview-size-picker.d.ts +0 -1
  173. package/build-types/dataviews-layouts/grid/preview-size-picker.d.ts.map +1 -1
  174. package/build-types/dataviews-layouts/index.d.ts +3 -3
  175. package/build-types/dataviews-layouts/list/index.d.ts.map +1 -1
  176. package/build-types/dataviews-layouts/table/column-primary.d.ts.map +1 -1
  177. package/build-types/dataviews-layouts/table/index.d.ts +1 -1
  178. package/build-types/dataviews-layouts/table/index.d.ts.map +1 -1
  179. package/build-types/field-types/array.d.ts.map +1 -1
  180. package/build-types/field-types/boolean.d.ts +5 -4
  181. package/build-types/field-types/boolean.d.ts.map +1 -1
  182. package/build-types/field-types/date.d.ts +9 -5
  183. package/build-types/field-types/date.d.ts.map +1 -1
  184. package/build-types/field-types/datetime.d.ts +4 -3
  185. package/build-types/field-types/datetime.d.ts.map +1 -1
  186. package/build-types/field-types/email.d.ts +4 -3
  187. package/build-types/field-types/email.d.ts.map +1 -1
  188. package/build-types/field-types/index.d.ts.map +1 -1
  189. package/build-types/field-types/integer.d.ts +4 -3
  190. package/build-types/field-types/integer.d.ts.map +1 -1
  191. package/build-types/field-types/media.d.ts +4 -3
  192. package/build-types/field-types/media.d.ts.map +1 -1
  193. package/build-types/field-types/text.d.ts +4 -3
  194. package/build-types/field-types/text.d.ts.map +1 -1
  195. package/build-types/filter-and-sort-data-view.d.ts.map +1 -1
  196. package/build-types/normalize-fields.d.ts.map +1 -1
  197. package/build-types/types.d.ts +20 -7
  198. package/build-types/types.d.ts.map +1 -1
  199. package/build-types/validation.d.ts.map +1 -1
  200. package/build-wp/index.js +1561 -670
  201. package/package.json +15 -14
  202. package/src/components/dataform/stories/index.story.tsx +229 -2
  203. package/src/components/dataviews/index.tsx +30 -10
  204. package/src/components/dataviews/stories/fixtures.tsx +3 -1
  205. package/src/components/dataviews/stories/index.story.tsx +49 -29
  206. package/src/components/dataviews/stories/style.css +6 -0
  207. package/src/components/dataviews-context/index.ts +8 -2
  208. package/src/components/dataviews-filters/filter.tsx +17 -7
  209. package/src/components/dataviews-filters/index.tsx +17 -2
  210. package/src/components/dataviews-filters/input-widget.tsx +7 -1
  211. package/src/components/dataviews-filters/reset-filters.tsx +4 -2
  212. package/src/components/dataviews-filters/style.scss +8 -2
  213. package/src/components/dataviews-layout/index.tsx +3 -0
  214. package/src/components/dataviews-view-config/index.tsx +5 -3
  215. package/src/dataform-controls/boolean.tsx +19 -2
  216. package/src/dataform-controls/date.tsx +499 -0
  217. package/src/dataform-controls/datetime.tsx +5 -91
  218. package/src/dataform-controls/email.tsx +19 -2
  219. package/src/dataform-controls/index.tsx +2 -0
  220. package/src/dataform-controls/integer.tsx +30 -4
  221. package/src/dataform-controls/relative-date-control.tsx +106 -0
  222. package/src/dataform-controls/select.tsx +23 -13
  223. package/src/dataform-controls/style.scss +19 -2
  224. package/src/dataform-controls/text.tsx +19 -2
  225. package/src/dataviews-layouts/grid/index.tsx +46 -24
  226. package/src/dataviews-layouts/grid/preview-size-picker.tsx +48 -73
  227. package/src/dataviews-layouts/grid/style.scss +15 -28
  228. package/src/dataviews-layouts/list/index.tsx +7 -4
  229. package/src/dataviews-layouts/list/style.scss +3 -3
  230. package/src/dataviews-layouts/table/column-primary.tsx +29 -5
  231. package/src/dataviews-layouts/table/index.tsx +134 -42
  232. package/src/dataviews-layouts/table/style.scss +45 -1
  233. package/src/field-types/array.tsx +33 -21
  234. package/src/field-types/boolean.tsx +15 -9
  235. package/src/field-types/date.ts +51 -15
  236. package/src/field-types/datetime.tsx +19 -13
  237. package/src/field-types/email.tsx +26 -21
  238. package/src/field-types/index.tsx +18 -8
  239. package/src/field-types/integer.tsx +26 -22
  240. package/src/field-types/media.tsx +19 -13
  241. package/src/field-types/text.tsx +19 -13
  242. package/src/filter-and-sort-data-view.ts +11 -4
  243. package/src/normalize-fields.ts +4 -8
  244. package/src/test/dataviews.tsx +129 -0
  245. package/src/test/filter-and-sort-data-view.js +52 -2
  246. package/src/test/validation.ts +4 -15
  247. package/src/types.ts +28 -8
  248. package/src/validation.ts +30 -1
  249. package/tsconfig.json +1 -0
  250. package/tsconfig.tsbuildinfo +1 -1
@@ -28,7 +28,8 @@ export default function ResetFilter( {
28
28
  ! view.search &&
29
29
  ! view.filters?.some(
30
30
  ( _filter ) =>
31
- _filter.value !== undefined || ! isPrimary( _filter.field )
31
+ ! _filter.isLocked &&
32
+ ( _filter.value !== undefined || ! isPrimary( _filter.field ) )
32
33
  );
33
34
  return (
34
35
  <Button
@@ -42,7 +43,8 @@ export default function ResetFilter( {
42
43
  ...view,
43
44
  page: 1,
44
45
  search: '',
45
- filters: [],
46
+ filters:
47
+ view.filters?.filter( ( f ) => !! f.isLocked ) || [],
46
48
  } );
47
49
  } }
48
50
  >
@@ -22,7 +22,9 @@
22
22
  line-height: $default-line-height;
23
23
 
24
24
  .components-popover__content {
25
- width: 230px;
25
+ width: 100%;
26
+ min-width: 230px;
27
+ max-width: 250px;
26
28
  border-radius: $grid-unit-05;
27
29
  }
28
30
 
@@ -78,11 +80,15 @@
78
80
  align-items: center;
79
81
  box-sizing: border-box;
80
82
 
83
+ &.is-not-clickable {
84
+ cursor: default;
85
+ }
86
+
81
87
  &.has-reset {
82
88
  padding-inline-end: $button-size-small + $grid-unit-05;
83
89
  }
84
90
 
85
- &:hover,
91
+ &:hover:not(&.is-not-clickable),
86
92
  &:focus-visible,
87
93
  &[aria-expanded="true"] {
88
94
  background: $gray-200;
@@ -7,6 +7,7 @@ import type { ComponentType } from 'react';
7
7
  * WordPress dependencies
8
8
  */
9
9
  import { useContext } from '@wordpress/element';
10
+ import { __ } from '@wordpress/i18n';
10
11
 
11
12
  /**
12
13
  * Internal dependencies
@@ -35,6 +36,7 @@ export default function DataViewsLayout( { className }: DataViewsLayoutProps ) {
35
36
  onClickItem,
36
37
  isItemClickable,
37
38
  renderItemLink,
39
+ empty = __( 'No results' ),
38
40
  } = useContext( DataViewsContext );
39
41
 
40
42
  const ViewComponent = VIEW_LAYOUTS.find( ( v ) => v.type === view.type )
@@ -57,6 +59,7 @@ export default function DataViewsLayout( { className }: DataViewsLayoutProps ) {
57
59
  renderItemLink={ renderItemLink }
58
60
  isItemClickable={ isItemClickable }
59
61
  view={ view }
62
+ empty={ empty }
60
63
  />
61
64
  );
62
65
  }
@@ -213,10 +213,12 @@ function SortDirectionControl() {
213
213
  );
214
214
  }
215
215
 
216
- const PAGE_SIZE_VALUES = [ 10, 20, 50, 100 ];
217
216
  function ItemsPerPageControl() {
218
217
  const { view, perPageSizes, onChangeView } = useContext( DataViewsContext );
219
- const pageSizeValues = perPageSizes ?? PAGE_SIZE_VALUES;
218
+ if ( perPageSizes.length < 2 || perPageSizes.length > 6 ) {
219
+ return null;
220
+ }
221
+
220
222
  return (
221
223
  <ToggleGroupControl
222
224
  __nextHasNoMarginBottom
@@ -238,7 +240,7 @@ function ItemsPerPageControl() {
238
240
  } );
239
241
  } }
240
242
  >
241
- { pageSizeValues.map( ( value ) => {
243
+ { perPageSizes.map( ( value ) => {
242
244
  return (
243
245
  <ToggleGroupControlOption
244
246
  key={ value }
@@ -1,12 +1,15 @@
1
1
  /**
2
2
  * WordPress dependencies
3
3
  */
4
- import { ToggleControl } from '@wordpress/components';
4
+ import { privateApis } from '@wordpress/components';
5
5
 
6
6
  /**
7
7
  * Internal dependencies
8
8
  */
9
9
  import type { DataFormControlProps } from '../types';
10
+ import { unlock } from '../lock-unlock';
11
+
12
+ const { ValidatedToggleControl } = unlock( privateApis );
10
13
 
11
14
  export default function Boolean< Item >( {
12
15
  field,
@@ -17,7 +20,21 @@ export default function Boolean< Item >( {
17
20
  const { id, getValue, label } = field;
18
21
 
19
22
  return (
20
- <ToggleControl
23
+ <ValidatedToggleControl
24
+ required={ !! field.isValid.required }
25
+ customValidator={ ( newValue: any ) => {
26
+ if ( field.isValid?.custom ) {
27
+ return field.isValid.custom(
28
+ {
29
+ ...data,
30
+ [ id ]: newValue,
31
+ },
32
+ field
33
+ );
34
+ }
35
+
36
+ return null;
37
+ } }
21
38
  hidden={ hideLabelFromVision }
22
39
  __nextHasNoMarginBottom
23
40
  label={ label }
@@ -0,0 +1,499 @@
1
+ /**
2
+ * WordPress dependencies
3
+ */
4
+ import {
5
+ BaseControl,
6
+ Button,
7
+ privateApis as componentsPrivateApis,
8
+ __experimentalInputControl as InputControl,
9
+ __experimentalHStack as HStack,
10
+ __experimentalVStack as VStack,
11
+ } from '@wordpress/components';
12
+ import { useCallback, useMemo, useState } from '@wordpress/element';
13
+ import { __ } from '@wordpress/i18n';
14
+ import { getDate, getSettings } from '@wordpress/date';
15
+
16
+ /**
17
+ * External dependencies
18
+ */
19
+ import {
20
+ format,
21
+ isValid,
22
+ subMonths,
23
+ subDays,
24
+ subYears,
25
+ startOfMonth,
26
+ startOfYear,
27
+ } from 'date-fns';
28
+
29
+ /**
30
+ * Internal dependencies
31
+ */
32
+ import RelativeDateControl, {
33
+ TIME_UNITS_OPTIONS,
34
+ } from './relative-date-control';
35
+ import {
36
+ OPERATOR_IN_THE_PAST,
37
+ OPERATOR_OVER,
38
+ OPERATOR_BETWEEN,
39
+ } from '../constants';
40
+ import { unlock } from '../lock-unlock';
41
+ import type { DataFormControlProps } from '../types';
42
+
43
+ const { DateCalendar, DateRangeCalendar } = unlock( componentsPrivateApis );
44
+
45
+ const DATE_PRESETS: {
46
+ id: string;
47
+ label: string;
48
+ getValue: () => Date;
49
+ }[] = [
50
+ {
51
+ id: 'today',
52
+ label: __( 'Today' ),
53
+ getValue: () => getDate( null ),
54
+ },
55
+ {
56
+ id: 'yesterday',
57
+ label: __( 'Yesterday' ),
58
+ getValue: () => {
59
+ const today = getDate( null );
60
+ return subDays( today, 1 );
61
+ },
62
+ },
63
+ {
64
+ id: 'past-week',
65
+ label: __( 'Past week' ),
66
+ getValue: () => {
67
+ const today = getDate( null );
68
+ return subDays( today, 7 );
69
+ },
70
+ },
71
+ {
72
+ id: 'past-month',
73
+ label: __( 'Past month' ),
74
+ getValue: () => {
75
+ const today = getDate( null );
76
+ return subMonths( today, 1 );
77
+ },
78
+ },
79
+ ];
80
+
81
+ const DATE_RANGE_PRESETS = [
82
+ {
83
+ id: 'last-7-days',
84
+ label: __( 'Last 7 days' ),
85
+ getValue: () => {
86
+ const today = getDate( null );
87
+ return [ subDays( today, 7 ), today ];
88
+ },
89
+ },
90
+ {
91
+ id: 'last-30-days',
92
+ label: __( 'Last 30 days' ),
93
+ getValue: () => {
94
+ const today = getDate( null );
95
+ return [ subDays( today, 30 ), today ];
96
+ },
97
+ },
98
+ {
99
+ id: 'month-to-date',
100
+ label: __( 'Month to date' ),
101
+ getValue: () => {
102
+ const today = getDate( null );
103
+ return [ startOfMonth( today ), today ];
104
+ },
105
+ },
106
+ {
107
+ id: 'last-year',
108
+ label: __( 'Last year' ),
109
+ getValue: () => {
110
+ const today = getDate( null );
111
+ return [ subYears( today, 1 ), today ];
112
+ },
113
+ },
114
+ {
115
+ id: 'year-to-date',
116
+ label: __( 'Year to date' ),
117
+ getValue: () => {
118
+ const today = getDate( null );
119
+ return [ startOfYear( today ), today ];
120
+ },
121
+ },
122
+ ];
123
+
124
+ const parseDate = ( dateString?: string ): Date | null => {
125
+ if ( ! dateString ) {
126
+ return null;
127
+ }
128
+ const parsed = getDate( dateString );
129
+ return parsed && isValid( parsed ) ? parsed : null;
130
+ };
131
+
132
+ const formatDate = ( date?: Date | string ): string => {
133
+ if ( ! date ) {
134
+ return '';
135
+ }
136
+ return typeof date === 'string' ? date : format( date, 'yyyy-MM-dd' );
137
+ };
138
+
139
+ function CalendarDateControl( {
140
+ id,
141
+ value,
142
+ onChange,
143
+ label,
144
+ hideLabelFromVision,
145
+ className,
146
+ }: {
147
+ id: string;
148
+ value: string | undefined;
149
+ onChange: ( value: any ) => void;
150
+ label: string;
151
+ hideLabelFromVision?: boolean;
152
+ className?: string;
153
+ } ) {
154
+ const [ selectedPresetId, setSelectedPresetId ] = useState< string | null >(
155
+ null
156
+ );
157
+
158
+ const [ calendarMonth, setCalendarMonth ] = useState< Date >( () => {
159
+ const parsedDate = parseDate( value );
160
+ return parsedDate || new Date(); // Default to current month
161
+ } );
162
+
163
+ const onSelectDate = useCallback(
164
+ ( newDate: Date | undefined | null ) => {
165
+ const dateValue = newDate
166
+ ? format( newDate, 'yyyy-MM-dd' )
167
+ : undefined;
168
+ onChange( { [ id ]: dateValue } );
169
+ setSelectedPresetId( null );
170
+ },
171
+ [ id, onChange ]
172
+ );
173
+
174
+ const handlePresetClick = useCallback(
175
+ ( preset: ( typeof DATE_PRESETS )[ 0 ] ) => {
176
+ const presetDate = preset.getValue();
177
+ const dateValue = formatDate( presetDate );
178
+
179
+ setCalendarMonth( presetDate );
180
+ onChange( { [ id ]: dateValue } );
181
+ setSelectedPresetId( preset.id );
182
+ },
183
+ [ id, onChange ]
184
+ );
185
+
186
+ const handleManualDateChange = useCallback(
187
+ ( newValue?: string ) => {
188
+ onChange( { [ id ]: newValue } );
189
+ if ( newValue ) {
190
+ const parsedDate = parseDate( newValue );
191
+ if ( parsedDate ) {
192
+ setCalendarMonth( parsedDate );
193
+ }
194
+ }
195
+ setSelectedPresetId( null );
196
+ },
197
+ [ id, onChange ]
198
+ );
199
+
200
+ const {
201
+ timezone: { string: timezoneString },
202
+ l10n: { startOfWeek },
203
+ } = getSettings();
204
+
205
+ return (
206
+ <BaseControl
207
+ __nextHasNoMarginBottom
208
+ id={ id }
209
+ className={ className }
210
+ label={ label }
211
+ hideLabelFromVision={ hideLabelFromVision }
212
+ >
213
+ <VStack spacing={ 4 }>
214
+ { /* Preset buttons */ }
215
+ <HStack spacing={ 2 } wrap justify="flex-start">
216
+ { DATE_PRESETS.map( ( preset ) => {
217
+ const isSelected = selectedPresetId === preset.id;
218
+ return (
219
+ <Button
220
+ className="dataviews-controls__date-preset"
221
+ key={ preset.id }
222
+ variant="tertiary"
223
+ isPressed={ isSelected }
224
+ size="small"
225
+ onClick={ () => handlePresetClick( preset ) }
226
+ >
227
+ { preset.label }
228
+ </Button>
229
+ );
230
+ } ) }
231
+ <Button
232
+ className="dataviews-controls__date-preset"
233
+ variant="tertiary"
234
+ isPressed={ ! selectedPresetId }
235
+ size="small"
236
+ disabled={ !! selectedPresetId }
237
+ accessibleWhenDisabled={ false }
238
+ >
239
+ { __( 'Custom' ) }
240
+ </Button>
241
+ </HStack>
242
+
243
+ { /* Manual date input */ }
244
+ <InputControl
245
+ __next40pxDefaultSize
246
+ type="date"
247
+ label={ __( 'Date' ) }
248
+ hideLabelFromVision
249
+ value={ value }
250
+ onChange={ handleManualDateChange }
251
+ />
252
+
253
+ { /* Calendar widget */ }
254
+ <DateCalendar
255
+ style={ { width: '100%' } }
256
+ selected={
257
+ value ? parseDate( value ) || undefined : undefined
258
+ }
259
+ onSelect={ onSelectDate }
260
+ month={ calendarMonth }
261
+ onMonthChange={ setCalendarMonth }
262
+ timeZone={ timezoneString || undefined }
263
+ weekStartsOn={ startOfWeek }
264
+ />
265
+ </VStack>
266
+ </BaseControl>
267
+ );
268
+ }
269
+
270
+ function CalendarDateRangeControl( {
271
+ id,
272
+ value,
273
+ onChange,
274
+ label,
275
+ hideLabelFromVision,
276
+ className,
277
+ }: {
278
+ id: string;
279
+ value: [ string, string ] | undefined;
280
+ onChange: ( value: any ) => void;
281
+ label: string;
282
+ hideLabelFromVision?: boolean;
283
+ className?: string;
284
+ } ) {
285
+ const [ selectedPresetId, setSelectedPresetId ] = useState< string | null >(
286
+ null
287
+ );
288
+
289
+ const selectedRange = useMemo( () => {
290
+ if ( ! value ) {
291
+ return { from: undefined, to: undefined };
292
+ }
293
+
294
+ const [ from, to ] = value;
295
+ return {
296
+ from: parseDate( from ) || undefined,
297
+ to: parseDate( to ) || undefined,
298
+ };
299
+ }, [ value ] );
300
+
301
+ const [ calendarMonth, setCalendarMonth ] = useState< Date >( () => {
302
+ return selectedRange.from || new Date();
303
+ } );
304
+
305
+ const updateDateRange = useCallback(
306
+ ( fromDate?: Date | string, toDate?: Date | string ) => {
307
+ if ( fromDate && toDate ) {
308
+ onChange( {
309
+ [ id ]: [ formatDate( fromDate ), formatDate( toDate ) ],
310
+ } );
311
+ } else if ( ! fromDate && ! toDate ) {
312
+ onChange( { [ id ]: undefined } );
313
+ }
314
+ // Do nothing if only one date is set - wait for both
315
+ },
316
+ [ id, onChange ]
317
+ );
318
+
319
+ const onSelectCalendarRange = useCallback(
320
+ (
321
+ newRange:
322
+ | { from: Date | undefined; to?: Date | undefined }
323
+ | undefined
324
+ ) => {
325
+ updateDateRange( newRange?.from, newRange?.to );
326
+ setSelectedPresetId( null );
327
+ },
328
+ [ updateDateRange ]
329
+ );
330
+
331
+ const handlePresetClick = useCallback(
332
+ ( preset: ( typeof DATE_RANGE_PRESETS )[ 0 ] ) => {
333
+ const [ startDate, endDate ] = preset.getValue();
334
+ setCalendarMonth( startDate );
335
+ updateDateRange( startDate, endDate );
336
+ setSelectedPresetId( preset.id );
337
+ },
338
+ [ updateDateRange ]
339
+ );
340
+
341
+ const handleManualDateChange = useCallback(
342
+ ( fromOrTo: 'from' | 'to', newValue?: string ) => {
343
+ const [ currentFrom, currentTo ] = value || [
344
+ undefined,
345
+ undefined,
346
+ ];
347
+ const updatedFrom = fromOrTo === 'from' ? newValue : currentFrom;
348
+ const updatedTo = fromOrTo === 'to' ? newValue : currentTo;
349
+
350
+ updateDateRange( updatedFrom, updatedTo );
351
+
352
+ if ( newValue ) {
353
+ const parsedDate = parseDate( newValue );
354
+ if ( parsedDate ) {
355
+ setCalendarMonth( parsedDate );
356
+ }
357
+ }
358
+
359
+ setSelectedPresetId( null );
360
+ },
361
+ [ value, updateDateRange ]
362
+ );
363
+
364
+ const { timezone, l10n } = getSettings();
365
+
366
+ return (
367
+ <BaseControl
368
+ __nextHasNoMarginBottom
369
+ id={ id }
370
+ className={ className }
371
+ label={ label }
372
+ hideLabelFromVision={ hideLabelFromVision }
373
+ >
374
+ <VStack spacing={ 4 }>
375
+ { /* Preset buttons */ }
376
+ <HStack spacing={ 2 } wrap justify="flex-start">
377
+ { DATE_RANGE_PRESETS.map( ( preset ) => {
378
+ const isSelected = selectedPresetId === preset.id;
379
+ return (
380
+ <Button
381
+ className="dataviews-controls__date-preset"
382
+ key={ preset.id }
383
+ variant="tertiary"
384
+ isPressed={ isSelected }
385
+ size="small"
386
+ onClick={ () => handlePresetClick( preset ) }
387
+ >
388
+ { preset.label }
389
+ </Button>
390
+ );
391
+ } ) }
392
+ <Button
393
+ className="dataviews-controls__date-preset"
394
+ variant="tertiary"
395
+ isPressed={ ! selectedPresetId }
396
+ size="small"
397
+ accessibleWhenDisabled={ false }
398
+ disabled={ !! selectedPresetId }
399
+ >
400
+ { __( 'Custom' ) }
401
+ </Button>
402
+ </HStack>
403
+
404
+ { /* Manual date range inputs */ }
405
+ <HStack spacing={ 2 }>
406
+ <InputControl
407
+ __next40pxDefaultSize
408
+ type="date"
409
+ label={ __( 'From' ) }
410
+ hideLabelFromVision
411
+ value={ value?.[ 0 ] }
412
+ onChange={ ( newValue ) =>
413
+ handleManualDateChange( 'from', newValue )
414
+ }
415
+ />
416
+ <InputControl
417
+ __next40pxDefaultSize
418
+ type="date"
419
+ label={ __( 'To' ) }
420
+ hideLabelFromVision
421
+ value={ value?.[ 1 ] }
422
+ onChange={ ( newValue ) =>
423
+ handleManualDateChange( 'to', newValue )
424
+ }
425
+ />
426
+ </HStack>
427
+
428
+ <DateRangeCalendar
429
+ style={ { width: '100%' } }
430
+ selected={ selectedRange }
431
+ onSelect={ onSelectCalendarRange }
432
+ month={ calendarMonth }
433
+ onMonthChange={ setCalendarMonth }
434
+ timeZone={ timezone.string || undefined }
435
+ weekStartsOn={ l10n.startOfWeek }
436
+ />
437
+ </VStack>
438
+ </BaseControl>
439
+ );
440
+ }
441
+
442
+ export default function DateControl< Item >( {
443
+ data,
444
+ field,
445
+ onChange,
446
+ hideLabelFromVision,
447
+ operator,
448
+ }: DataFormControlProps< Item > ) {
449
+ const { id, label } = field;
450
+ const value = field.getValue( { item: data } );
451
+
452
+ if ( operator === OPERATOR_IN_THE_PAST || operator === OPERATOR_OVER ) {
453
+ return (
454
+ <RelativeDateControl
455
+ className="dataviews-controls__date"
456
+ id={ id }
457
+ value={ value && typeof value === 'object' ? value : {} }
458
+ onChange={ onChange }
459
+ label={ label }
460
+ hideLabelFromVision={ hideLabelFromVision }
461
+ options={ TIME_UNITS_OPTIONS[ operator ] }
462
+ />
463
+ );
464
+ }
465
+
466
+ if ( operator === OPERATOR_BETWEEN ) {
467
+ let dateRangeValue: [ string, string ] | undefined;
468
+ if (
469
+ Array.isArray( value ) &&
470
+ value.length === 2 &&
471
+ value.every( ( date ) => typeof date === 'string' )
472
+ ) {
473
+ // Ensure the value is expected format
474
+ dateRangeValue = value as unknown as [ string, string ];
475
+ }
476
+
477
+ return (
478
+ <CalendarDateRangeControl
479
+ className="dataviews-controls__date"
480
+ id={ id }
481
+ value={ dateRangeValue }
482
+ onChange={ onChange }
483
+ label={ label }
484
+ hideLabelFromVision={ hideLabelFromVision }
485
+ />
486
+ );
487
+ }
488
+
489
+ return (
490
+ <CalendarDateControl
491
+ className="dataviews-controls__date"
492
+ id={ id }
493
+ value={ typeof value === 'string' ? value : undefined }
494
+ onChange={ onChange }
495
+ label={ label }
496
+ hideLabelFromVision={ hideLabelFromVision }
497
+ />
498
+ );
499
+ }