@axinom/mosaic-ui 0.32.0 → 0.33.0-rc.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 (54) hide show
  1. package/dist/components/Accordion/AccordionItem/AccordionItem.d.ts +2 -0
  2. package/dist/components/Accordion/AccordionItem/AccordionItem.d.ts.map +1 -1
  3. package/dist/components/Actions/Actions.d.ts.map +1 -1
  4. package/dist/components/Explorer/Explorer.d.ts.map +1 -1
  5. package/dist/components/Explorer/Explorer.model.d.ts +5 -0
  6. package/dist/components/Explorer/Explorer.model.d.ts.map +1 -1
  7. package/dist/components/Explorer/SelectionExplorer/SelectionExplorer.d.ts.map +1 -1
  8. package/dist/components/Filters/Filter/Filter.d.ts.map +1 -1
  9. package/dist/components/Filters/Filters.model.d.ts +7 -2
  10. package/dist/components/Filters/Filters.model.d.ts.map +1 -1
  11. package/dist/components/Filters/SelectionTypes/MultiOptionFilter/MultiOptionFilter.d.ts +13 -0
  12. package/dist/components/Filters/SelectionTypes/MultiOptionFilter/MultiOptionFilter.d.ts.map +1 -0
  13. package/dist/components/FormStation/FormStation.d.ts.map +1 -1
  14. package/dist/components/InfoPanel/Section/Section.d.ts +3 -1
  15. package/dist/components/InfoPanel/Section/Section.d.ts.map +1 -1
  16. package/dist/hooks/useBusy/useBusy.d.ts +4 -0
  17. package/dist/hooks/useBusy/useBusy.d.ts.map +1 -0
  18. package/dist/index.es.js +3 -3
  19. package/dist/index.es.js.map +1 -1
  20. package/dist/index.js +3 -3
  21. package/dist/index.js.map +1 -1
  22. package/dist/initialize.d.ts +11 -1
  23. package/dist/initialize.d.ts.map +1 -1
  24. package/dist/types/ui-config.d.ts +7 -0
  25. package/dist/types/ui-config.d.ts.map +1 -1
  26. package/package.json +3 -3
  27. package/src/components/Accordion/AccordionItem/AccordionItem.spec.tsx +11 -0
  28. package/src/components/Accordion/AccordionItem/AccordionItem.tsx +5 -1
  29. package/src/components/Actions/Actions.spec.tsx +19 -0
  30. package/src/components/Actions/Actions.tsx +8 -1
  31. package/src/components/Explorer/Explorer.model.ts +5 -0
  32. package/src/components/Explorer/Explorer.spec.tsx +37 -5
  33. package/src/components/Explorer/Explorer.tsx +5 -3
  34. package/src/components/Explorer/SelectionExplorer/SelectionExplorer.spec.tsx +30 -0
  35. package/src/components/Explorer/SelectionExplorer/SelectionExplorer.tsx +1 -0
  36. package/src/components/Filters/Filter/Filter.tsx +24 -0
  37. package/src/components/Filters/Filters.model.ts +7 -1
  38. package/src/components/Filters/Filters.stories.tsx +20 -0
  39. package/src/components/Filters/SelectionTypes/MultiOptionFilter/MultiOptionFilter.scss +40 -0
  40. package/src/components/Filters/SelectionTypes/MultiOptionFilter/MultiOptionFilter.tsx +71 -0
  41. package/src/components/FormStation/FormStation.scss +29 -0
  42. package/src/components/FormStation/FormStation.spec.tsx +66 -8
  43. package/src/components/FormStation/FormStation.tsx +5 -1
  44. package/src/components/InfoPanel/Paragraph/Paragraph.scss +1 -1
  45. package/src/components/InfoPanel/Section/Section.scss +34 -2
  46. package/src/components/InfoPanel/Section/Section.spec.tsx +117 -0
  47. package/src/components/InfoPanel/Section/Section.tsx +32 -9
  48. package/src/components/LandingPageTiles/TileLarge/TileLarge.scss +3 -3
  49. package/src/components/LandingPageTiles/TileSmall/TileSmall.scss +3 -3
  50. package/src/hooks/useBusy/useBusy.spec.tsx +34 -0
  51. package/src/hooks/useBusy/useBusy.tsx +14 -0
  52. package/src/initialize.ts +30 -2
  53. package/src/styles/variables.scss +3 -0
  54. package/src/types/ui-config.ts +15 -0
@@ -1,5 +1,10 @@
1
- import { ShowNotification } from './types/ui-config';
1
+ import { AddIndicator, CustomEventEmitter, RemoveIndicator, ShowNotification } from './types/ui-config';
2
2
  export declare let showNotification: ShowNotification | (() => void);
3
+ export declare let addIndicator: AddIndicator | (() => void);
4
+ export declare let removeIndicator: RemoveIndicator | (() => void);
5
+ export declare let on: CustomEventEmitter['on'] | (() => void);
6
+ export declare let showSaveIndicator: () => void;
7
+ export declare let hideSaveIndicator: () => void;
3
8
  /**
4
9
  * Passes the PiralApi methods to the UI library.
5
10
  * @param app {UiConfig} object containing PiralApi methods for use in UI library.
@@ -7,5 +12,10 @@ export declare let showNotification: ShowNotification | (() => void);
7
12
  export declare function initializeUi(app: UiConfig): void;
8
13
  export interface UiConfig {
9
14
  showNotification: ShowNotification;
15
+ addIndicator: AddIndicator;
16
+ removeIndicator: RemoveIndicator;
17
+ on: CustomEventEmitter['on'];
18
+ showSaveIndicator: () => void;
19
+ hideSaveIndicator: () => void;
10
20
  }
11
21
  //# sourceMappingURL=initialize.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"initialize.d.ts","sourceRoot":"","sources":["../src/initialize.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAErD,eAAO,IAAI,gBAAgB,EAAE,gBAAgB,GAAG,CAAC,MAAM,IAAI,CAC7B,CAAC;AAE/B;;;GAGG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,QAAQ,GAAG,IAAI,CAEhD;AAED,MAAM,WAAW,QAAQ;IACvB,gBAAgB,EAAE,gBAAgB,CAAC;CACpC"}
1
+ {"version":3,"file":"initialize.d.ts","sourceRoot":"","sources":["../src/initialize.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,YAAY,EACZ,kBAAkB,EAClB,eAAe,EACf,gBAAgB,EACjB,MAAM,mBAAmB,CAAC;AAE3B,eAAO,IAAI,gBAAgB,EAAE,gBAAgB,GAAG,CAAC,MAAM,IAAI,CAC7B,CAAC;AAE/B,eAAO,IAAI,YAAY,EAAE,YAAY,GAAG,CAAC,MAAM,IAAI,CAA4B,CAAC;AAEhF,eAAO,IAAI,eAAe,EAAE,eAAe,GAAG,CAAC,MAAM,IAAI,CAC5B,CAAC;AAE9B,eAAO,IAAI,EAAE,EAAE,kBAAkB,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,IAAI,CAAkB,CAAC;AAExE,eAAO,IAAI,iBAAiB,EAAE,MAAM,IAAoC,CAAC;AAEzE,eAAO,IAAI,iBAAiB,EAAE,MAAM,IAAoC,CAAC;AAEzE;;;GAGG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,QAAQ,GAAG,IAAI,CAShD;AAED,MAAM,WAAW,QAAQ;IACvB,gBAAgB,EAAE,gBAAgB,CAAC;IACnC,YAAY,EAAE,YAAY,CAAC;IAC3B,eAAe,EAAE,eAAe,CAAC;IACjC,EAAE,EAAE,kBAAkB,CAAC,IAAI,CAAC,CAAC;IAC7B,iBAAiB,EAAE,MAAM,IAAI,CAAC;IAC9B,iBAAiB,EAAE,MAAM,IAAI,CAAC;CAC/B"}
@@ -17,4 +17,11 @@ export interface NotificationOptions {
17
17
  export type NotificationId = string | number;
18
18
  export type NotificationBody = string | ReactNode | Component;
19
19
  export type NotificationType = 'success' | 'error' | 'info' | 'warning';
20
+ export type IndicatorId = number;
21
+ export type AddIndicator = (content: React.ReactNode) => IndicatorId;
22
+ export type RemoveIndicator = (id: IndicatorId) => void;
23
+ export type UpdateIndicator = (id: IndicatorId, content: React.ReactNode) => void;
24
+ export interface CustomEventEmitter {
25
+ on(eventName: string, listener: (...args: unknown[]) => void): void;
26
+ }
20
27
  //# sourceMappingURL=ui-config.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ui-config.d.ts","sourceRoot":"","sources":["../../src/types/ui-config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAE7C,MAAM,MAAM,gBAAgB,GAAG,CAAC,YAAY,EAAE,YAAY,KAAK,cAAc,CAAC;AAE9E,MAAM,MAAM,kBAAkB,GAAG,CAC/B,EAAE,EAAE,cAAc,EAClB,YAAY,EAAE,YAAY,KACvB,IAAI,CAAC;AAEV,MAAM,MAAM,mBAAmB,GAAG,CAAC,EAAE,EAAE,cAAc,KAAK,IAAI,CAAC;AAE/D,MAAM,WAAW,YAAY;IAC3B,2CAA2C;IAC3C,KAAK,EAAE,MAAM,CAAC;IACd,wCAAwC;IACxC,IAAI,CAAC,EAAE,gBAAgB,CAAC;IACxB,8CAA8C;IAC9C,OAAO,CAAC,EAAE,mBAAmB,CAAC;CAC/B;AAED,MAAM,WAAW,mBAAmB;IAClC,IAAI,CAAC,EAAE,gBAAgB,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC;CAC5B;AAED,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG,MAAM,CAAC;AAE7C,MAAM,MAAM,gBAAgB,GAAG,MAAM,GAAG,SAAS,GAAG,SAAS,CAAC;AAE9D,MAAM,MAAM,gBAAgB,GAAG,SAAS,GAAG,OAAO,GAAG,MAAM,GAAG,SAAS,CAAC"}
1
+ {"version":3,"file":"ui-config.d.ts","sourceRoot":"","sources":["../../src/types/ui-config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAE7C,MAAM,MAAM,gBAAgB,GAAG,CAAC,YAAY,EAAE,YAAY,KAAK,cAAc,CAAC;AAE9E,MAAM,MAAM,kBAAkB,GAAG,CAC/B,EAAE,EAAE,cAAc,EAClB,YAAY,EAAE,YAAY,KACvB,IAAI,CAAC;AAEV,MAAM,MAAM,mBAAmB,GAAG,CAAC,EAAE,EAAE,cAAc,KAAK,IAAI,CAAC;AAE/D,MAAM,WAAW,YAAY;IAC3B,2CAA2C;IAC3C,KAAK,EAAE,MAAM,CAAC;IACd,wCAAwC;IACxC,IAAI,CAAC,EAAE,gBAAgB,CAAC;IACxB,8CAA8C;IAC9C,OAAO,CAAC,EAAE,mBAAmB,CAAC;CAC/B;AAED,MAAM,WAAW,mBAAmB;IAClC,IAAI,CAAC,EAAE,gBAAgB,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC;CAC5B;AAED,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG,MAAM,CAAC;AAE7C,MAAM,MAAM,gBAAgB,GAAG,MAAM,GAAG,SAAS,GAAG,SAAS,CAAC;AAE9D,MAAM,MAAM,gBAAgB,GAAG,SAAS,GAAG,OAAO,GAAG,MAAM,GAAG,SAAS,CAAC;AAExE,MAAM,MAAM,WAAW,GAAG,MAAM,CAAC;AAEjC,MAAM,MAAM,YAAY,GAAG,CAAC,OAAO,EAAE,KAAK,CAAC,SAAS,KAAK,WAAW,CAAC;AAErE,MAAM,MAAM,eAAe,GAAG,CAAC,EAAE,EAAE,WAAW,KAAK,IAAI,CAAC;AAExD,MAAM,MAAM,eAAe,GAAG,CAC5B,EAAE,EAAE,WAAW,EACf,OAAO,EAAE,KAAK,CAAC,SAAS,KACrB,IAAI,CAAC;AAEV,MAAM,WAAW,kBAAkB;IACjC,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,GAAG,IAAI,CAAC;CACrE"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@axinom/mosaic-ui",
3
- "version": "0.32.0",
3
+ "version": "0.33.0-rc.0",
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.5",
35
+ "@axinom/mosaic-core": "^0.4.6-rc.0",
36
36
  "@faker-js/faker": "^7.4.0",
37
37
  "@popperjs/core": "^2.9.2",
38
38
  "clsx": "^1.1.0",
@@ -102,5 +102,5 @@
102
102
  "publishConfig": {
103
103
  "access": "public"
104
104
  },
105
- "gitHead": "b0c12ef914e4ead795480b4fc90b13bc1088bd3f"
105
+ "gitHead": "32853a01e5b6de8cb3712ea5e1bd75780df8d59d"
106
106
  }
@@ -105,4 +105,15 @@ describe('AccordionItem', () => {
105
105
 
106
106
  expect(containerStyles).toEqual({ maxHeight: 100 });
107
107
  });
108
+
109
+ it('creates a class based off of the className prop', () => {
110
+ const mockClassName = 'test-class';
111
+ const wrapper = shallow(
112
+ <AccordionItem header={<b>Item 1</b>} className={mockClassName} />,
113
+ );
114
+
115
+ const item = wrapper.find('.test-class');
116
+
117
+ expect(item.hasClass(mockClassName)).toBe(true);
118
+ });
108
119
  });
@@ -18,6 +18,9 @@ export interface AccordionItemProps {
18
18
 
19
19
  /** Sticky state of the current row. When used within the Accordion component, this prop will be overridden with the value from stickyRows. */
20
20
  sticky?: boolean;
21
+
22
+ /** CSS Class name for additional styles */
23
+ className?: string;
21
24
  }
22
25
 
23
26
  /**
@@ -34,6 +37,7 @@ export const AccordionItem: React.FC<AccordionItemProps> = ({
34
37
  isExpanded = false,
35
38
  allowLeftSpacing = true,
36
39
  sticky = false,
40
+ className,
37
41
  }) => {
38
42
  const [scrollHeight, setScrollHeight] = useState<number>(0);
39
43
  const elementRef = useRef<HTMLDivElement>(null);
@@ -45,7 +49,7 @@ export const AccordionItem: React.FC<AccordionItemProps> = ({
45
49
  }, [scrollHeight, children]);
46
50
 
47
51
  return (
48
- <div data-test-id="accordion-item">
52
+ <div data-test-id="accordion-item" className={className}>
49
53
  <div
50
54
  data-test-id="accordion-item-row"
51
55
  className={clsx(
@@ -1,10 +1,13 @@
1
1
  import { mount, shallow } from 'enzyme';
2
2
  import React from 'react';
3
+ import { on } from '../../initialize';
3
4
  import { IconName } from '../Icons';
4
5
  import { Action } from './Action/Action';
5
6
  import { Actions } from './Actions';
6
7
  import { ActionData } from './Actions.models';
7
8
 
9
+ jest.mock('../../initialize');
10
+
8
11
  const spy1 = jest.fn();
9
12
  const spy2 = jest.fn();
10
13
  const spy3 = jest.fn();
@@ -76,4 +79,20 @@ describe('Actions', () => {
76
79
 
77
80
  expect(action.prop('action').icon).toBe(iconProp);
78
81
  });
82
+
83
+ it('disables all actions when busy', () => {
84
+ (on as jest.Mock).mockImplementationOnce((event, cb) => {
85
+ cb(true);
86
+ });
87
+
88
+ const wrapper = shallow(<Actions actions={mockActions} />);
89
+
90
+ expect(on).toHaveBeenCalledWith('busy', expect.any(Function));
91
+
92
+ const actions = wrapper.find(Action);
93
+
94
+ actions.forEach((action) => {
95
+ expect(action.prop('action').isDisabled).toBe(true);
96
+ });
97
+ });
79
98
  });
@@ -1,5 +1,6 @@
1
1
  import clsx from 'clsx';
2
2
  import React from 'react';
3
+ import { useBusy } from '../../hooks/useBusy/useBusy';
3
4
  import { Action } from './Action/Action';
4
5
  import { ActionData, ContextActionData } from './Actions.models';
5
6
  import classes from './Actions.scss';
@@ -34,6 +35,8 @@ export const Actions: React.FC<ActionsProps> = ({
34
35
  width: width,
35
36
  } as React.CSSProperties;
36
37
 
38
+ const { isBusy } = useBusy();
39
+
37
40
  return (
38
41
  <div
39
42
  className={clsx(classes.container, 'actions-container', className)}
@@ -41,7 +44,11 @@ export const Actions: React.FC<ActionsProps> = ({
41
44
  data-test-id="actions"
42
45
  >
43
46
  {actions.map((action, index) => (
44
- <Action key={index} action={action} onActionClick={onActionClick} />
47
+ <Action
48
+ key={index}
49
+ action={{ isDisabled: isBusy, ...action }}
50
+ onActionClick={onActionClick}
51
+ />
45
52
  ))}
46
53
  </div>
47
54
  );
@@ -36,6 +36,11 @@ export interface ExplorerBulkAction<T extends Data>
36
36
  * Whether the Explorer should reload the data once the bulk action is completed
37
37
  */
38
38
  reloadData?: boolean;
39
+ /**
40
+ * Whether or not to show toast notification once the bulk action is started
41
+ * (shown by default)
42
+ */
43
+ showStartedNotification?: boolean;
39
44
  }
40
45
 
41
46
  export type PageIdentifier = string | number | undefined;
@@ -270,7 +270,7 @@ describe('Explorer', () => {
270
270
  const header = wrapper.find(PageHeader);
271
271
 
272
272
  await act(async () => {
273
- header.prop('bulkActions')?.[0].onClick();
273
+ header.prop('bulkActions')?.[0].onClick?.();
274
274
 
275
275
  await wrapper.update();
276
276
  });
@@ -280,7 +280,7 @@ describe('Explorer', () => {
280
280
  });
281
281
 
282
282
  it('Calls "showNotification" when bulk action is clicked', async () => {
283
- const [provider, spy] = getDataProvider();
283
+ const [provider] = getDataProvider();
284
284
 
285
285
  const label = 'Something';
286
286
  const wrapper = await actWithReturn(async () => {
@@ -303,7 +303,7 @@ describe('Explorer', () => {
303
303
 
304
304
  const header = wrapper.find(PageHeader);
305
305
  await act(async () => {
306
- header.prop('bulkActions')?.[0].onClick();
306
+ header.prop('bulkActions')?.[0].onClick?.();
307
307
  await wrapper.update();
308
308
  });
309
309
 
@@ -313,6 +313,38 @@ describe('Explorer', () => {
313
313
  });
314
314
  });
315
315
 
316
+ it('Does not call "showNotification" when `showStartedNotification=false` and bulk action is clicked', async () => {
317
+ const [provider] = getDataProvider();
318
+
319
+ const label = 'Something';
320
+ const wrapper = await actWithReturn(async () => {
321
+ const wrapper = mount(
322
+ <Explorer
323
+ columns={mockListColumns}
324
+ dataProvider={provider}
325
+ stationKey="mock-key"
326
+ bulkActions={[
327
+ {
328
+ label,
329
+ onClick: jest.fn(),
330
+ reloadData: true,
331
+ showStartedNotification: false,
332
+ },
333
+ ]}
334
+ />,
335
+ );
336
+ return wrapper;
337
+ });
338
+
339
+ const header = wrapper.find(PageHeader);
340
+ await act(async () => {
341
+ header.prop('bulkActions')?.[0].onClick?.();
342
+ await wrapper.update();
343
+ });
344
+
345
+ expect(showNotificationSpy).toHaveBeenCalledTimes(0);
346
+ });
347
+
316
348
  it.todo(`raises page header bulk action with 'SELECT_ALL'`);
317
349
 
318
350
  it.todo(`raises page header bulk action with 'SINGLE_ITEMS'`);
@@ -1672,7 +1704,7 @@ describe('Explorer', () => {
1672
1704
  const menu = wrapper.find(InlineMenu);
1673
1705
 
1674
1706
  await act(async () => {
1675
- await menu.prop('actions')?.[0].onActionSelected();
1707
+ await menu.prop('actions')?.[0].onActionSelected?.();
1676
1708
  await wrapper.update();
1677
1709
  });
1678
1710
 
@@ -1728,7 +1760,7 @@ describe('Explorer', () => {
1728
1760
  const menu = wrapper.find(InlineMenu);
1729
1761
 
1730
1762
  await act(async () => {
1731
- await menu.prop('actions')?.[0].onActionSelected();
1763
+ await menu.prop('actions')?.[0].onActionSelected?.();
1732
1764
  await wrapper.update();
1733
1765
  });
1734
1766
 
@@ -365,9 +365,11 @@ export const Explorer = React.forwardRef(function Explorer<T extends Data>(
365
365
  return {
366
366
  ...action,
367
367
  onClick: async () => {
368
- showNotification({
369
- title: `Bulk Action '${action.label}' Started`,
370
- });
368
+ if (action.showStartedNotification !== false) {
369
+ showNotification({
370
+ title: `Bulk Action '${action.label}' Started`,
371
+ });
372
+ }
371
373
 
372
374
  try {
373
375
  const result = await action.onClick(getBulkActionSelection());
@@ -2,7 +2,9 @@ import { mount, shallow } from 'enzyme';
2
2
  import React from 'react';
3
3
  import { act } from 'react-dom/test-utils';
4
4
  import { actWithReturn } from '../../../helpers/testing';
5
+ import * as app from '../../../initialize';
5
6
  import { Column } from '../../List';
7
+ import { PageHeader } from '../../PageHeader';
6
8
  import { PageHeaderBulkActions } from '../../PageHeader/PageHeaderBulkActions/PageHeaderBulkActions';
7
9
  import { Explorer } from '../Explorer';
8
10
  import { ExplorerDataProvider } from '../Explorer.model';
@@ -103,6 +105,34 @@ describe('SelectionExplorer', () => {
103
105
  expect(bulkActions.exists()).toBe(false);
104
106
  });
105
107
 
108
+ it('Does not call "showNotification" when "Apply Selection" is clicked', async () => {
109
+ const [provider] = getDataProvider();
110
+ const showNotificationSpy: jest.SpyInstance = jest.spyOn(
111
+ app,
112
+ 'showNotification',
113
+ );
114
+
115
+ const wrapper = await actWithReturn(async () => {
116
+ const wrapper = mount(
117
+ <SelectionExplorer
118
+ columns={mockListColumns}
119
+ dataProvider={provider}
120
+ stationKey="mock-key"
121
+ allowBulkSelect={true}
122
+ />,
123
+ );
124
+ return wrapper;
125
+ });
126
+
127
+ const header = wrapper.find(PageHeader);
128
+ await act(async () => {
129
+ header.prop('bulkActions')?.[0].onClick?.();
130
+ await wrapper.update();
131
+ });
132
+
133
+ expect(showNotificationSpy).toHaveBeenCalledTimes(0);
134
+ });
135
+
106
136
  it('sends onSelection callback when the selection of a single item is triggered', async () => {
107
137
  const [provider] = getDataProvider();
108
138
  const spy = jest.fn();
@@ -68,6 +68,7 @@ export const SelectionExplorer = React.forwardRef(function SelectionExplorer<
68
68
  onClick: ((arg: ItemSelection<T>) => {
69
69
  onSelection(arg);
70
70
  }) as ExplorerBulkAction<T>['onClick'],
71
+ showStartedNotification: false,
71
72
  },
72
73
  ]
73
74
  : []),
@@ -14,6 +14,7 @@ import { formatDate, formatDateTime } from '../../Utils/Transformers/DateTime';
14
14
  import { FilterType, FilterTypes, FilterValue } from '../Filters.model';
15
15
  import { DateTimeFilter } from '../SelectionTypes/DateTimeFilter/DateTimeFilter';
16
16
  import { FreeTextFilter } from '../SelectionTypes/FreeTextFilter/FreeTextFilter';
17
+ import { MultiOptionsFilter } from '../SelectionTypes/MultiOptionFilter/MultiOptionFilter';
17
18
  import { NumericTextFilter } from '../SelectionTypes/NumericTextFilter/NumericTextFilter';
18
19
  import { OptionsFilter } from '../SelectionTypes/OptionsFilter/OptionsFilter';
19
20
  import { SearcheableOptionsFilter } from '../SelectionTypes/SearcheableOptionsFilter/SearcheableOptionsFilter';
@@ -78,6 +79,19 @@ export const Filter = <T extends Data>({
78
79
  return formatDateTime(String(value));
79
80
  case FilterTypes.Options:
80
81
  return options.options.find((option) => option.value === value)?.label;
82
+ case FilterTypes.MultipleOptions: {
83
+ const selectedVal: string[] = [];
84
+ String(value)
85
+ .split(',')
86
+ .map((item: string) =>
87
+ selectedVal.push(
88
+ options?.options.find(
89
+ (option) => option.value.toString() === item,
90
+ )?.label ?? '',
91
+ ),
92
+ );
93
+ return selectedVal.join(', ');
94
+ }
81
95
  case FilterTypes.SearcheableOptions:
82
96
  return stringValue;
83
97
  default:
@@ -122,6 +136,16 @@ export const Filter = <T extends Data>({
122
136
  />
123
137
  );
124
138
 
139
+ case FilterTypes.MultipleOptions:
140
+ return (
141
+ <MultiOptionsFilter
142
+ value={value as string}
143
+ options={options.options}
144
+ onSelect={(value: FilterValue) =>
145
+ onFilterValueChange(options.property, value?.toString())
146
+ }
147
+ />
148
+ );
125
149
  case FilterTypes.SearcheableOptions:
126
150
  return (
127
151
  <SearcheableOptionsFilter
@@ -8,6 +8,7 @@ export enum FilterTypes {
8
8
  Date,
9
9
  DateTime,
10
10
  Custom,
11
+ MultipleOptions,
11
12
  }
12
13
 
13
14
  export interface FilterConfig<T extends Data> {
@@ -36,6 +37,10 @@ export interface OptionsFilter<T extends Data> extends FilterConfig<T> {
36
37
  options: Option[];
37
38
  }
38
39
 
40
+ export interface MultipleOptions<T extends Data> extends FilterConfig<T> {
41
+ type: FilterTypes.MultipleOptions;
42
+ options: Option[];
43
+ }
39
44
  export interface SearcheableOptionsFilter<T extends Data>
40
45
  extends FilterConfig<T> {
41
46
  type: FilterTypes.SearcheableOptions;
@@ -87,7 +92,8 @@ export type FilterType<T extends Data> =
87
92
  | SearcheableOptionsFilter<T>
88
93
  | DateFilter<T>
89
94
  | DateTimeFilter<T>
90
- | CustomFilter<T>;
95
+ | CustomFilter<T>
96
+ | MultipleOptions<T>;
91
97
 
92
98
  export type FilterValues<T> = {
93
99
  [K in keyof T]?: FilterValue;
@@ -43,6 +43,7 @@ interface FilterStoryData {
43
43
  datetime: Date;
44
44
  id: number;
45
45
  holderType: string;
46
+ roleTypes: string;
46
47
  }
47
48
 
48
49
  const options: Option[] = generateItemArray(10, (index) => ({
@@ -55,6 +56,11 @@ const searcheableOptions: Option[] = generateItemArray(10, (index) => ({
55
56
  value: index,
56
57
  }));
57
58
 
59
+ const multipleOptions: Option[] = generateItemArray(10, (index) => ({
60
+ label: faker.lorem.words(),
61
+ value: index,
62
+ }));
63
+
58
64
  const textFilter: FilterType<FilterStoryData> = {
59
65
  label: 'Title',
60
66
  property: 'title',
@@ -76,6 +82,13 @@ const searcheableOptionFilter: FilterType<FilterStoryData> = {
76
82
  searcheableOptions.filter((option) => option.label.includes(searchText)),
77
83
  };
78
84
 
85
+ const multipleOptionFilter: FilterType<FilterStoryData> = {
86
+ label: 'Role Types',
87
+ property: 'roleTypes',
88
+ type: FilterTypes.MultipleOptions,
89
+ options: multipleOptions,
90
+ };
91
+
79
92
  const numberFilter: FilterType<FilterStoryData> = {
80
93
  label: 'ID',
81
94
  property: 'id',
@@ -167,6 +180,12 @@ export const searchableOptionsFilter: StoryObj<typeof Filters> = {
167
180
  },
168
181
  };
169
182
 
183
+ export const multiOptionFilter: StoryObj<typeof Filters> = {
184
+ args: {
185
+ options: [multipleOptionFilter],
186
+ },
187
+ };
188
+
170
189
  export const DateFilter: StoryObj<typeof Filters> = {
171
190
  args: {
172
191
  options: [dateFilter, dateTimeFilter],
@@ -196,6 +215,7 @@ export const MultipleFilters: StoryObj<typeof Filters> = {
196
215
  dateTimeFilter,
197
216
  customFilter,
198
217
  searcheableOptionFilter,
218
+ multipleOptionFilter,
199
219
  ]}
200
220
  defaultValues={filters}
201
221
  onFiltersChange={(filters) => {
@@ -0,0 +1,40 @@
1
+ @import '../../../../styles/common.scss';
2
+
3
+ .multiFilterContainer {
4
+ padding: 0;
5
+ .Checkbox {
6
+ padding-left: 20px;
7
+ padding-right: 20px;
8
+ grid-template-columns: auto 30px;
9
+ label {
10
+ color: var(--multi-option-label-colorr, $multi-option-label-color);
11
+ font-weight: normal;
12
+ }
13
+ input {
14
+ border-color: var(
15
+ --multi-option-checbox-border,
16
+ $multi-option-checbox-border
17
+ );
18
+ }
19
+ &:hover {
20
+ background-color: var(
21
+ --filter-background-selected-color,
22
+ $filter-background-selected-color
23
+ );
24
+ }
25
+ &.selected {
26
+ background-color: var(
27
+ --filter-background-selected-color,
28
+ $filter-background-selected-color
29
+ );
30
+ }
31
+ }
32
+ }
33
+
34
+ .applyButtonContainer {
35
+ display: grid;
36
+
37
+ .applyButton {
38
+ height: 50px;
39
+ }
40
+ }
@@ -0,0 +1,71 @@
1
+ import clsx from 'clsx';
2
+ import React, { useState } from 'react';
3
+ import { ButtonContext } from '../../../Buttons';
4
+ import { TextButton } from '../../../Buttons/TextButton/TextButton';
5
+ import { Checkbox } from '../../../FormElements';
6
+ import { Option } from '../../Filters.model';
7
+ import classes from './MultiOptionFilter.scss';
8
+
9
+ export interface MultiOptionFilterProps {
10
+ value?: string;
11
+
12
+ /** Array of Options to be displayed */
13
+ options: Option[];
14
+
15
+ /** Callback triggered when a new filter value is selected */
16
+ onSelect: (text: string[]) => void;
17
+
18
+ /** CSS Class name for additional styles */
19
+ className?: string;
20
+ }
21
+
22
+ export const MultiOptionsFilter: React.FC<MultiOptionFilterProps> = ({
23
+ value,
24
+ options,
25
+ onSelect,
26
+ }) => {
27
+ const [selectedOptionList, setSelectedOptionList] = useState<string[]>(
28
+ value ? value.split(',') : [],
29
+ );
30
+
31
+ return (
32
+ <div className={clsx(classes.multiFilterContainer)}>
33
+ {options?.map((option: Option) => (
34
+ <Checkbox
35
+ className={clsx(
36
+ classes.Checkbox,
37
+ selectedOptionList.includes(option.value.toString()) &&
38
+ classes.selected,
39
+ )}
40
+ key={option.value}
41
+ name={option.label}
42
+ value={selectedOptionList.includes(option.value.toString())}
43
+ label={option.label}
44
+ onChange={(value) => {
45
+ if (value) {
46
+ setSelectedOptionList([
47
+ ...selectedOptionList,
48
+ option.value.toString(),
49
+ ]);
50
+ } else {
51
+ setSelectedOptionList(
52
+ [...selectedOptionList].filter(
53
+ (i) => i !== option.value.toString(),
54
+ ),
55
+ );
56
+ }
57
+ }}
58
+ />
59
+ ))}
60
+ <div className={clsx(classes.applyButtonContainer)}>
61
+ <TextButton
62
+ className={clsx(classes.applyButton)}
63
+ text="Apply"
64
+ buttonContext={ButtonContext.Active}
65
+ onButtonClicked={() => onSelect(selectedOptionList)}
66
+ disabled={selectedOptionList.length === 0}
67
+ />
68
+ </div>
69
+ </div>
70
+ );
71
+ };
@@ -122,3 +122,32 @@
122
122
  row-gap: 20px;
123
123
  }
124
124
  }
125
+
126
+ .indicator {
127
+ display: grid;
128
+ grid-template-columns: auto 20px;
129
+ font-weight: bold;
130
+ color: var(--form-indicator-color, $form-indicator-color);
131
+
132
+ &::after {
133
+ overflow: hidden;
134
+ display: inline-block;
135
+ vertical-align: bottom;
136
+ -webkit-animation: ellipsis steps(4, end) 900ms infinite;
137
+ animation: ellipsis steps(4, end) 900ms infinite;
138
+ content: '\2026'; /* ascii code for the ellipsis character */
139
+ width: 0px;
140
+ }
141
+ }
142
+
143
+ @keyframes ellipsis {
144
+ to {
145
+ width: 1.25em;
146
+ }
147
+ }
148
+
149
+ @-webkit-keyframes ellipsis {
150
+ to {
151
+ width: 1.25em;
152
+ }
153
+ }