@wordpress/dataviews 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/LICENSE.md +788 -0
  3. package/README.md +224 -0
  4. package/build/add-filter.js +90 -0
  5. package/build/add-filter.js.map +1 -0
  6. package/build/constants.js +55 -0
  7. package/build/constants.js.map +1 -0
  8. package/build/dataviews.js +93 -0
  9. package/build/dataviews.js.map +1 -0
  10. package/build/filter-summary.js +137 -0
  11. package/build/filter-summary.js.map +1 -0
  12. package/build/filters.js +75 -0
  13. package/build/filters.js.map +1 -0
  14. package/build/index.js +21 -0
  15. package/build/index.js.map +1 -0
  16. package/build/item-actions.js +185 -0
  17. package/build/item-actions.js.map +1 -0
  18. package/build/lock-unlock.js +18 -0
  19. package/build/lock-unlock.js.map +1 -0
  20. package/build/pagination.js +123 -0
  21. package/build/pagination.js.map +1 -0
  22. package/build/reset-filters.js +33 -0
  23. package/build/reset-filters.js.map +1 -0
  24. package/build/search.js +46 -0
  25. package/build/search.js.map +1 -0
  26. package/build/view-actions.js +223 -0
  27. package/build/view-actions.js.map +1 -0
  28. package/build/view-grid.js +80 -0
  29. package/build/view-grid.js.map +1 -0
  30. package/build/view-list.js +83 -0
  31. package/build/view-list.js.map +1 -0
  32. package/build/view-table.js +286 -0
  33. package/build/view-table.js.map +1 -0
  34. package/build-module/add-filter.js +83 -0
  35. package/build-module/add-filter.js.map +1 -0
  36. package/build-module/constants.js +41 -0
  37. package/build-module/constants.js.map +1 -0
  38. package/build-module/dataviews.js +85 -0
  39. package/build-module/dataviews.js.map +1 -0
  40. package/build-module/filter-summary.js +130 -0
  41. package/build-module/filter-summary.js.map +1 -0
  42. package/build-module/filters.js +67 -0
  43. package/build-module/filters.js.map +1 -0
  44. package/build-module/index.js +3 -0
  45. package/build-module/index.js.map +1 -0
  46. package/build-module/item-actions.js +178 -0
  47. package/build-module/item-actions.js.map +1 -0
  48. package/build-module/lock-unlock.js +9 -0
  49. package/build-module/lock-unlock.js.map +1 -0
  50. package/build-module/pagination.js +115 -0
  51. package/build-module/pagination.js.map +1 -0
  52. package/build-module/reset-filters.js +26 -0
  53. package/build-module/reset-filters.js.map +1 -0
  54. package/build-module/search.js +39 -0
  55. package/build-module/search.js.map +1 -0
  56. package/build-module/view-actions.js +216 -0
  57. package/build-module/view-actions.js.map +1 -0
  58. package/build-module/view-grid.js +72 -0
  59. package/build-module/view-grid.js.map +1 -0
  60. package/build-module/view-list.js +75 -0
  61. package/build-module/view-list.js.map +1 -0
  62. package/build-module/view-table.js +277 -0
  63. package/build-module/view-table.js.map +1 -0
  64. package/build-style/style-rtl.css +325 -0
  65. package/build-style/style.css +325 -0
  66. package/package.json +49 -0
  67. package/src/add-filter.js +106 -0
  68. package/src/constants.js +50 -0
  69. package/src/dataviews.js +99 -0
  70. package/src/filter-summary.js +221 -0
  71. package/src/filters.js +84 -0
  72. package/src/index.js +2 -0
  73. package/src/item-actions.js +211 -0
  74. package/src/lock-unlock.js +10 -0
  75. package/src/pagination.js +144 -0
  76. package/src/reset-filters.js +26 -0
  77. package/src/search.js +38 -0
  78. package/src/stories/fixtures.js +126 -0
  79. package/src/stories/index.story.js +137 -0
  80. package/src/style.scss +245 -0
  81. package/src/view-actions.js +298 -0
  82. package/src/view-grid.js +100 -0
  83. package/src/view-list.js +99 -0
  84. package/src/view-table.js +425 -0
@@ -0,0 +1,298 @@
1
+ /**
2
+ * WordPress dependencies
3
+ */
4
+ import {
5
+ Button,
6
+ Icon,
7
+ privateApis as componentsPrivateApis,
8
+ } from '@wordpress/components';
9
+ import { chevronRightSmall, check, arrowUp, arrowDown } from '@wordpress/icons';
10
+ import { __ } from '@wordpress/i18n';
11
+
12
+ /**
13
+ * Internal dependencies
14
+ */
15
+ import { unlock } from './lock-unlock';
16
+ import { VIEW_LAYOUTS, LAYOUT_TABLE } from './constants';
17
+
18
+ const {
19
+ DropdownMenuV2: DropdownMenu,
20
+ DropdownMenuGroupV2: DropdownMenuGroup,
21
+ DropdownMenuItemV2: DropdownMenuItem,
22
+ DropdownSubMenuV2: DropdownSubMenu,
23
+ DropdownSubMenuTriggerV2: DropdownSubMenuTrigger,
24
+ } = unlock( componentsPrivateApis );
25
+
26
+ function ViewTypeMenu( { view, onChangeView, supportedLayouts } ) {
27
+ let _availableViews = VIEW_LAYOUTS;
28
+ if ( supportedLayouts ) {
29
+ _availableViews = _availableViews.filter( ( _view ) =>
30
+ supportedLayouts.includes( _view.type )
31
+ );
32
+ }
33
+ if ( _availableViews.length === 1 ) {
34
+ return null;
35
+ }
36
+ const activeView = _availableViews.find( ( v ) => view.type === v.type );
37
+ return (
38
+ <DropdownSubMenu
39
+ trigger={
40
+ <DropdownSubMenuTrigger
41
+ suffix={
42
+ <>
43
+ { activeView.label }
44
+ <Icon icon={ chevronRightSmall } />
45
+ </>
46
+ }
47
+ >
48
+ { __( 'Layout' ) }
49
+ </DropdownSubMenuTrigger>
50
+ }
51
+ >
52
+ { _availableViews.map( ( availableView ) => {
53
+ return (
54
+ <DropdownMenuItem
55
+ key={ availableView.type }
56
+ role="menuitemradio"
57
+ aria-checked={ availableView.id === view.type }
58
+ prefix={
59
+ availableView.type === view.type && (
60
+ <Icon icon={ check } />
61
+ )
62
+ }
63
+ onSelect={ ( event ) => {
64
+ // We need to handle this on DropDown component probably..
65
+ event.preventDefault();
66
+ onChangeView( {
67
+ ...view,
68
+ type: availableView.type,
69
+ } );
70
+ } }
71
+ >
72
+ { availableView.label }
73
+ </DropdownMenuItem>
74
+ );
75
+ } ) }
76
+ </DropdownSubMenu>
77
+ );
78
+ }
79
+
80
+ const PAGE_SIZE_VALUES = [ 10, 20, 50, 100 ];
81
+ function PageSizeMenu( { view, onChangeView } ) {
82
+ return (
83
+ <DropdownSubMenu
84
+ trigger={
85
+ <DropdownSubMenuTrigger
86
+ suffix={
87
+ <>
88
+ { view.perPage }
89
+ <Icon icon={ chevronRightSmall } />
90
+ </>
91
+ }
92
+ >
93
+ { /* TODO: probably label per view type. */ }
94
+ { __( 'Rows per page' ) }
95
+ </DropdownSubMenuTrigger>
96
+ }
97
+ >
98
+ { PAGE_SIZE_VALUES.map( ( size ) => {
99
+ return (
100
+ <DropdownMenuItem
101
+ key={ size }
102
+ role="menuitemradio"
103
+ aria-checked={ view.perPage === size }
104
+ prefix={
105
+ view.perPage === size && <Icon icon={ check } />
106
+ }
107
+ onSelect={ ( event ) => {
108
+ // We need to handle this on DropDown component probably..
109
+ event.preventDefault();
110
+ onChangeView( { ...view, perPage: size, page: 1 } );
111
+ } }
112
+ >
113
+ { size }
114
+ </DropdownMenuItem>
115
+ );
116
+ } ) }
117
+ </DropdownSubMenu>
118
+ );
119
+ }
120
+
121
+ function FieldsVisibilityMenu( { view, onChangeView, fields } ) {
122
+ const hidableFields = fields.filter(
123
+ ( field ) =>
124
+ field.enableHiding !== false && field.id !== view.layout.mediaField
125
+ );
126
+ if ( ! hidableFields?.length ) {
127
+ return null;
128
+ }
129
+ return (
130
+ <DropdownSubMenu
131
+ trigger={
132
+ <DropdownSubMenuTrigger
133
+ suffix={ <Icon icon={ chevronRightSmall } /> }
134
+ >
135
+ { __( 'Fields' ) }
136
+ </DropdownSubMenuTrigger>
137
+ }
138
+ >
139
+ { hidableFields?.map( ( field ) => {
140
+ return (
141
+ <DropdownMenuItem
142
+ key={ field.id }
143
+ role="menuitemcheckbox"
144
+ prefix={
145
+ ! view.hiddenFields?.includes( field.id ) && (
146
+ <Icon icon={ check } />
147
+ )
148
+ }
149
+ onSelect={ ( event ) => {
150
+ event.preventDefault();
151
+ onChangeView( {
152
+ ...view,
153
+ hiddenFields: view.hiddenFields?.includes(
154
+ field.id
155
+ )
156
+ ? view.hiddenFields.filter(
157
+ ( id ) => id !== field.id
158
+ )
159
+ : [
160
+ ...( view.hiddenFields || [] ),
161
+ field.id,
162
+ ],
163
+ } );
164
+ } }
165
+ >
166
+ { field.header }
167
+ </DropdownMenuItem>
168
+ );
169
+ } ) }
170
+ </DropdownSubMenu>
171
+ );
172
+ }
173
+
174
+ // This object is used to construct the sorting options per sortable field.
175
+ const sortingItemsInfo = {
176
+ asc: { icon: arrowUp, label: __( 'Sort ascending' ) },
177
+ desc: { icon: arrowDown, label: __( 'Sort descending' ) },
178
+ };
179
+ function SortMenu( { fields, view, onChangeView } ) {
180
+ const sortableFields = fields.filter(
181
+ ( field ) => field.enableSorting !== false
182
+ );
183
+ if ( ! sortableFields?.length ) {
184
+ return null;
185
+ }
186
+ const currentSortedField = fields.find(
187
+ ( field ) => field.id === view.sort?.field
188
+ );
189
+ return (
190
+ <DropdownSubMenu
191
+ trigger={
192
+ <DropdownSubMenuTrigger
193
+ suffix={
194
+ <>
195
+ { currentSortedField?.header }
196
+ <Icon icon={ chevronRightSmall } />
197
+ </>
198
+ }
199
+ >
200
+ { __( 'Sort by' ) }
201
+ </DropdownSubMenuTrigger>
202
+ }
203
+ >
204
+ { sortableFields?.map( ( field ) => {
205
+ const sortedDirection = view.sort?.direction;
206
+ return (
207
+ <DropdownSubMenu
208
+ key={ field.id }
209
+ trigger={
210
+ <DropdownSubMenuTrigger
211
+ suffix={ <Icon icon={ chevronRightSmall } /> }
212
+ >
213
+ { field.header }
214
+ </DropdownSubMenuTrigger>
215
+ }
216
+ side="left"
217
+ >
218
+ { Object.entries( sortingItemsInfo ).map(
219
+ ( [ direction, info ] ) => {
220
+ const isActive =
221
+ currentSortedField &&
222
+ sortedDirection === direction &&
223
+ field.id === currentSortedField.id;
224
+ return (
225
+ <DropdownMenuItem
226
+ key={ direction }
227
+ role="menuitemradio"
228
+ aria-checked={ isActive }
229
+ prefix={ <Icon icon={ info.icon } /> }
230
+ suffix={
231
+ isActive && <Icon icon={ check } />
232
+ }
233
+ onSelect={ ( event ) => {
234
+ event.preventDefault();
235
+ onChangeView( {
236
+ ...view,
237
+ sort: {
238
+ field: field.id,
239
+ direction,
240
+ },
241
+ } );
242
+ } }
243
+ >
244
+ { info.label }
245
+ </DropdownMenuItem>
246
+ );
247
+ }
248
+ ) }
249
+ </DropdownSubMenu>
250
+ );
251
+ } ) }
252
+ </DropdownSubMenu>
253
+ );
254
+ }
255
+
256
+ export default function ViewActions( {
257
+ fields,
258
+ view,
259
+ onChangeView,
260
+ supportedLayouts,
261
+ } ) {
262
+ return (
263
+ <DropdownMenu
264
+ trigger={
265
+ <Button
266
+ variant="tertiary"
267
+ size="compact"
268
+ icon={
269
+ VIEW_LAYOUTS.find( ( v ) => v.type === view.type )
270
+ ?.icon ||
271
+ VIEW_LAYOUTS.find( ( v ) => v.type === LAYOUT_TABLE )
272
+ .icon
273
+ }
274
+ label={ __( 'View options' ) }
275
+ />
276
+ }
277
+ >
278
+ <DropdownMenuGroup>
279
+ <ViewTypeMenu
280
+ view={ view }
281
+ onChangeView={ onChangeView }
282
+ supportedLayouts={ supportedLayouts }
283
+ />
284
+ <SortMenu
285
+ fields={ fields }
286
+ view={ view }
287
+ onChangeView={ onChangeView }
288
+ />
289
+ <FieldsVisibilityMenu
290
+ fields={ fields }
291
+ view={ view }
292
+ onChangeView={ onChangeView }
293
+ />
294
+ <PageSizeMenu view={ view } onChangeView={ onChangeView } />
295
+ </DropdownMenuGroup>
296
+ </DropdownMenu>
297
+ );
298
+ }
@@ -0,0 +1,100 @@
1
+ /**
2
+ * WordPress dependencies
3
+ */
4
+ import {
5
+ FlexBlock,
6
+ __experimentalGrid as Grid,
7
+ __experimentalHStack as HStack,
8
+ __experimentalVStack as VStack,
9
+ } from '@wordpress/components';
10
+ import { useAsyncList } from '@wordpress/compose';
11
+
12
+ /**
13
+ * Internal dependencies
14
+ */
15
+ import ItemActions from './item-actions';
16
+
17
+ export default function ViewGrid( {
18
+ data,
19
+ fields,
20
+ view,
21
+ actions,
22
+ getItemId,
23
+ deferredRendering,
24
+ } ) {
25
+ const mediaField = fields.find(
26
+ ( field ) => field.id === view.layout.mediaField
27
+ );
28
+ const primaryField = fields.find(
29
+ ( field ) => field.id === view.layout.primaryField
30
+ );
31
+ const visibleFields = fields.filter(
32
+ ( field ) =>
33
+ ! view.hiddenFields.includes( field.id ) &&
34
+ ! [ view.layout.mediaField, view.layout.primaryField ].includes(
35
+ field.id
36
+ )
37
+ );
38
+ const shownData = useAsyncList( data, { step: 3 } );
39
+ const usedData = deferredRendering ? shownData : data;
40
+ return (
41
+ <Grid
42
+ gap={ 8 }
43
+ columns={ 2 }
44
+ alignment="top"
45
+ className="dataviews-grid-view"
46
+ >
47
+ { usedData.map( ( item, index ) => (
48
+ <VStack
49
+ spacing={ 3 }
50
+ key={ getItemId?.( item ) || index }
51
+ className="dataviews-view-grid__card"
52
+ >
53
+ <div className="dataviews-view-grid__media">
54
+ { mediaField?.render( { item } ) }
55
+ </div>
56
+ <HStack
57
+ className="dataviews-view-grid__primary-field"
58
+ justify="space-between"
59
+ >
60
+ <FlexBlock>
61
+ { primaryField?.render( { item } ) }
62
+ </FlexBlock>
63
+ <ItemActions
64
+ item={ item }
65
+ actions={ actions }
66
+ isCompact
67
+ />
68
+ </HStack>
69
+ <VStack
70
+ className="dataviews-view-grid__fields"
71
+ spacing={ 3 }
72
+ >
73
+ { visibleFields.map( ( field ) => {
74
+ const renderedValue = field.render( {
75
+ item,
76
+ } );
77
+ if ( ! renderedValue ) {
78
+ return null;
79
+ }
80
+ return (
81
+ <VStack
82
+ className="dataviews-view-grid__field"
83
+ key={ field.id }
84
+ spacing={ 1 }
85
+ >
86
+ <div className="dataviews-view-grid__field-header">
87
+ { field.header }
88
+ </div>
89
+ <div className="dataviews-view-grid__field-value">
90
+ { field.render( { item } ) }
91
+ </div>
92
+ </VStack>
93
+ );
94
+ } ) }
95
+ </VStack>
96
+ </VStack>
97
+ ) ) }
98
+ </Grid>
99
+ );
100
+ }
@@ -0,0 +1,99 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import classNames from 'classnames';
5
+
6
+ /**
7
+ * WordPress dependencies
8
+ */
9
+ import { useAsyncList } from '@wordpress/compose';
10
+ import {
11
+ __experimentalHStack as HStack,
12
+ __experimentalVStack as VStack,
13
+ } from '@wordpress/components';
14
+ import { ENTER, SPACE } from '@wordpress/keycodes';
15
+
16
+ export default function ViewList( {
17
+ view,
18
+ fields,
19
+ data,
20
+ getItemId,
21
+ onSelectionChange,
22
+ selection,
23
+ deferredRendering,
24
+ } ) {
25
+ const shownData = useAsyncList( data, { step: 3 } );
26
+ const usedData = deferredRendering ? shownData : data;
27
+ const mediaField = fields.find(
28
+ ( field ) => field.id === view.layout.mediaField
29
+ );
30
+ const primaryField = fields.find(
31
+ ( field ) => field.id === view.layout.primaryField
32
+ );
33
+ const visibleFields = fields.filter(
34
+ ( field ) =>
35
+ ! view.hiddenFields.includes( field.id ) &&
36
+ ! [ view.layout.primaryField, view.layout.mediaField ].includes(
37
+ field.id
38
+ )
39
+ );
40
+
41
+ const onEnter = ( item ) => ( event ) => {
42
+ const { keyCode } = event;
43
+ if ( [ ENTER, SPACE ].includes( keyCode ) ) {
44
+ onSelectionChange( [ item ] );
45
+ }
46
+ };
47
+
48
+ return (
49
+ <ul className="dataviews-list-view">
50
+ { usedData.map( ( item, index ) => {
51
+ return (
52
+ <li key={ getItemId?.( item ) || index }>
53
+ <div
54
+ role="button"
55
+ tabIndex={ 0 }
56
+ aria-pressed={ selection.includes( item.id ) }
57
+ onKeyDown={ onEnter( item ) }
58
+ className={ classNames(
59
+ 'dataviews-list-view__item',
60
+ {
61
+ 'dataviews-list-view__item-selected':
62
+ selection.includes( item.id ),
63
+ }
64
+ ) }
65
+ onClick={ () => onSelectionChange( [ item ] ) }
66
+ >
67
+ <HStack spacing={ 3 }>
68
+ <div className="dataviews-list-view__media-wrapper">
69
+ { mediaField?.render( { item } ) || (
70
+ <div className="dataviews-list-view__media-placeholder"></div>
71
+ ) }
72
+ </div>
73
+ <HStack>
74
+ <VStack spacing={ 1 }>
75
+ { primaryField?.render( { item } ) }
76
+ <div className="dataviews-list-view__fields">
77
+ { visibleFields.map( ( field ) => {
78
+ return (
79
+ <span
80
+ key={ field.id }
81
+ className="dataviews-list-view__field"
82
+ >
83
+ { field.render( {
84
+ item,
85
+ } ) }
86
+ </span>
87
+ );
88
+ } ) }
89
+ </div>
90
+ </VStack>
91
+ </HStack>
92
+ </HStack>
93
+ </div>
94
+ </li>
95
+ );
96
+ } ) }
97
+ </ul>
98
+ );
99
+ }