@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.
- package/dist/components/Accordion/AccordionItem/AccordionItem.d.ts +2 -0
- package/dist/components/Accordion/AccordionItem/AccordionItem.d.ts.map +1 -1
- package/dist/components/Actions/Actions.d.ts.map +1 -1
- package/dist/components/Explorer/Explorer.d.ts.map +1 -1
- package/dist/components/Explorer/Explorer.model.d.ts +5 -0
- package/dist/components/Explorer/Explorer.model.d.ts.map +1 -1
- package/dist/components/Explorer/SelectionExplorer/SelectionExplorer.d.ts.map +1 -1
- package/dist/components/Filters/Filter/Filter.d.ts.map +1 -1
- package/dist/components/Filters/Filters.model.d.ts +7 -2
- package/dist/components/Filters/Filters.model.d.ts.map +1 -1
- package/dist/components/Filters/SelectionTypes/MultiOptionFilter/MultiOptionFilter.d.ts +13 -0
- package/dist/components/Filters/SelectionTypes/MultiOptionFilter/MultiOptionFilter.d.ts.map +1 -0
- package/dist/components/FormStation/FormStation.d.ts.map +1 -1
- package/dist/components/InfoPanel/Section/Section.d.ts +3 -1
- package/dist/components/InfoPanel/Section/Section.d.ts.map +1 -1
- package/dist/hooks/useBusy/useBusy.d.ts +4 -0
- package/dist/hooks/useBusy/useBusy.d.ts.map +1 -0
- package/dist/index.es.js +3 -3
- package/dist/index.es.js.map +1 -1
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/dist/initialize.d.ts +11 -1
- package/dist/initialize.d.ts.map +1 -1
- package/dist/types/ui-config.d.ts +7 -0
- package/dist/types/ui-config.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/components/Accordion/AccordionItem/AccordionItem.spec.tsx +11 -0
- package/src/components/Accordion/AccordionItem/AccordionItem.tsx +5 -1
- package/src/components/Actions/Actions.spec.tsx +19 -0
- package/src/components/Actions/Actions.tsx +8 -1
- package/src/components/Explorer/Explorer.model.ts +5 -0
- package/src/components/Explorer/Explorer.spec.tsx +37 -5
- package/src/components/Explorer/Explorer.tsx +5 -3
- package/src/components/Explorer/SelectionExplorer/SelectionExplorer.spec.tsx +30 -0
- package/src/components/Explorer/SelectionExplorer/SelectionExplorer.tsx +1 -0
- package/src/components/Filters/Filter/Filter.tsx +24 -0
- package/src/components/Filters/Filters.model.ts +7 -1
- package/src/components/Filters/Filters.stories.tsx +20 -0
- package/src/components/Filters/SelectionTypes/MultiOptionFilter/MultiOptionFilter.scss +40 -0
- package/src/components/Filters/SelectionTypes/MultiOptionFilter/MultiOptionFilter.tsx +71 -0
- package/src/components/FormStation/FormStation.scss +29 -0
- package/src/components/FormStation/FormStation.spec.tsx +66 -8
- package/src/components/FormStation/FormStation.tsx +5 -1
- package/src/components/InfoPanel/Paragraph/Paragraph.scss +1 -1
- package/src/components/InfoPanel/Section/Section.scss +34 -2
- package/src/components/InfoPanel/Section/Section.spec.tsx +117 -0
- package/src/components/InfoPanel/Section/Section.tsx +32 -9
- package/src/components/LandingPageTiles/TileLarge/TileLarge.scss +3 -3
- package/src/components/LandingPageTiles/TileSmall/TileSmall.scss +3 -3
- package/src/hooks/useBusy/useBusy.spec.tsx +34 -0
- package/src/hooks/useBusy/useBusy.tsx +14 -0
- package/src/initialize.ts +30 -2
- package/src/styles/variables.scss +3 -0
- package/src/types/ui-config.ts +15 -0
package/dist/initialize.d.ts
CHANGED
|
@@ -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
|
package/dist/initialize.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"initialize.d.ts","sourceRoot":"","sources":["../src/initialize.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
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.
|
|
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.
|
|
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": "
|
|
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
|
|
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
|
|
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
|
-
|
|
369
|
-
|
|
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();
|
|
@@ -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
|
+
}
|