@wordpress/dataviews 1.1.0 → 2.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 (176) hide show
  1. package/CHANGELOG.md +27 -5
  2. package/README.md +33 -30
  3. package/build/add-filter.js +30 -22
  4. package/build/add-filter.js.map +1 -1
  5. package/build/bulk-actions-toolbar.js +187 -0
  6. package/build/bulk-actions-toolbar.js.map +1 -0
  7. package/build/bulk-actions.js +75 -62
  8. package/build/bulk-actions.js.map +1 -1
  9. package/build/constants.js +17 -10
  10. package/build/constants.js.map +1 -1
  11. package/build/dataviews.js +64 -50
  12. package/build/dataviews.js.map +1 -1
  13. package/build/filter-and-sort-data-view.js +2 -2
  14. package/build/filter-and-sort-data-view.js.map +1 -1
  15. package/build/filter-summary.js +106 -96
  16. package/build/filter-summary.js.map +1 -1
  17. package/build/filters.js +18 -17
  18. package/build/filters.js.map +1 -1
  19. package/build/index.js.map +1 -1
  20. package/build/item-actions.js +101 -69
  21. package/build/item-actions.js.map +1 -1
  22. package/build/layouts.js.map +1 -1
  23. package/build/lock-unlock.js.map +1 -1
  24. package/build/normalize-fields.js.map +1 -1
  25. package/build/pagination.js +66 -57
  26. package/build/pagination.js.map +1 -1
  27. package/build/reset-filters.js +9 -4
  28. package/build/reset-filters.js.map +1 -1
  29. package/build/search-widget.js +108 -89
  30. package/build/search-widget.js.map +1 -1
  31. package/build/search.js +13 -6
  32. package/build/search.js.map +1 -1
  33. package/build/single-selection-checkbox.js +6 -2
  34. package/build/single-selection-checkbox.js.map +1 -1
  35. package/build/types.js.map +1 -1
  36. package/build/utils.js +3 -15
  37. package/build/utils.js.map +1 -1
  38. package/build/view-actions.js +168 -120
  39. package/build/view-actions.js.map +1 -1
  40. package/build/view-grid.js +119 -106
  41. package/build/view-grid.js.map +1 -1
  42. package/build/view-list.js +217 -83
  43. package/build/view-list.js.map +1 -1
  44. package/build/view-table.js +227 -199
  45. package/build/view-table.js.map +1 -1
  46. package/build-module/add-filter.js +30 -22
  47. package/build-module/add-filter.js.map +1 -1
  48. package/build-module/bulk-actions-toolbar.js +182 -0
  49. package/build-module/bulk-actions-toolbar.js.map +1 -0
  50. package/build-module/bulk-actions.js +77 -62
  51. package/build-module/bulk-actions.js.map +1 -1
  52. package/build-module/constants.js +16 -9
  53. package/build-module/constants.js.map +1 -1
  54. package/build-module/dataviews.js +65 -50
  55. package/build-module/dataviews.js.map +1 -1
  56. package/build-module/filter-and-sort-data-view.js +2 -2
  57. package/build-module/filter-and-sort-data-view.js.map +1 -1
  58. package/build-module/filter-summary.js +107 -97
  59. package/build-module/filter-summary.js.map +1 -1
  60. package/build-module/filters.js +18 -17
  61. package/build-module/filters.js.map +1 -1
  62. package/build-module/index.js.map +1 -1
  63. package/build-module/item-actions.js +102 -71
  64. package/build-module/item-actions.js.map +1 -1
  65. package/build-module/layouts.js.map +1 -1
  66. package/build-module/lock-unlock.js.map +1 -1
  67. package/build-module/normalize-fields.js.map +1 -1
  68. package/build-module/pagination.js +67 -57
  69. package/build-module/pagination.js.map +1 -1
  70. package/build-module/reset-filters.js +9 -4
  71. package/build-module/reset-filters.js.map +1 -1
  72. package/build-module/search-widget.js +109 -89
  73. package/build-module/search-widget.js.map +1 -1
  74. package/build-module/search.js +13 -6
  75. package/build-module/search.js.map +1 -1
  76. package/build-module/single-selection-checkbox.js +6 -2
  77. package/build-module/single-selection-checkbox.js.map +1 -1
  78. package/build-module/types.js.map +1 -1
  79. package/build-module/utils.js +2 -13
  80. package/build-module/utils.js.map +1 -1
  81. package/build-module/view-actions.js +170 -121
  82. package/build-module/view-actions.js.map +1 -1
  83. package/build-module/view-grid.js +121 -106
  84. package/build-module/view-grid.js.map +1 -1
  85. package/build-module/view-list.js +219 -85
  86. package/build-module/view-list.js.map +1 -1
  87. package/build-module/view-table.js +230 -201
  88. package/build-module/view-table.js.map +1 -1
  89. package/build-style/style-rtl.css +168 -44
  90. package/build-style/style.css +168 -44
  91. package/build-types/add-filter.d.ts +11 -0
  92. package/build-types/add-filter.d.ts.map +1 -0
  93. package/build-types/bulk-actions-toolbar.d.ts +12 -0
  94. package/build-types/bulk-actions-toolbar.d.ts.map +1 -0
  95. package/build-types/bulk-actions.d.ts +14 -0
  96. package/build-types/bulk-actions.d.ts.map +1 -0
  97. package/build-types/constants.d.ts +19 -32
  98. package/build-types/constants.d.ts.map +1 -1
  99. package/build-types/dataviews.d.ts +22 -0
  100. package/build-types/dataviews.d.ts.map +1 -0
  101. package/build-types/filter-and-sort-data-view.d.ts +3 -3
  102. package/build-types/filter-and-sort-data-view.d.ts.map +1 -1
  103. package/build-types/filter-summary.d.ts +14 -0
  104. package/build-types/filter-summary.d.ts.map +1 -0
  105. package/build-types/filters.d.ts +13 -0
  106. package/build-types/filters.d.ts.map +1 -0
  107. package/build-types/index.d.ts +4 -0
  108. package/build-types/index.d.ts.map +1 -0
  109. package/build-types/item-actions.d.ts +35 -0
  110. package/build-types/item-actions.d.ts.map +1 -0
  111. package/build-types/layouts.d.ts +24 -0
  112. package/build-types/layouts.d.ts.map +1 -0
  113. package/build-types/lock-unlock.d.ts +2 -0
  114. package/build-types/lock-unlock.d.ts.map +1 -0
  115. package/build-types/normalize-fields.d.ts +2 -2
  116. package/build-types/normalize-fields.d.ts.map +1 -1
  117. package/build-types/pagination.d.ts +16 -0
  118. package/build-types/pagination.d.ts.map +1 -0
  119. package/build-types/reset-filters.d.ts +13 -0
  120. package/build-types/reset-filters.d.ts.map +1 -0
  121. package/build-types/search-widget.d.ts +10 -0
  122. package/build-types/search-widget.d.ts.map +1 -0
  123. package/build-types/search.d.ts +13 -0
  124. package/build-types/search.d.ts.map +1 -0
  125. package/build-types/single-selection-checkbox.d.ts +17 -0
  126. package/build-types/single-selection-checkbox.d.ts.map +1 -0
  127. package/build-types/stories/fixtures.d.ts +114 -0
  128. package/build-types/stories/fixtures.d.ts.map +1 -0
  129. package/build-types/stories/index.story.d.ts +15 -0
  130. package/build-types/stories/index.story.d.ts.map +1 -0
  131. package/build-types/types.d.ts +221 -21
  132. package/build-types/types.d.ts.map +1 -1
  133. package/build-types/utils.d.ts +3 -0
  134. package/build-types/utils.d.ts.map +1 -0
  135. package/build-types/view-actions.d.ts +12 -0
  136. package/build-types/view-actions.d.ts.map +1 -0
  137. package/build-types/view-grid.d.ts +4 -0
  138. package/build-types/view-grid.d.ts.map +1 -0
  139. package/build-types/view-list.d.ts +4 -0
  140. package/build-types/view-list.d.ts.map +1 -0
  141. package/build-types/view-table.d.ts +5 -0
  142. package/build-types/view-table.d.ts.map +1 -0
  143. package/package.json +12 -13
  144. package/src/{add-filter.js → add-filter.tsx} +17 -1
  145. package/src/bulk-actions-toolbar.tsx +272 -0
  146. package/src/{bulk-actions.js → bulk-actions.tsx} +77 -17
  147. package/src/constants.ts +12 -5
  148. package/src/{dataviews.js → dataviews.tsx} +54 -14
  149. package/src/filter-and-sort-data-view.ts +13 -8
  150. package/src/{filter-summary.js → filter-summary.tsx} +38 -9
  151. package/src/{filters.js → filters.tsx} +18 -6
  152. package/src/{item-actions.js → item-actions.tsx} +119 -30
  153. package/src/normalize-fields.ts +4 -2
  154. package/src/{pagination.js → pagination.tsx} +29 -8
  155. package/src/{reset-filters.js → reset-filters.tsx} +17 -2
  156. package/src/{search-widget.js → search-widget.tsx} +27 -7
  157. package/src/{search.js → search.tsx} +22 -5
  158. package/src/{single-selection-checkbox.js → single-selection-checkbox.tsx} +17 -2
  159. package/src/style.scss +166 -43
  160. package/src/types.ts +286 -21
  161. package/src/{utils.js → utils.ts} +5 -13
  162. package/src/{view-actions.js → view-actions.tsx} +105 -49
  163. package/src/{view-grid.js → view-grid.tsx} +31 -18
  164. package/src/view-list.tsx +410 -0
  165. package/src/{view-table.js → view-table.tsx} +99 -40
  166. package/tsconfig.json +3 -4
  167. package/tsconfig.tsbuildinfo +1 -1
  168. package/build/dropdown-menu-helper.js +0 -71
  169. package/build/dropdown-menu-helper.js.map +0 -1
  170. package/build-module/dropdown-menu-helper.js +0 -64
  171. package/build-module/dropdown-menu-helper.js.map +0 -1
  172. package/src/dropdown-menu-helper.js +0 -61
  173. package/src/view-list.js +0 -207
  174. /package/src/{index.js → index.ts} +0 -0
  175. /package/src/{layouts.js → layouts.ts} +0 -0
  176. /package/src/{lock-unlock.js → lock-unlock.ts} +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wordpress/dataviews",
3
- "version": "1.1.0",
3
+ "version": "2.0.0",
4
4
  "description": "DataViews is a component that provides an API to render datasets using different types of layouts (table, grid, list, etc.).",
5
5
  "author": "The WordPress Contributors",
6
6
  "license": "GPL-2.0-or-later",
@@ -19,7 +19,8 @@
19
19
  "url": "https://github.com/WordPress/gutenberg/issues"
20
20
  },
21
21
  "engines": {
22
- "node": ">=12"
22
+ "node": ">=18.12.0",
23
+ "npm": ">=8.19.2"
23
24
  },
24
25
  "main": "build/index.js",
25
26
  "module": "build-module/index.js",
@@ -29,16 +30,14 @@
29
30
  "dependencies": {
30
31
  "@ariakit/react": "^0.3.12",
31
32
  "@babel/runtime": "^7.16.0",
32
- "@wordpress/a11y": "^3.57.0",
33
- "@wordpress/components": "^27.5.0",
34
- "@wordpress/compose": "^6.34.0",
35
- "@wordpress/element": "^5.34.0",
36
- "@wordpress/i18n": "^4.57.0",
37
- "@wordpress/icons": "^9.48.0",
38
- "@wordpress/keycodes": "^3.57.0",
39
- "@wordpress/primitives": "^3.55.0",
40
- "@wordpress/private-apis": "^0.39.0",
41
- "classnames": "^2.3.1",
33
+ "@wordpress/components": "^28.0.0",
34
+ "@wordpress/compose": "^7.0.0",
35
+ "@wordpress/element": "^6.0.0",
36
+ "@wordpress/i18n": "^5.0.0",
37
+ "@wordpress/icons": "^10.0.0",
38
+ "@wordpress/primitives": "^4.0.0",
39
+ "@wordpress/private-apis": "^1.0.0",
40
+ "clsx": "^2.1.1",
42
41
  "remove-accents": "^0.5.0"
43
42
  },
44
43
  "peerDependencies": {
@@ -47,5 +46,5 @@
47
46
  "publishConfig": {
48
47
  "access": "public"
49
48
  },
50
- "gitHead": "581d8a5580dba8f600b7268d51eb554771ae482c"
49
+ "gitHead": "2f30cddff15723ac7017fd009fc5913b7b419400"
51
50
  }
@@ -1,3 +1,8 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import type { Ref } from 'react';
5
+
1
6
  /**
2
7
  * WordPress dependencies
3
8
  */
@@ -12,6 +17,7 @@ import { forwardRef } from '@wordpress/element';
12
17
  * Internal dependencies
13
18
  */
14
19
  import { unlock } from './lock-unlock';
20
+ import type { NormalizedFilter, View } from './types';
15
21
 
16
22
  const {
17
23
  DropdownMenuV2: DropdownMenu,
@@ -19,7 +25,17 @@ const {
19
25
  DropdownMenuItemLabelV2: DropdownMenuItemLabel,
20
26
  } = unlock( componentsPrivateApis );
21
27
 
22
- function AddFilter( { filters, view, onChangeView, setOpenedFilter }, ref ) {
28
+ interface AddFilterProps {
29
+ filters: NormalizedFilter[];
30
+ view: View;
31
+ onChangeView: ( view: View ) => void;
32
+ setOpenedFilter: ( filter: string | null ) => void;
33
+ }
34
+
35
+ function AddFilter(
36
+ { filters, view, onChangeView, setOpenedFilter }: AddFilterProps,
37
+ ref: Ref< HTMLButtonElement >
38
+ ) {
23
39
  if ( ! filters.length || filters.every( ( { isPrimary } ) => isPrimary ) ) {
24
40
  return null;
25
41
  }
@@ -0,0 +1,272 @@
1
+ /**
2
+ * WordPress dependencies
3
+ */
4
+ import {
5
+ ToolbarButton,
6
+ Toolbar,
7
+ ToolbarGroup,
8
+ __unstableMotion as motion,
9
+ __unstableAnimatePresence as AnimatePresence,
10
+ } from '@wordpress/components';
11
+ import { useMemo, useState, useRef } from '@wordpress/element';
12
+ import { _n, sprintf, __ } from '@wordpress/i18n';
13
+ import { closeSmall } from '@wordpress/icons';
14
+ import { useReducedMotion } from '@wordpress/compose';
15
+
16
+ /**
17
+ * Internal dependencies
18
+ */
19
+ import { ActionWithModal } from './item-actions';
20
+ import type { Action, AnyItem } from './types';
21
+ import type { ActionTriggerProps } from './item-actions';
22
+
23
+ interface ActionButtonProps< Item extends AnyItem > {
24
+ action: Action< Item >;
25
+ selectedItems: Item[];
26
+ actionInProgress: string | null;
27
+ setActionInProgress: ( actionId: string | null ) => void;
28
+ }
29
+
30
+ interface ToolbarContentProps< Item extends AnyItem > {
31
+ selection: string[];
32
+ actionsToShow: Action< Item >[];
33
+ selectedItems: Item[];
34
+ onSelectionChange: ( selection: Item[] ) => void;
35
+ }
36
+
37
+ interface BulkActionsToolbarProps< Item extends AnyItem > {
38
+ data: Item[];
39
+ selection: string[];
40
+ actions: Action< Item >[];
41
+ onSelectionChange: ( selection: Item[] ) => void;
42
+ getItemId: ( item: Item ) => string;
43
+ }
44
+
45
+ const SNACKBAR_VARIANTS = {
46
+ init: {
47
+ bottom: -48,
48
+ },
49
+ open: {
50
+ bottom: 24,
51
+ transition: {
52
+ bottom: { type: 'tween', duration: 0.2, ease: [ 0, 0, 0.2, 1 ] },
53
+ },
54
+ },
55
+ exit: {
56
+ opacity: 0,
57
+ bottom: 24,
58
+ transition: {
59
+ opacity: { type: 'tween', duration: 0.2, ease: [ 0, 0, 0.2, 1 ] },
60
+ },
61
+ },
62
+ };
63
+
64
+ function ActionTrigger< Item extends AnyItem >( {
65
+ action,
66
+ onClick,
67
+ isBusy,
68
+ items,
69
+ }: ActionTriggerProps< Item > ) {
70
+ const label =
71
+ typeof action.label === 'string' ? action.label : action.label( items );
72
+ return (
73
+ <ToolbarButton
74
+ disabled={ isBusy }
75
+ label={ label }
76
+ icon={ action.icon }
77
+ isDestructive={ action.isDestructive }
78
+ size="compact"
79
+ onClick={ onClick }
80
+ isBusy={ isBusy }
81
+ __experimentalIsFocusable
82
+ tooltipPosition="top"
83
+ />
84
+ );
85
+ }
86
+
87
+ const EMPTY_ARRAY: [] = [];
88
+
89
+ function ActionButton< Item extends AnyItem >( {
90
+ action,
91
+ selectedItems,
92
+ actionInProgress,
93
+ setActionInProgress,
94
+ }: ActionButtonProps< Item > ) {
95
+ const selectedEligibleItems = useMemo( () => {
96
+ return selectedItems.filter( ( item ) => {
97
+ return ! action.isEligible || action.isEligible( item );
98
+ } );
99
+ }, [ action, selectedItems ] );
100
+ if ( 'RenderModal' in action ) {
101
+ return (
102
+ <ActionWithModal
103
+ key={ action.id }
104
+ action={ action }
105
+ items={ selectedEligibleItems }
106
+ ActionTrigger={ ActionTrigger }
107
+ />
108
+ );
109
+ }
110
+ return (
111
+ <ActionTrigger
112
+ key={ action.id }
113
+ action={ action }
114
+ onClick={ () => {
115
+ setActionInProgress( action.id );
116
+ action.callback( selectedItems );
117
+ } }
118
+ items={ selectedEligibleItems }
119
+ isBusy={ actionInProgress === action.id }
120
+ />
121
+ );
122
+ }
123
+
124
+ function renderToolbarContent< Item extends AnyItem >(
125
+ selection: string[],
126
+ actionsToShow: Action< Item >[],
127
+ selectedItems: Item[],
128
+ actionInProgress: string | null,
129
+ setActionInProgress: ( actionId: string | null ) => void,
130
+ onSelectionChange: ( selection: Item[] ) => void
131
+ ) {
132
+ return (
133
+ <>
134
+ <ToolbarGroup>
135
+ <div className="dataviews-bulk-actions__selection-count">
136
+ { selection.length === 1
137
+ ? __( '1 item selected' )
138
+ : sprintf(
139
+ // translators: %s: Total number of selected items.
140
+ _n(
141
+ '%s item selected',
142
+ '%s items selected',
143
+ selection.length
144
+ ),
145
+ selection.length
146
+ ) }
147
+ </div>
148
+ </ToolbarGroup>
149
+ <ToolbarGroup>
150
+ { actionsToShow.map( ( action ) => {
151
+ return (
152
+ <ActionButton
153
+ key={ action.id }
154
+ action={ action }
155
+ selectedItems={ selectedItems }
156
+ actionInProgress={ actionInProgress }
157
+ setActionInProgress={ setActionInProgress }
158
+ />
159
+ );
160
+ } ) }
161
+ </ToolbarGroup>
162
+ <ToolbarGroup>
163
+ <ToolbarButton
164
+ icon={ closeSmall }
165
+ showTooltip
166
+ tooltipPosition="top"
167
+ label={ __( 'Cancel' ) }
168
+ disabled={ !! actionInProgress }
169
+ onClick={ () => {
170
+ onSelectionChange( EMPTY_ARRAY );
171
+ } }
172
+ />
173
+ </ToolbarGroup>
174
+ </>
175
+ );
176
+ }
177
+
178
+ function ToolbarContent< Item extends AnyItem >( {
179
+ selection,
180
+ actionsToShow,
181
+ selectedItems,
182
+ onSelectionChange,
183
+ }: ToolbarContentProps< Item > ) {
184
+ const [ actionInProgress, setActionInProgress ] = useState< string | null >(
185
+ null
186
+ );
187
+ const buttons = useRef< JSX.Element | null >( null );
188
+ if ( ! actionInProgress ) {
189
+ if ( buttons.current ) {
190
+ buttons.current = null;
191
+ }
192
+ return renderToolbarContent(
193
+ selection,
194
+ actionsToShow,
195
+ selectedItems,
196
+ actionInProgress,
197
+ setActionInProgress,
198
+ onSelectionChange
199
+ );
200
+ } else if ( ! buttons.current ) {
201
+ buttons.current = renderToolbarContent(
202
+ selection,
203
+ actionsToShow,
204
+ selectedItems,
205
+ actionInProgress,
206
+ setActionInProgress,
207
+ onSelectionChange
208
+ );
209
+ }
210
+ return buttons.current;
211
+ }
212
+
213
+ export default function BulkActionsToolbar< Item extends AnyItem >( {
214
+ data,
215
+ selection,
216
+ actions = EMPTY_ARRAY,
217
+ onSelectionChange,
218
+ getItemId,
219
+ }: BulkActionsToolbarProps< Item > ) {
220
+ const isReducedMotion = useReducedMotion();
221
+ const selectedItems = useMemo( () => {
222
+ return data.filter( ( item ) =>
223
+ selection.includes( getItemId( item ) )
224
+ );
225
+ }, [ selection, data, getItemId ] );
226
+
227
+ const actionsToShow = useMemo(
228
+ () =>
229
+ actions.filter( ( action ) => {
230
+ return (
231
+ action.supportsBulk &&
232
+ action.icon &&
233
+ selectedItems.some(
234
+ ( item ) =>
235
+ ! action.isEligible || action.isEligible( item )
236
+ )
237
+ );
238
+ } ),
239
+ [ actions, selectedItems ]
240
+ );
241
+
242
+ if (
243
+ ( selection && selection.length === 0 ) ||
244
+ actionsToShow.length === 0
245
+ ) {
246
+ return null;
247
+ }
248
+
249
+ return (
250
+ <AnimatePresence>
251
+ <motion.div
252
+ layout={ ! isReducedMotion } // See https://www.framer.com/docs/animation/#layout-animations
253
+ initial="init"
254
+ animate="open"
255
+ exit="exit"
256
+ variants={ isReducedMotion ? undefined : SNACKBAR_VARIANTS }
257
+ className="dataviews-bulk-actions"
258
+ >
259
+ <Toolbar label={ __( 'Bulk actions' ) }>
260
+ <div className="dataviews-bulk-actions-toolbar-wrapper">
261
+ <ToolbarContent
262
+ selection={ selection }
263
+ actionsToShow={ actionsToShow }
264
+ selectedItems={ selectedItems }
265
+ onSelectionChange={ onSelectionChange }
266
+ />
267
+ </div>
268
+ </Toolbar>
269
+ </motion.div>
270
+ </AnimatePresence>
271
+ );
272
+ }
@@ -13,6 +13,7 @@ import { useMemo, useState, useCallback, useEffect } from '@wordpress/element';
13
13
  * Internal dependencies
14
14
  */
15
15
  import { unlock } from './lock-unlock';
16
+ import type { Action, ActionModal, AnyItem } from './types';
16
17
 
17
18
  const {
18
19
  DropdownMenuV2: DropdownMenu,
@@ -21,40 +22,85 @@ const {
21
22
  DropdownMenuSeparatorV2: DropdownMenuSeparator,
22
23
  } = unlock( componentsPrivateApis );
23
24
 
24
- export function useHasAPossibleBulkAction( actions, item ) {
25
+ interface ActionWithModalProps< Item extends AnyItem > {
26
+ action: ActionModal< Item >;
27
+ selectedItems: Item[];
28
+ setActionWithModal: ( action?: ActionModal< Item > ) => void;
29
+ onMenuOpenChange: ( isOpen: boolean ) => void;
30
+ }
31
+
32
+ interface BulkActionsItemProps< Item extends AnyItem > {
33
+ action: Action< Item >;
34
+ selectedItems: Item[];
35
+ setActionWithModal: ( action?: ActionModal< Item > ) => void;
36
+ }
37
+
38
+ interface ActionsMenuGroupProps< Item extends AnyItem > {
39
+ actions: Action< Item >[];
40
+ selectedItems: Item[];
41
+ setActionWithModal: ( action?: ActionModal< Item > ) => void;
42
+ }
43
+
44
+ interface BulkActionsProps< Item extends AnyItem > {
45
+ data: Item[];
46
+ actions: Action< Item >[];
47
+ selection: string[];
48
+ onSelectionChange: ( selection: Item[] ) => void;
49
+ getItemId: ( item: Item ) => string;
50
+ }
51
+
52
+ export function useHasAPossibleBulkAction< Item extends AnyItem >(
53
+ actions: Action< Item >[],
54
+ item: Item
55
+ ) {
25
56
  return useMemo( () => {
26
57
  return actions.some( ( action ) => {
27
- return action.supportsBulk && action.isEligible( item );
58
+ return (
59
+ action.supportsBulk &&
60
+ ( ! action.isEligible || action.isEligible( item ) )
61
+ );
28
62
  } );
29
63
  }, [ actions, item ] );
30
64
  }
31
65
 
32
- export function useSomeItemHasAPossibleBulkAction( actions, data ) {
66
+ export function useSomeItemHasAPossibleBulkAction< Item extends AnyItem >(
67
+ actions: Action< Item >[],
68
+ data: Item[]
69
+ ) {
33
70
  return useMemo( () => {
34
71
  return data.some( ( item ) => {
35
72
  return actions.some( ( action ) => {
36
- return action.supportsBulk && action.isEligible( item );
73
+ return (
74
+ action.supportsBulk &&
75
+ ( ! action.isEligible || action.isEligible( item ) )
76
+ );
37
77
  } );
38
78
  } );
39
79
  }, [ actions, data ] );
40
80
  }
41
81
 
42
- function ActionWithModal( {
82
+ function ActionWithModal< Item extends AnyItem >( {
43
83
  action,
44
84
  selectedItems,
45
85
  setActionWithModal,
46
86
  onMenuOpenChange,
47
- } ) {
87
+ }: ActionWithModalProps< Item > ) {
48
88
  const eligibleItems = useMemo( () => {
49
- return selectedItems.filter( ( item ) => action.isEligible( item ) );
89
+ return selectedItems.filter(
90
+ ( item ) => ! action.isEligible || action.isEligible( item )
91
+ );
50
92
  }, [ action, selectedItems ] );
51
93
  const { RenderModal, hideModalHeader } = action;
52
94
  const onCloseModal = useCallback( () => {
53
95
  setActionWithModal( undefined );
54
96
  }, [ setActionWithModal ] );
97
+ const label =
98
+ typeof action.label === 'string'
99
+ ? action.label
100
+ : action.label( selectedItems );
55
101
  return (
56
102
  <Modal
57
- title={ ! hideModalHeader && action.label }
103
+ title={ ! hideModalHeader ? label : undefined }
58
104
  __experimentalHideHeader={ !! hideModalHeader }
59
105
  onRequestClose={ onCloseModal }
60
106
  overlayClassName="dataviews-action-modal"
@@ -62,18 +108,24 @@ function ActionWithModal( {
62
108
  <RenderModal
63
109
  items={ eligibleItems }
64
110
  closeModal={ onCloseModal }
65
- onPerform={ () => onMenuOpenChange( false ) }
111
+ onActionPerformed={ () => onMenuOpenChange( false ) }
66
112
  />
67
113
  </Modal>
68
114
  );
69
115
  }
70
116
 
71
- function BulkActionItem( { action, selectedItems, setActionWithModal } ) {
117
+ function BulkActionItem< Item extends AnyItem >( {
118
+ action,
119
+ selectedItems,
120
+ setActionWithModal,
121
+ }: BulkActionsItemProps< Item > ) {
72
122
  const eligibleItems = useMemo( () => {
73
- return selectedItems.filter( ( item ) => action.isEligible( item ) );
123
+ return selectedItems.filter(
124
+ ( item ) => ! action.isEligible || action.isEligible( item )
125
+ );
74
126
  }, [ action, selectedItems ] );
75
127
 
76
- const shouldShowModal = !! action.RenderModal;
128
+ const shouldShowModal = 'RenderModal' in action;
77
129
 
78
130
  return (
79
131
  <DropdownMenuItem
@@ -96,7 +148,11 @@ function BulkActionItem( { action, selectedItems, setActionWithModal } ) {
96
148
  );
97
149
  }
98
150
 
99
- function ActionsMenuGroup( { actions, selectedItems, setActionWithModal } ) {
151
+ function ActionsMenuGroup< Item extends AnyItem >( {
152
+ actions,
153
+ selectedItems,
154
+ setActionWithModal,
155
+ }: ActionsMenuGroupProps< Item > ) {
100
156
  return (
101
157
  <>
102
158
  <DropdownMenuGroup>
@@ -114,22 +170,26 @@ function ActionsMenuGroup( { actions, selectedItems, setActionWithModal } ) {
114
170
  );
115
171
  }
116
172
 
117
- export default function BulkActions( {
173
+ export default function BulkActions< Item extends AnyItem >( {
118
174
  data,
119
175
  actions,
120
176
  selection,
121
177
  onSelectionChange,
122
178
  getItemId,
123
- } ) {
179
+ }: BulkActionsProps< Item > ) {
124
180
  const bulkActions = useMemo(
125
181
  () => actions.filter( ( action ) => action.supportsBulk ),
126
182
  [ actions ]
127
183
  );
128
184
  const [ isMenuOpen, onMenuOpenChange ] = useState( false );
129
- const [ actionWithModal, setActionWithModal ] = useState();
185
+ const [ actionWithModal, setActionWithModal ] = useState<
186
+ ActionModal< Item > | undefined
187
+ >();
130
188
  const selectableItems = useMemo( () => {
131
189
  return data.filter( ( item ) => {
132
- return bulkActions.some( ( action ) => action.isEligible( item ) );
190
+ return bulkActions.some(
191
+ ( action ) => ! action.isEligible || action.isEligible( item )
192
+ );
133
193
  } );
134
194
  }, [ data, bulkActions ] );
135
195
 
package/src/constants.ts CHANGED
@@ -3,6 +3,11 @@
3
3
  */
4
4
  import { __ } from '@wordpress/i18n';
5
5
 
6
+ /**
7
+ * Internal dependencies
8
+ */
9
+ import type { Operator } from './types';
10
+
6
11
  // Filter operators.
7
12
  export const OPERATOR_IS = 'is';
8
13
  export const OPERATOR_IS_NOT = 'isNot';
@@ -19,7 +24,7 @@ export const ALL_OPERATORS = [
19
24
  OPERATOR_IS_ALL,
20
25
  OPERATOR_IS_NOT_ALL,
21
26
  ];
22
- export const OPERATORS = {
27
+ export const OPERATORS: Record< Operator, { key: string; label: string } > = {
23
28
  [ OPERATOR_IS ]: {
24
29
  key: 'is-filter',
25
30
  label: __( 'Is' ),
@@ -46,10 +51,12 @@ export const OPERATORS = {
46
51
  },
47
52
  };
48
53
 
49
- // Sorting
50
- export const SORTING_DIRECTIONS = {
51
- asc: { label: __( 'Sort ascending' ) },
52
- desc: { label: __( 'Sort descending' ) },
54
+ export const SORTING_DIRECTIONS = [ 'asc', 'desc' ] as const;
55
+ export const sortArrows = { asc: '↑', desc: '↓' };
56
+ export const sortValues = { asc: 'ascending', desc: 'descending' } as const;
57
+ export const sortLabels = {
58
+ asc: __( 'Sort ascending' ),
59
+ desc: __( 'Sort descending' ),
53
60
  };
54
61
 
55
62
  // View layouts.
@@ -1,3 +1,8 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import type { ComponentType } from 'react';
5
+
1
6
  /**
2
7
  * WordPress dependencies
3
8
  */
@@ -15,21 +20,47 @@ import { LAYOUT_TABLE, LAYOUT_GRID } from './constants';
15
20
  import { VIEW_LAYOUTS } from './layouts';
16
21
  import BulkActions from './bulk-actions';
17
22
  import { normalizeFields } from './normalize-fields';
23
+ import BulkActionsToolbar from './bulk-actions-toolbar';
24
+ import type { Action, AnyItem, Field, View, ViewBaseProps } from './types';
18
25
 
19
- const defaultGetItemId = ( item ) => item.id;
26
+ interface DataViewsProps< Item extends AnyItem > {
27
+ view: View;
28
+ onChangeView: ( view: View ) => void;
29
+ fields: Field< Item >[];
30
+ search?: boolean;
31
+ searchLabel?: string;
32
+ actions?: Action< Item >[];
33
+ data: Item[];
34
+ getItemId?: ( item: Item ) => string;
35
+ isLoading?: boolean;
36
+ paginationInfo: {
37
+ totalItems: number;
38
+ totalPages: number;
39
+ };
40
+ supportedLayouts: string[];
41
+ onSelectionChange?: ( items: Item[] ) => void;
42
+ }
43
+
44
+ const defaultGetItemId = ( item: AnyItem ) => item.id;
20
45
  const defaultOnSelectionChange = () => {};
21
46
 
22
- function useSomeItemHasAPossibleBulkAction( actions, data ) {
47
+ function useSomeItemHasAPossibleBulkAction< Item extends AnyItem >(
48
+ actions: Action< Item >[],
49
+ data: Item[]
50
+ ) {
23
51
  return useMemo( () => {
24
52
  return data.some( ( item ) => {
25
53
  return actions.some( ( action ) => {
26
- return action.supportsBulk && action.isEligible( item );
54
+ return (
55
+ action.supportsBulk &&
56
+ ( ! action.isEligible || action.isEligible( item ) )
57
+ );
27
58
  } );
28
59
  } );
29
60
  }, [ actions, data ] );
30
61
  }
31
62
 
32
- export default function DataViews( {
63
+ export default function DataViews< Item extends AnyItem >( {
33
64
  view,
34
65
  onChangeView,
35
66
  fields,
@@ -42,9 +73,9 @@ export default function DataViews( {
42
73
  paginationInfo,
43
74
  supportedLayouts,
44
75
  onSelectionChange = defaultOnSelectionChange,
45
- } ) {
46
- const [ selection, setSelection ] = useState( [] );
47
- const [ openedFilter, setOpenedFilter ] = useState( null );
76
+ }: DataViewsProps< Item > ) {
77
+ const [ selection, setSelection ] = useState< string[] >( [] );
78
+ const [ openedFilter, setOpenedFilter ] = useState< string | null >( null );
48
79
 
49
80
  useEffect( () => {
50
81
  if (
@@ -66,16 +97,15 @@ export default function DataViews( {
66
97
  }, [ selection, data, getItemId, onSelectionChange ] );
67
98
 
68
99
  const onSetSelection = useCallback(
69
- ( items ) => {
100
+ ( items: Item[] ) => {
70
101
  setSelection( items.map( ( item ) => getItemId( item ) ) );
71
102
  onSelectionChange( items );
72
103
  },
73
104
  [ setSelection, getItemId, onSelectionChange ]
74
105
  );
75
106
 
76
- const ViewComponent = VIEW_LAYOUTS.find(
77
- ( v ) => v.type === view.type
78
- ).component;
107
+ const ViewComponent = VIEW_LAYOUTS.find( ( v ) => v.type === view.type )
108
+ ?.component as ComponentType< ViewBaseProps< Item > >;
79
109
  const _fields = useMemo( () => normalizeFields( fields ), [ fields ] );
80
110
 
81
111
  const hasPossibleBulkAction = useSomeItemHasAPossibleBulkAction(
@@ -127,22 +157,32 @@ export default function DataViews( {
127
157
  />
128
158
  </HStack>
129
159
  <ViewComponent
130
- fields={ _fields }
131
- view={ view }
132
- onChangeView={ onChangeView }
133
160
  actions={ actions }
134
161
  data={ data }
162
+ fields={ _fields }
135
163
  getItemId={ getItemId }
136
164
  isLoading={ isLoading }
165
+ onChangeView={ onChangeView }
137
166
  onSelectionChange={ onSetSelection }
138
167
  selection={ selection }
139
168
  setOpenedFilter={ setOpenedFilter }
169
+ view={ view }
140
170
  />
141
171
  <Pagination
142
172
  view={ view }
143
173
  onChangeView={ onChangeView }
144
174
  paginationInfo={ paginationInfo }
145
175
  />
176
+ { [ LAYOUT_TABLE, LAYOUT_GRID ].includes( view.type ) &&
177
+ hasPossibleBulkAction && (
178
+ <BulkActionsToolbar
179
+ data={ data }
180
+ actions={ actions }
181
+ selection={ selection }
182
+ onSelectionChange={ onSetSelection }
183
+ getItemId={ getItemId }
184
+ />
185
+ ) }
146
186
  </div>
147
187
  );
148
188
  }