@wordpress/dataviews 0.7.0 → 0.9.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 (79) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +49 -18
  3. package/build/constants.js +28 -10
  4. package/build/constants.js.map +1 -1
  5. package/build/dataviews.js +3 -7
  6. package/build/dataviews.js.map +1 -1
  7. package/build/filter-and-sort-data-view.js +147 -0
  8. package/build/filter-and-sort-data-view.js.map +1 -0
  9. package/build/filter-summary.js +33 -12
  10. package/build/filter-summary.js.map +1 -1
  11. package/build/filters.js +11 -16
  12. package/build/filters.js.map +1 -1
  13. package/build/index.js +3 -9
  14. package/build/index.js.map +1 -1
  15. package/build/item-actions.js +20 -39
  16. package/build/item-actions.js.map +1 -1
  17. package/build/normalize-fields.js +25 -0
  18. package/build/normalize-fields.js.map +1 -0
  19. package/build/pagination.js +2 -2
  20. package/build/pagination.js.map +1 -1
  21. package/build/search-widget.js +34 -10
  22. package/build/search-widget.js.map +1 -1
  23. package/build/utils.js +25 -67
  24. package/build/utils.js.map +1 -1
  25. package/build/view-grid.js +25 -12
  26. package/build/view-grid.js.map +1 -1
  27. package/build/view-list.js +122 -58
  28. package/build/view-list.js.map +1 -1
  29. package/build/view-table.js +53 -8
  30. package/build/view-table.js.map +1 -1
  31. package/build-module/constants.js +27 -9
  32. package/build-module/constants.js.map +1 -1
  33. package/build-module/dataviews.js +3 -7
  34. package/build-module/dataviews.js.map +1 -1
  35. package/build-module/filter-and-sort-data-view.js +139 -0
  36. package/build-module/filter-and-sort-data-view.js.map +1 -0
  37. package/build-module/filter-summary.js +34 -13
  38. package/build-module/filter-summary.js.map +1 -1
  39. package/build-module/filters.js +12 -17
  40. package/build-module/filters.js.map +1 -1
  41. package/build-module/index.js +1 -1
  42. package/build-module/index.js.map +1 -1
  43. package/build-module/item-actions.js +20 -39
  44. package/build-module/item-actions.js.map +1 -1
  45. package/build-module/normalize-fields.js +19 -0
  46. package/build-module/normalize-fields.js.map +1 -0
  47. package/build-module/pagination.js +2 -2
  48. package/build-module/pagination.js.map +1 -1
  49. package/build-module/search-widget.js +35 -11
  50. package/build-module/search-widget.js.map +1 -1
  51. package/build-module/utils.js +25 -66
  52. package/build-module/utils.js.map +1 -1
  53. package/build-module/view-grid.js +26 -13
  54. package/build-module/view-grid.js.map +1 -1
  55. package/build-module/view-list.js +124 -60
  56. package/build-module/view-list.js.map +1 -1
  57. package/build-module/view-table.js +55 -10
  58. package/build-module/view-table.js.map +1 -1
  59. package/build-style/style-rtl.css +41 -11
  60. package/build-style/style.css +41 -11
  61. package/package.json +11 -11
  62. package/src/constants.js +35 -9
  63. package/src/dataviews.js +3 -7
  64. package/src/filter-and-sort-data-view.js +154 -0
  65. package/src/filter-summary.js +76 -23
  66. package/src/filters.js +20 -26
  67. package/src/index.js +1 -1
  68. package/src/item-actions.js +19 -55
  69. package/src/normalize-fields.js +17 -0
  70. package/src/pagination.js +2 -2
  71. package/src/search-widget.js +63 -21
  72. package/src/stories/fixtures.js +87 -2
  73. package/src/stories/index.story.js +5 -74
  74. package/src/style.scss +53 -14
  75. package/src/test/filter-and-sort-data-view.js +276 -0
  76. package/src/utils.js +38 -56
  77. package/src/view-grid.js +36 -11
  78. package/src/view-list.js +159 -69
  79. package/src/view-table.js +71 -9
@@ -0,0 +1,276 @@
1
+ /**
2
+ * Internal dependencies
3
+ */
4
+ import { filterSortAndPaginate } from '../filter-and-sort-data-view';
5
+ import { data, fields } from '../stories/fixtures';
6
+
7
+ describe( 'filters', () => {
8
+ it( 'should return empty if the data is empty', () => {
9
+ expect( filterSortAndPaginate( null, {}, [] ) ).toStrictEqual( {
10
+ data: [],
11
+ paginationInfo: { totalItems: 0, totalPages: 0 },
12
+ } );
13
+ } );
14
+
15
+ it( 'should return the same data if no filters are applied', () => {
16
+ expect(
17
+ filterSortAndPaginate(
18
+ data,
19
+ {
20
+ filters: [],
21
+ },
22
+ []
23
+ )
24
+ ).toStrictEqual( {
25
+ data,
26
+ paginationInfo: { totalItems: data.length, totalPages: 1 },
27
+ } );
28
+ } );
29
+
30
+ it( 'should search using searchable fields (title)', () => {
31
+ const { data: result } = filterSortAndPaginate(
32
+ data,
33
+ {
34
+ search: 'Neptu',
35
+ filters: [],
36
+ },
37
+ fields
38
+ );
39
+ expect( result ).toHaveLength( 1 );
40
+ expect( result[ 0 ].title ).toBe( 'Neptune' );
41
+ } );
42
+
43
+ it( 'should search using searchable fields (description)', () => {
44
+ const { data: result } = filterSortAndPaginate(
45
+ data,
46
+ {
47
+ search: 'photo',
48
+ filters: [],
49
+ },
50
+ fields
51
+ );
52
+ expect( result ).toHaveLength( 1 );
53
+ expect( result[ 0 ].description ).toBe( 'NASA photo' );
54
+ } );
55
+
56
+ it( 'should perform case-insensitive and accent-insensitive search', () => {
57
+ const { data: result } = filterSortAndPaginate(
58
+ data,
59
+ {
60
+ search: 'nete ven',
61
+ filters: [],
62
+ },
63
+ fields
64
+ );
65
+ expect( result ).toHaveLength( 1 );
66
+ expect( result[ 0 ].description ).toBe( 'La planète Vénus' );
67
+ } );
68
+
69
+ it( 'should search using IS filter', () => {
70
+ const { data: result } = filterSortAndPaginate(
71
+ data,
72
+ {
73
+ filters: [
74
+ {
75
+ field: 'type',
76
+ operator: 'is',
77
+ value: 'Ice giant',
78
+ },
79
+ ],
80
+ },
81
+ fields
82
+ );
83
+ expect( result ).toHaveLength( 2 );
84
+ expect( result[ 0 ].title ).toBe( 'Neptune' );
85
+ expect( result[ 1 ].title ).toBe( 'Uranus' );
86
+ } );
87
+
88
+ it( 'should search using IS NOT filter', () => {
89
+ const { data: result } = filterSortAndPaginate(
90
+ data,
91
+ {
92
+ filters: [
93
+ {
94
+ field: 'type',
95
+ operator: 'isNot',
96
+ value: 'Ice giant',
97
+ },
98
+ ],
99
+ },
100
+ fields
101
+ );
102
+ expect( result ).toHaveLength( 9 );
103
+ expect( result[ 0 ].title ).toBe( 'Apollo' );
104
+ expect( result[ 1 ].title ).toBe( 'Space' );
105
+ expect( result[ 2 ].title ).toBe( 'NASA' );
106
+ expect( result[ 3 ].title ).toBe( 'Mercury' );
107
+ expect( result[ 4 ].title ).toBe( 'Venus' );
108
+ expect( result[ 5 ].title ).toBe( 'Earth' );
109
+ expect( result[ 6 ].title ).toBe( 'Mars' );
110
+ expect( result[ 7 ].title ).toBe( 'Jupiter' );
111
+ expect( result[ 8 ].title ).toBe( 'Saturn' );
112
+ } );
113
+
114
+ it( 'should search using IS ANY filter for STRING values', () => {
115
+ const { data: result } = filterSortAndPaginate(
116
+ data,
117
+ {
118
+ filters: [
119
+ {
120
+ field: 'type',
121
+ operator: 'isAny',
122
+ value: [ 'Ice giant' ],
123
+ },
124
+ ],
125
+ },
126
+ fields
127
+ );
128
+ expect( result ).toHaveLength( 2 );
129
+ expect( result[ 0 ].title ).toBe( 'Neptune' );
130
+ expect( result[ 1 ].title ).toBe( 'Uranus' );
131
+ } );
132
+
133
+ it( 'should search using IS NONE filter for STRING values', () => {
134
+ const { data: result } = filterSortAndPaginate(
135
+ data,
136
+ {
137
+ filters: [
138
+ {
139
+ field: 'type',
140
+ operator: 'isNone',
141
+ value: [ 'Ice giant', 'Gas giant', 'Terrestrial' ],
142
+ },
143
+ ],
144
+ },
145
+ fields
146
+ );
147
+ expect( result ).toHaveLength( 3 );
148
+ expect( result[ 0 ].title ).toBe( 'Apollo' );
149
+ expect( result[ 1 ].title ).toBe( 'Space' );
150
+ expect( result[ 2 ].title ).toBe( 'NASA' );
151
+ } );
152
+
153
+ it( 'should search using IS ANY filter for ARRAY values', () => {
154
+ const { data: result } = filterSortAndPaginate(
155
+ data,
156
+ {
157
+ filters: [
158
+ {
159
+ field: 'categories',
160
+ operator: 'isAny',
161
+ value: [ 'NASA' ],
162
+ },
163
+ ],
164
+ },
165
+ fields
166
+ );
167
+ expect( result ).toHaveLength( 2 );
168
+ expect( result[ 0 ].title ).toBe( 'Apollo' );
169
+ expect( result[ 1 ].title ).toBe( 'NASA' );
170
+ } );
171
+
172
+ it( 'should search using IS NONE filter for ARRAY values', () => {
173
+ const { data: result } = filterSortAndPaginate(
174
+ data,
175
+ {
176
+ filters: [
177
+ {
178
+ field: 'categories',
179
+ operator: 'isNone',
180
+ value: [ 'Space' ],
181
+ },
182
+ ],
183
+ },
184
+ fields
185
+ );
186
+ expect( result ).toHaveLength( 1 );
187
+ expect( result[ 0 ].title ).toBe( 'NASA' );
188
+ } );
189
+
190
+ it( 'should search using IS ALL filter', () => {
191
+ const { data: result } = filterSortAndPaginate(
192
+ data,
193
+ {
194
+ filters: [
195
+ {
196
+ field: 'categories',
197
+ operator: 'isAll',
198
+ value: [ 'Planet', 'Solar system' ],
199
+ },
200
+ ],
201
+ },
202
+ fields
203
+ );
204
+ expect( result ).toHaveLength( 7 );
205
+ expect( result[ 0 ].title ).toBe( 'Neptune' );
206
+ expect( result[ 1 ].title ).toBe( 'Mercury' );
207
+ expect( result[ 2 ].title ).toBe( 'Venus' );
208
+ expect( result[ 3 ].title ).toBe( 'Earth' );
209
+ expect( result[ 4 ].title ).toBe( 'Mars' );
210
+ expect( result[ 5 ].title ).toBe( 'Jupiter' );
211
+ expect( result[ 6 ].title ).toBe( 'Saturn' );
212
+ } );
213
+
214
+ it( 'should search using IS NOT ALL filter', () => {
215
+ const { data: result } = filterSortAndPaginate(
216
+ data,
217
+ {
218
+ filters: [
219
+ {
220
+ field: 'categories',
221
+ operator: 'isNotAll',
222
+ value: [ 'Planet', 'Solar system' ],
223
+ },
224
+ ],
225
+ },
226
+ fields
227
+ );
228
+ expect( result ).toHaveLength( 3 );
229
+ expect( result[ 0 ].title ).toBe( 'Apollo' );
230
+ expect( result[ 1 ].title ).toBe( 'Space' );
231
+ expect( result[ 2 ].title ).toBe( 'NASA' );
232
+ } );
233
+ } );
234
+
235
+ describe( 'sorting', () => {
236
+ it( 'should sort', () => {
237
+ const { data: result } = filterSortAndPaginate(
238
+ data,
239
+ {
240
+ sort: { field: 'title', direction: 'desc' },
241
+ filters: [
242
+ {
243
+ field: 'type',
244
+ operator: 'isAny',
245
+ value: [ 'Ice giant' ],
246
+ },
247
+ ],
248
+ },
249
+ fields
250
+ );
251
+ expect( result ).toHaveLength( 2 );
252
+ expect( result[ 0 ].title ).toBe( 'Uranus' );
253
+ expect( result[ 1 ].title ).toBe( 'Neptune' );
254
+ } );
255
+ } );
256
+
257
+ describe( 'pagination', () => {
258
+ it( 'should paginate', () => {
259
+ const { data: result, paginationInfo } = filterSortAndPaginate(
260
+ data,
261
+ {
262
+ perPage: 2,
263
+ page: 2,
264
+ filters: [],
265
+ },
266
+ fields
267
+ );
268
+ expect( result ).toHaveLength( 2 );
269
+ expect( result[ 0 ].title ).toBe( 'NASA' );
270
+ expect( result[ 1 ].title ).toBe( 'Neptune' );
271
+ expect( paginationInfo ).toStrictEqual( {
272
+ totalItems: data.length,
273
+ totalPages: 6,
274
+ } );
275
+ } );
276
+ } );
package/src/utils.js CHANGED
@@ -1,66 +1,48 @@
1
1
  /**
2
2
  * Internal dependencies
3
3
  */
4
- import { OPERATORS } from './constants';
5
-
6
- /**
7
- * Helper util to sort data by text fields, when sorting is done client side.
8
- *
9
- * @param {Object} params Function params.
10
- * @param {Object[]} params.data Data to sort.
11
- * @param {Object} params.view Current view object.
12
- * @param {Object[]} params.fields Array of available fields.
13
- * @param {string[]} params.textFields Array of the field ids to sort.
14
- *
15
- * @return {Object[]} Sorted data.
16
- */
17
- export const sortByTextFields = ( { data, view, fields, textFields } ) => {
18
- const sortedData = [ ...data ];
19
- const fieldId = view.sort.field;
20
- if ( textFields.includes( fieldId ) ) {
21
- const fieldToSort = fields.find( ( field ) => {
22
- return field.id === fieldId;
23
- } );
24
- sortedData.sort( ( a, b ) => {
25
- const valueA = fieldToSort.getValue( { item: a } ) ?? '';
26
- const valueB = fieldToSort.getValue( { item: b } ) ?? '';
27
- return view.sort.direction === 'asc'
28
- ? valueA.localeCompare( valueB )
29
- : valueB.localeCompare( valueA );
30
- } );
31
- }
32
- return sortedData;
33
- };
34
-
35
- /**
36
- * Helper util to get the paginated data and the paginateInfo needed,
37
- * when pagination is done client side.
38
- *
39
- * @param {Object} params Function params.
40
- * @param {Object[]} params.data Available data.
41
- * @param {Object} params.view Current view object.
42
- *
43
- * @return {Object} Paginated data and paginationInfo.
44
- */
45
- export function getPaginationResults( { data, view } ) {
46
- const start = ( view.page - 1 ) * view.perPage;
47
- const totalItems = data?.length || 0;
48
- data = data?.slice( start, start + view.perPage );
49
- return {
50
- data,
51
- paginationInfo: {
52
- totalItems,
53
- totalPages: Math.ceil( totalItems / view.perPage ),
54
- },
55
- };
56
- }
4
+ import {
5
+ ALL_OPERATORS,
6
+ OPERATOR_IS,
7
+ OPERATOR_IS_NOT,
8
+ OPERATOR_IS_ANY,
9
+ OPERATOR_IS_NONE,
10
+ } from './constants';
57
11
 
58
12
  export const sanitizeOperators = ( field ) => {
59
13
  let operators = field.filterBy?.operators;
14
+
15
+ // Assign default values.
60
16
  if ( ! operators || ! Array.isArray( operators ) ) {
61
- operators = Object.keys( OPERATORS );
17
+ operators = [ OPERATOR_IS_ANY, OPERATOR_IS_NONE ];
18
+ }
19
+
20
+ // Transform legacy in, notIn operators to is, isNot.
21
+ // To be removed in the future.
22
+ if ( operators.includes( 'in' ) ) {
23
+ operators = operators.filter( ( operator ) => operator !== 'is' );
24
+ operators.push( 'is' );
25
+ }
26
+ if ( operators.includes( 'notIn' ) ) {
27
+ operators = operators.filter( ( operator ) => operator !== 'notIn' );
28
+ operators.push( 'isNot' );
62
29
  }
63
- return operators.filter( ( operator ) =>
64
- Object.keys( OPERATORS ).includes( operator )
30
+
31
+ // Make sure only valid operators are used.
32
+ operators = operators.filter( ( operator ) =>
33
+ ALL_OPERATORS.includes( operator )
65
34
  );
35
+
36
+ // Do not allow mixing single & multiselection operators.
37
+ // Remove multiselection operators if any of the single selection ones is present.
38
+ if (
39
+ operators.includes( OPERATOR_IS ) ||
40
+ operators.includes( OPERATOR_IS_NOT )
41
+ ) {
42
+ operators = operators.filter( ( operator ) =>
43
+ [ OPERATOR_IS, OPERATOR_IS_NOT ].includes( operator )
44
+ );
45
+ }
46
+
47
+ return operators;
66
48
  };
package/src/view-grid.js CHANGED
@@ -10,8 +10,9 @@ import {
10
10
  __experimentalGrid as Grid,
11
11
  __experimentalHStack as HStack,
12
12
  __experimentalVStack as VStack,
13
- Tooltip,
14
13
  Spinner,
14
+ Flex,
15
+ FlexItem,
15
16
  } from '@wordpress/components';
16
17
  import { __ } from '@wordpress/i18n';
17
18
  import { useAsyncList } from '@wordpress/compose';
@@ -34,6 +35,7 @@ function GridItem( {
34
35
  mediaField,
35
36
  primaryField,
36
37
  visibleFields,
38
+ displayAsColumnFields,
37
39
  } ) {
38
40
  const hasBulkAction = useHasAPossibleBulkAction( actions, item );
39
41
  const id = getItemId( item );
@@ -46,9 +48,12 @@ function GridItem( {
46
48
  'is-selected': hasBulkAction && isSelected,
47
49
  } ) }
48
50
  onClickCapture={ ( event ) => {
49
- if ( hasBulkAction && ( event.ctrlKey || event.metaKey ) ) {
51
+ if ( event.ctrlKey || event.metaKey ) {
50
52
  event.stopPropagation();
51
53
  event.preventDefault();
54
+ if ( ! hasBulkAction ) {
55
+ return;
56
+ }
52
57
  if ( ! isSelected ) {
53
58
  onSelectionChange(
54
59
  data.filter( ( _item ) => {
@@ -104,17 +109,34 @@ function GridItem( {
104
109
  return null;
105
110
  }
106
111
  return (
107
- <VStack
108
- className="dataviews-view-grid__field"
112
+ <Flex
113
+ className={ classnames(
114
+ 'dataviews-view-grid__field',
115
+ displayAsColumnFields?.includes( field.id )
116
+ ? 'is-column'
117
+ : 'is-row'
118
+ ) }
109
119
  key={ field.id }
110
- spacing={ 1 }
120
+ gap={ 1 }
121
+ justify="flex-start"
122
+ expanded
123
+ style={ { height: 'auto' } }
124
+ direction={
125
+ displayAsColumnFields?.includes( field.id )
126
+ ? 'column'
127
+ : 'row'
128
+ }
111
129
  >
112
- <Tooltip text={ field.header } placement="left">
113
- <div className="dataviews-view-grid__field-value">
114
- { renderedValue }
115
- </div>
116
- </Tooltip>
117
- </VStack>
130
+ <FlexItem className="dataviews-view-grid__field-name">
131
+ { field.header }
132
+ </FlexItem>
133
+ <FlexItem
134
+ className="dataviews-view-grid__field-value"
135
+ style={ { maxHeight: 'none' } }
136
+ >
137
+ { renderedValue }
138
+ </FlexItem>
139
+ </Flex>
118
140
  );
119
141
  } ) }
120
142
  </VStack>
@@ -172,6 +194,9 @@ export default function ViewGrid( {
172
194
  mediaField={ mediaField }
173
195
  primaryField={ primaryField }
174
196
  visibleFields={ visibleFields }
197
+ displayAsColumnFields={
198
+ view.layout.displayAsColumnFields
199
+ }
175
200
  />
176
201
  );
177
202
  } ) }