@axinom/mosaic-ui 0.45.0-rc.3 → 0.45.0-rc.5

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@axinom/mosaic-ui",
3
- "version": "0.45.0-rc.3",
3
+ "version": "0.45.0-rc.5",
4
4
  "description": "UI components for building Axinom Mosaic applications",
5
5
  "author": "Axinom",
6
6
  "license": "PROPRIETARY",
@@ -32,7 +32,7 @@
32
32
  "build-storybook": "storybook build"
33
33
  },
34
34
  "dependencies": {
35
- "@axinom/mosaic-core": "^0.4.18-rc.3",
35
+ "@axinom/mosaic-core": "^0.4.18-rc.5",
36
36
  "@faker-js/faker": "^7.4.0",
37
37
  "@popperjs/core": "^2.11.8",
38
38
  "clsx": "^1.1.0",
@@ -105,5 +105,5 @@
105
105
  "publishConfig": {
106
106
  "access": "public"
107
107
  },
108
- "gitHead": "48d581221b821f9f2d72e2ae16345c387d672f48"
108
+ "gitHead": "dc25615bd1c2a5624c53a9070a12884da3f2c5d2"
109
109
  }
@@ -320,6 +320,17 @@ describe('DynamicListRow', () => {
320
320
  dataWithPosition.position,
321
321
  Number(mockNewPosition),
322
322
  );
323
+
324
+ input.prop('onBlur')?.({
325
+ // @ts-expect-error not full event args object
326
+ currentTarget: { value: mockNewPosition },
327
+ });
328
+
329
+ expect(inputSpy).toHaveBeenCalledTimes(2);
330
+ expect(inputSpy).toHaveBeenCalledWith(
331
+ dataWithPosition.position,
332
+ Number(mockNewPosition),
333
+ );
323
334
  });
324
335
 
325
336
  it('onPositionInputChanged is not emitted if current position and new position are the same', () => {
@@ -160,6 +160,11 @@ export const DynamicListRow = <T extends Data>({
160
160
  event.key === 'Enter' &&
161
161
  onPositionInputChangedHandler(Number(event.currentTarget.value))
162
162
  }
163
+ onBlur={(event) => {
164
+ onPositionInputChangedHandler(
165
+ Number(event.currentTarget.value),
166
+ );
167
+ }}
163
168
  />
164
169
  </div>
165
170
  </div>
@@ -19,6 +19,7 @@ import {
19
19
  Column,
20
20
  ItemSelectEventArgs,
21
21
  List,
22
+ ListElement,
22
23
  ListSelectMode,
23
24
  SortData,
24
25
  } from '../List';
@@ -227,6 +228,8 @@ export const Explorer = React.forwardRef(function Explorer<T extends Data>(
227
228
  getState<SortData<T>>(stationKey, 'sort') ?? defaultSortOrder,
228
229
  );
229
230
 
231
+ const listRef = React.useRef<ListElement>(null);
232
+
230
233
  useEffect(() => {
231
234
  if (
232
235
  globalStateOptions.filters &&
@@ -436,10 +439,12 @@ export const Explorer = React.forwardRef(function Explorer<T extends Data>(
436
439
  onFiltersChange={(args) => {
437
440
  onFiltersChange(args);
438
441
  setActiveFilters(args);
442
+ listRef.current?.resetSelection();
439
443
  }}
440
444
  />
441
445
  <div>
442
446
  <List<T>
447
+ ref={listRef}
443
448
  columns={columns}
444
449
  data={data}
445
450
  isLoading={isLoading}
@@ -121,8 +121,8 @@ const generateBulkActions = <T extends Data>(
121
121
  ): ExplorerBulkAction<T>[] =>
122
122
  generateItemArray(amount, (index) => ({
123
123
  label: `Bulk Action ${index + 1}`,
124
- onClick: () => {
125
- action('bulkActionClicked')();
124
+ onClick: (args) => {
125
+ action('bulkActionClicked')(args);
126
126
  return slowFunc();
127
127
  },
128
128
  icon: IconName.ChevronRight,
@@ -106,3 +106,10 @@ export interface SortData<T> {
106
106
 
107
107
  columnSortKey?: string;
108
108
  }
109
+
110
+ export interface ListElement {
111
+ /**
112
+ * Resets the selection of the list.
113
+ */
114
+ resetSelection: () => void;
115
+ }
@@ -5,7 +5,7 @@ import { MemoryRouter } from 'react-router';
5
5
  import { Data } from '../../types/data';
6
6
  import { TextButton } from '../Buttons/TextButton/TextButton';
7
7
  import { List } from './List';
8
- import { Column, ListSelectMode, SortData } from './List.model';
8
+ import { Column, ListElement, ListSelectMode, SortData } from './List.model';
9
9
  import { ListHeader } from './ListHeader/ListHeader';
10
10
  import { ListRow } from './ListRow/ListRow';
11
11
  import { ListRowLoader } from './ListRow/ListRowLoader';
@@ -366,6 +366,88 @@ describe('List', () => {
366
366
  });
367
367
  });
368
368
 
369
+ it('clears the SINGLE_ITEMS selection when resetSelection is called', () => {
370
+ const ref = React.createRef<ListElement>();
371
+ const spy = jest.fn();
372
+
373
+ const wrapper = mount(
374
+ <List
375
+ ref={ref}
376
+ columns={mockListColumns}
377
+ data={mockListData}
378
+ selectionMode={ListSelectMode.Multi}
379
+ onItemSelected={spy}
380
+ />,
381
+ );
382
+
383
+ let rows = wrapper.find(ListRow);
384
+
385
+ act(() => {
386
+ rows.at(0).prop('onItemSelected')!(true);
387
+ rows.at(1).prop('onItemSelected')!(true);
388
+ rows.at(2).prop('onItemSelected')!(true);
389
+ });
390
+
391
+ wrapper.update();
392
+
393
+ rows = wrapper.find(ListRow);
394
+
395
+ expect(rows.at(0).prop('itemSelected')).toBeTruthy();
396
+ expect(rows.at(1).prop('itemSelected')).toBeTruthy();
397
+ expect(rows.at(2).prop('itemSelected')).toBeTruthy();
398
+
399
+ act(() => {
400
+ ref.current?.resetSelection();
401
+ });
402
+
403
+ wrapper.update();
404
+
405
+ rows = wrapper.find(ListRow);
406
+
407
+ expect(rows.at(0).prop('itemSelected')).toBeFalsy();
408
+ expect(rows.at(1).prop('itemSelected')).toBeFalsy();
409
+ expect(rows.at(2).prop('itemSelected')).toBeFalsy();
410
+ });
411
+
412
+ it('clears the SELECT_ALL selection when resetSelection is called', () => {
413
+ const ref = React.createRef<ListElement>();
414
+ const spy = jest.fn();
415
+
416
+ const wrapper = mount(
417
+ <List
418
+ ref={ref}
419
+ columns={mockListColumns}
420
+ data={mockListData}
421
+ selectionMode={ListSelectMode.Multi}
422
+ onItemSelected={spy}
423
+ />,
424
+ );
425
+
426
+ let headerRow = wrapper.find(ListHeader);
427
+
428
+ act(() => {
429
+ headerRow.prop('onCheckboxToggled')!(true);
430
+ });
431
+
432
+ wrapper.update();
433
+
434
+ headerRow = wrapper.find(ListHeader);
435
+
436
+ expect(headerRow.prop('itemSelected')).toBeTruthy();
437
+
438
+ act(() => {
439
+ ref.current?.resetSelection();
440
+ });
441
+
442
+ wrapper.update();
443
+
444
+ wrapper.update();
445
+
446
+ headerRow = wrapper.find(ListHeader);
447
+
448
+ expect(headerRow.prop('itemSelected')).toBeFalsy();
449
+ });
450
+
369
451
  it('does not use onItemSelected if provided with a generateItemLink', () => {
370
452
  const spy = jest.fn();
371
453
  const wrapper = mount(
@@ -5,6 +5,7 @@ import React, {
5
5
  ReactElement,
6
6
  useCallback,
7
7
  useEffect,
8
+ useImperativeHandle,
8
9
  useRef,
9
10
  useState,
10
11
  } from 'react';
@@ -16,6 +17,7 @@ import { TextButton } from '../Buttons/TextButton/TextButton';
16
17
  import {
17
18
  Column,
18
19
  ItemSelectEventArgs,
20
+ ListElement,
19
21
  ListItem,
20
22
  ListSelectMode,
21
23
  SortData,
@@ -24,6 +26,7 @@ import classes from './List.scss';
24
26
  import { ListHeader } from './ListHeader/ListHeader';
25
27
  import { ListRow } from './ListRow/ListRow';
26
28
  import { ListRowLoader } from './ListRow/ListRowLoader';
29
+ import { getActionButtonVisibility, isTrigger, useSort } from './helpers';
27
30
  import { useColumnsSize } from './useColumnsSize';
28
31
 
29
32
  export interface ListProps<T extends Data> {
@@ -105,85 +108,40 @@ export interface ListProps<T extends Data> {
105
108
  inlineMenuActions?: (data: T) => ActionData[];
106
109
  }
107
110
 
108
- const noItemsMessage = (
109
- itemsCount: number,
110
- isLoading: boolean,
111
- isError: boolean,
112
- ): ReactElement | undefined => {
113
- if (!isLoading && !isError && itemsCount === 0) {
114
- return (
115
- <div className={clsx(classes.NoData)} data-test-id="list-empty">
116
- <p>No items found</p>
117
- </div>
118
- );
119
- }
120
- };
121
-
122
- const isTrigger = function <T extends Data>(
123
- index: number,
124
- listItems: ListItem<T>[],
125
- loadingTriggerOffset: number,
126
- ): boolean {
127
- return listItems.length - loadingTriggerOffset === index + 1 ? true : false;
128
- };
129
-
130
- const useSort = <T,>(
131
- defaultSortOrder: SortData<T> | undefined,
132
- onSortChanged: (sort: SortData<T>) => void,
133
- ): {
134
- readonly sort: SortData<T> | undefined;
135
- readonly sortChangedHandler: (sort: SortData<T>) => void;
136
- } => {
137
- const [sort, setSort] = useState<SortData<T> | undefined>(defaultSortOrder);
138
-
139
- const sortChangedHandler = (sort: SortData<T>): void => {
140
- setSort(sort);
141
- onSortChanged && onSortChanged(sort);
142
- };
143
-
144
- return { sort, sortChangedHandler } as const;
145
- };
146
-
147
- /**
148
- * Renders various sets of data in a tabular format
149
- * @example
150
- * <List<DataInterface>
151
- * columns={[{propertyName: 'id', size: '1fr', label: 'Id'}]}
152
- * data={[{id: '1',desc: 'Description 1',title: 'Item 1'}]}
153
- * itemClicked={(item)=> {console.log(item)}}
154
- * />
155
- */
156
- export const List = <T extends Data>({
157
- columns,
158
- data = [],
159
- isLoading = false,
160
- isError = false,
161
- errorMsg = 'There was an error.',
162
- handleRetry = true,
163
- minimumWidth = '500px',
164
- columnGap = '5px',
165
- rowGap = '0px',
166
- headerRowHeight = '44px',
167
- listRowHeight = '50px',
168
- listRowActionSize = '50px',
169
- headerRowActionSize = '28px',
170
- horizontalTextAlign = 'left',
171
- verticalTextAlign = 'center',
172
- keyProperty = 'id' as keyof T,
173
- showActionButton = true,
174
- loadingTriggerOffset = 10,
175
- defaultSortOrder,
176
- selectionMode = ListSelectMode.None,
177
- enableSelectAll = true,
178
- onItemClicked = noop,
179
- onItemSelected = noop,
180
- onRequestMoreData = noop,
181
- onSortChanged = noop,
182
- onRetry = noop,
183
- generateItemLink,
184
- className = '',
185
- inlineMenuActions,
186
- }: PropsWithChildren<ListProps<T>>): JSX.Element => {
111
+ const ListRenderer = <T extends Data>(
112
+ {
113
+ columns,
114
+ data = [],
115
+ isLoading = false,
116
+ isError = false,
117
+ errorMsg = 'There was an error.',
118
+ handleRetry = true,
119
+ minimumWidth = '500px',
120
+ columnGap = '5px',
121
+ rowGap = '0px',
122
+ headerRowHeight = '44px',
123
+ listRowHeight = '50px',
124
+ listRowActionSize = '50px',
125
+ headerRowActionSize = '28px',
126
+ horizontalTextAlign = 'left',
127
+ verticalTextAlign = 'center',
128
+ keyProperty = 'id' as keyof T,
129
+ showActionButton = true,
130
+ loadingTriggerOffset = 10,
131
+ defaultSortOrder,
132
+ selectionMode = ListSelectMode.None,
133
+ enableSelectAll = true,
134
+ onItemClicked = noop,
135
+ onItemSelected = noop,
136
+ onRequestMoreData = noop,
137
+ onSortChanged = noop,
138
+ onRetry = noop,
139
+ generateItemLink,
140
+ className = '',
141
+ inlineMenuActions,
142
+ }: PropsWithChildren<ListProps<T>>,
143
+ ref?: React.ForwardedRef<ListElement>,
144
+ ): JSX.Element => {
187
145
  const [listItems, setListItems] = useState<ListItem<T>[]>([]);
188
146
  const isAllItemsChecked = useRef<boolean>(false);
189
147
 
@@ -225,11 +183,13 @@ export const List = <T extends Data>({
225
183
 
226
184
  const headerCheckboxHandler = useCallback(
227
185
  (checked: boolean): void => {
228
- const newListItems: ListItem<T>[] = data.map((i) => ({
229
- selected: checked,
230
- data: i,
231
- }));
232
- setListItems(newListItems);
186
+ setListItems((prevState) =>
187
+ prevState.map((i) => ({
188
+ selected: checked,
189
+ data: i.data,
190
+ })),
191
+ );
192
+
233
193
  isAllItemsChecked.current = checked;
234
194
 
235
195
  if (checked) {
@@ -243,7 +203,7 @@ export const List = <T extends Data>({
243
203
  });
244
204
  }
245
205
  },
246
- [data, onItemSelected],
206
+ [onItemSelected],
247
207
  );
248
208
 
249
209
  const itemSelectedHandler = (selected: boolean, index: number): void => {
@@ -261,6 +221,24 @@ export const List = <T extends Data>({
261
221
 
262
222
  const { sort, sortChangedHandler } = useSort(defaultSortOrder, onSortChanged);
263
223
 
224
+ useImperativeHandle(ref, () => ({
225
+ resetSelection: () => {
226
+ setListItems((prevState) =>
227
+ prevState.map((i) => ({
228
+ selected: false,
229
+ data: i.data,
230
+ })),
231
+ );
232
+
233
+ isAllItemsChecked.current = false;
234
+
235
+ onItemSelected({
236
+ mode: 'SINGLE_ITEMS',
237
+ items: [],
238
+ });
239
+ },
240
+ }));
241
+
264
242
  return (
265
243
  <div
266
244
  className={clsx(classes.wrapper, 'list-wrapper', className)}
@@ -349,13 +327,27 @@ export const List = <T extends Data>({
349
327
  );
350
328
  };
351
329
 
352
- function getActionButtonVisibility<T>(
353
- data: T,
354
- showActionButton: boolean | ((item: T) => boolean),
355
- ): boolean {
356
- if (typeof showActionButton === 'boolean') {
357
- return showActionButton;
358
- }
330
+ /**
331
+ * Renders various sets of data in a tabular format
332
+ * @example
333
+ * <List<DataInterface>
334
+ * columns={[{propertyName: 'id', size: '1fr', label: 'Id'}]}
335
+ * data={[{id: '1',desc: 'Description 1',title: 'Item 1'}]}
336
+ * itemClicked={(item)=> {console.log(item)}}
337
+ * />
338
+ */
339
+ export const List = React.forwardRef(ListRenderer);
359
340
 
360
- return showActionButton(data);
361
- }
341
+ const noItemsMessage = (
342
+ itemsCount: number,
343
+ isLoading: boolean,
344
+ isError: boolean,
345
+ ): ReactElement | undefined => {
346
+ if (!isLoading && !isError && itemsCount === 0) {
347
+ return (
348
+ <div className={clsx(classes.NoData)} data-test-id="list-empty">
349
+ <p>No items found</p>
350
+ </div>
351
+ );
352
+ }
353
+ };
@@ -0,0 +1,39 @@
1
+ import { useState } from 'react';
2
+ import { Data } from '../../types';
3
+ import { ListItem, SortData } from './List.model';
4
+
5
+ export const isTrigger = function <T extends Data>(
6
+ index: number,
7
+ listItems: ListItem<T>[],
8
+ loadingTriggerOffset: number,
9
+ ): boolean {
10
+ return listItems.length - loadingTriggerOffset === index + 1 ? true : false;
11
+ };
12
+
13
+ export const useSort = <T>(
14
+ defaultSortOrder: SortData<T> | undefined,
15
+ onSortChanged: (sort: SortData<T>) => void,
16
+ ): {
17
+ readonly sort: SortData<T> | undefined;
18
+ readonly sortChangedHandler: (sort: SortData<T>) => void;
19
+ } => {
20
+ const [sort, setSort] = useState<SortData<T> | undefined>(defaultSortOrder);
21
+
22
+ const sortChangedHandler = (sort: SortData<T>): void => {
23
+ setSort(sort);
24
+ onSortChanged && onSortChanged(sort);
25
+ };
26
+
27
+ return { sort, sortChangedHandler } as const;
28
+ };
29
+
30
+ export function getActionButtonVisibility<T>(
31
+ data: T,
32
+ showActionButton: boolean | ((item: T) => boolean),
33
+ ): boolean {
34
+ if (typeof showActionButton === 'boolean') {
35
+ return showActionButton;
36
+ }
37
+
38
+ return showActionButton(data);
39
+ }