@axinom/mosaic-ui 0.51.0-rc.9 → 0.52.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/Explorer/ConditionalSplit/ConditionalSplit.d.ts +8 -0
- package/dist/components/Explorer/ConditionalSplit/ConditionalSplit.d.ts.map +1 -0
- package/dist/components/Explorer/Explorer.d.ts +3 -1
- package/dist/components/Explorer/Explorer.d.ts.map +1 -1
- package/dist/components/Explorer/Explorer.model.d.ts +18 -1
- package/dist/components/Explorer/Explorer.model.d.ts.map +1 -1
- package/dist/components/Explorer/QuickEdit/QuickEditContext.d.ts +11 -0
- package/dist/components/Explorer/QuickEdit/QuickEditContext.d.ts.map +1 -0
- package/dist/components/Explorer/QuickEdit/useQuickEdit.d.ts +22 -0
- package/dist/components/Explorer/QuickEdit/useQuickEdit.d.ts.map +1 -0
- package/dist/components/Explorer/{InMemoryDataProvider.d.ts → helpers/InMemoryDataProvider.d.ts} +3 -3
- package/dist/components/Explorer/helpers/InMemoryDataProvider.d.ts.map +1 -0
- package/dist/components/Explorer/helpers/useActions.d.ts +31 -0
- package/dist/components/Explorer/helpers/useActions.d.ts.map +1 -0
- package/dist/components/Explorer/{useDataProvider.d.ts → helpers/useDataProvider.d.ts} +6 -6
- package/dist/components/Explorer/helpers/useDataProvider.d.ts.map +1 -0
- package/dist/components/Explorer/helpers/useFilters.d.ts +21 -0
- package/dist/components/Explorer/helpers/useFilters.d.ts.map +1 -0
- package/dist/components/Explorer/helpers/useStationMessage.d.ts +17 -0
- package/dist/components/Explorer/helpers/useStationMessage.d.ts.map +1 -0
- package/dist/components/Explorer/index.d.ts +2 -1
- package/dist/components/Explorer/index.d.ts.map +1 -1
- package/dist/components/FormStation/Create/Create.d.ts.map +1 -1
- package/dist/components/FormStation/FormStation.d.ts +4 -1
- package/dist/components/FormStation/FormStation.d.ts.map +1 -1
- package/dist/components/FormStation/FormStationHeader/FormStationHeader.d.ts +1 -0
- package/dist/components/FormStation/FormStationHeader/FormStationHeader.d.ts.map +1 -1
- package/dist/components/FormStation/SaveOnDemand/SaveOnDemand.d.ts +11 -0
- package/dist/components/FormStation/SaveOnDemand/SaveOnDemand.d.ts.map +1 -0
- package/dist/components/FormStation/helpers/useDataProvider.d.ts.map +1 -1
- package/dist/components/Icons/Icons.d.ts.map +1 -1
- package/dist/components/Icons/Icons.models.d.ts +28 -24
- package/dist/components/Icons/Icons.models.d.ts.map +1 -1
- package/dist/components/List/List.d.ts +1 -1
- package/dist/components/List/List.d.ts.map +1 -1
- package/dist/components/List/List.model.d.ts +4 -0
- package/dist/components/List/List.model.d.ts.map +1 -1
- package/dist/components/PageHeader/PageHeader.d.ts.map +1 -1
- package/dist/components/PageHeader/PageHeaderAction/PageHeaderAction.d.ts.map +1 -1
- package/dist/components/PageHeader/PageHeaderAction/PageHeaderAction.model.d.ts +2 -1
- package/dist/components/PageHeader/PageHeaderAction/PageHeaderAction.model.d.ts.map +1 -1
- package/dist/components/PageHeader/PageHeaderActionsGroup/PageHeaderActionsGroup.d.ts +1 -1
- package/dist/components/PageHeader/PageHeaderActionsGroup/PageHeaderActionsGroup.d.ts.map +1 -1
- package/dist/components/PageHeader/helpers/useElementWidthObserver.d.ts +6 -0
- package/dist/components/PageHeader/helpers/useElementWidthObserver.d.ts.map +1 -0
- package/dist/index.es.js +4 -4
- package/dist/index.es.js.map +1 -1
- package/dist/index.js +4 -4
- package/dist/index.js.map +1 -1
- package/dist/initialize.d.ts +1 -1
- package/dist/initialize.d.ts.map +1 -1
- package/package.json +4 -3
- package/src/components/EmptyStation/EmptyStation.spec.tsx +24 -0
- package/src/components/Explorer/ConditionalSplit/ConditionalSplit.tsx +23 -0
- package/src/components/Explorer/Explorer.model.ts +19 -1
- package/src/components/Explorer/Explorer.scss +4 -0
- package/src/components/Explorer/Explorer.spec.tsx +28 -3
- package/src/components/Explorer/Explorer.stories.tsx +90 -5
- package/src/components/Explorer/Explorer.tsx +149 -185
- package/src/components/Explorer/NavigationExplorer/NavigationExplorer.spec.tsx +26 -0
- package/src/components/Explorer/NavigationExplorer/NavigationExplorer.stories.tsx +2 -2
- package/src/components/Explorer/QuickEdit/QuickEditContext.tsx +16 -0
- package/src/components/Explorer/QuickEdit/useQuickEdit.spec.tsx +461 -0
- package/src/components/Explorer/QuickEdit/useQuickEdit.tsx +169 -0
- package/src/components/Explorer/SelectionExplorer/SelectionExplorer.spec.tsx +6 -0
- package/src/components/Explorer/SelectionExplorer/SelectionExplorer.stories.tsx +2 -2
- package/src/components/Explorer/{InMemoryDataProvider.ts → helpers/InMemoryDataProvider.ts} +4 -4
- package/src/components/Explorer/helpers/useActions.ts +203 -0
- package/src/components/Explorer/{useDataProvider.tsx → helpers/useDataProvider.tsx} +11 -11
- package/src/components/Explorer/helpers/useFilters.tsx +77 -0
- package/src/components/Explorer/{useStationMessage.tsx → helpers/useStationMessage.tsx} +8 -6
- package/src/components/Explorer/index.ts +10 -6
- package/src/components/FormStation/Create/Create.tsx +1 -0
- package/src/components/FormStation/FormStation.spec.tsx +62 -73
- package/src/components/FormStation/FormStation.tsx +31 -15
- package/src/components/FormStation/FormStationHeader/FormStationHeader.tsx +38 -18
- package/src/components/FormStation/SaveOnDemand/SaveOnDemand.tsx +55 -0
- package/src/components/FormStation/helpers/useDataProvider.ts +1 -8
- package/src/components/Icons/Icons.models.ts +4 -0
- package/src/components/Icons/Icons.tsx +78 -0
- package/src/components/InlineMenu/InlineMenu.spec.tsx +18 -0
- package/src/components/List/List.model.ts +5 -0
- package/src/components/List/List.tsx +29 -5
- package/src/components/List/ListRow/ListRow.spec.tsx +0 -10
- package/src/components/List/ListRow/ListRow.tsx +1 -1
- package/src/components/PageHeader/PageHeader.scss +1 -2
- package/src/components/PageHeader/PageHeader.stories.tsx +6 -2
- package/src/components/PageHeader/PageHeader.tsx +10 -16
- package/src/components/PageHeader/PageHeaderAction/PageHeaderAction.model.ts +1 -0
- package/src/components/PageHeader/PageHeaderAction/PageHeaderAction.scss +7 -0
- package/src/components/PageHeader/PageHeaderAction/PageHeaderAction.tsx +1 -0
- package/src/components/PageHeader/PageHeaderActionsGroup/PageHeaderActionsGroup.spec.tsx +19 -7
- package/src/components/PageHeader/PageHeaderActionsGroup/PageHeaderActionsGroup.tsx +19 -12
- package/src/components/PageHeader/helpers/useElementWidthObserver.tsx +30 -0
- package/src/initialize.ts +2 -2
- package/dist/components/Explorer/InMemoryDataProvider.d.ts.map +0 -1
- package/dist/components/Explorer/useDataProvider.d.ts.map +0 -1
- package/dist/components/Explorer/useStationMessage.d.ts +0 -15
- package/dist/components/Explorer/useStationMessage.d.ts.map +0 -1
- /package/src/components/Explorer/{InMemoryDataProvider.spec.ts → helpers/InMemoryDataProvider.spec.ts} +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { FormikValues, useFormikContext } from 'formik';
|
|
2
|
-
import React, {
|
|
2
|
+
import React, { useContext, useMemo } from 'react';
|
|
3
3
|
import { useHistory } from 'react-router-dom';
|
|
4
|
-
import {
|
|
4
|
+
import { QuickEditContext } from '../../Explorer';
|
|
5
5
|
import { IconName } from '../../Icons';
|
|
6
6
|
import {
|
|
7
7
|
PageHeader,
|
|
@@ -20,6 +20,7 @@ export const FormStationHeader: React.FC<
|
|
|
20
20
|
defaultTitle?: string;
|
|
21
21
|
cancelNavigationUrl?: string;
|
|
22
22
|
setTabTitle?: boolean;
|
|
23
|
+
showSaveHeaderAction?: boolean;
|
|
23
24
|
}
|
|
24
25
|
> = ({
|
|
25
26
|
titleProperty,
|
|
@@ -28,23 +29,10 @@ export const FormStationHeader: React.FC<
|
|
|
28
29
|
cancelNavigationUrl,
|
|
29
30
|
className,
|
|
30
31
|
setTabTitle,
|
|
32
|
+
showSaveHeaderAction,
|
|
31
33
|
}) => {
|
|
32
34
|
const { dirty, resetForm } = useFormikContext<FormikValues>();
|
|
33
|
-
|
|
34
|
-
useEffect(() => {
|
|
35
|
-
// Set the save indicator to dirty depending on the form state
|
|
36
|
-
if (dirty) {
|
|
37
|
-
setSaveIndicator(SaveIndicatorType.Dirty);
|
|
38
|
-
} else {
|
|
39
|
-
setSaveIndicator(SaveIndicatorType.Inactive);
|
|
40
|
-
}
|
|
41
|
-
return () => {
|
|
42
|
-
// The form is not always considered "not dirty" after the save
|
|
43
|
-
// so this code will make sure that the indicator is set to inactive
|
|
44
|
-
// when the station is left.
|
|
45
|
-
setSaveIndicator(SaveIndicatorType.Inactive);
|
|
46
|
-
};
|
|
47
|
-
}, [dirty]);
|
|
35
|
+
const quickEditContext = useContext(QuickEditContext);
|
|
48
36
|
|
|
49
37
|
const history = useHistory();
|
|
50
38
|
|
|
@@ -63,6 +51,31 @@ export const FormStationHeader: React.FC<
|
|
|
63
51
|
resetForm();
|
|
64
52
|
},
|
|
65
53
|
});
|
|
54
|
+
|
|
55
|
+
if (showSaveHeaderAction) {
|
|
56
|
+
actionItems.push({
|
|
57
|
+
label: 'Save',
|
|
58
|
+
icon: IconName.Save,
|
|
59
|
+
kind: 'action',
|
|
60
|
+
actionType: PageHeaderActionType.Context,
|
|
61
|
+
onClick: () => {
|
|
62
|
+
if (quickEditContext?.isQuickEditMode) {
|
|
63
|
+
quickEditContext.refresh();
|
|
64
|
+
} else {
|
|
65
|
+
history.replace(history.location.pathname);
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (quickEditContext?.detailsLink !== undefined) {
|
|
73
|
+
actionItems.push({
|
|
74
|
+
label: 'Open',
|
|
75
|
+
icon: IconName.ChevronRight,
|
|
76
|
+
kind: 'action',
|
|
77
|
+
path: quickEditContext.detailsLink,
|
|
78
|
+
});
|
|
66
79
|
}
|
|
67
80
|
|
|
68
81
|
if (cancelNavigationUrl) {
|
|
@@ -79,7 +92,14 @@ export const FormStationHeader: React.FC<
|
|
|
79
92
|
}
|
|
80
93
|
|
|
81
94
|
return actionItems;
|
|
82
|
-
}, [
|
|
95
|
+
}, [
|
|
96
|
+
cancelNavigationUrl,
|
|
97
|
+
dirty,
|
|
98
|
+
history,
|
|
99
|
+
quickEditContext,
|
|
100
|
+
resetForm,
|
|
101
|
+
showSaveHeaderAction,
|
|
102
|
+
]);
|
|
83
103
|
|
|
84
104
|
return (
|
|
85
105
|
<PageHeader
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { useFormikContext } from 'formik';
|
|
2
|
+
import { useEffect, useState } from 'react';
|
|
3
|
+
|
|
4
|
+
interface SaveOnDemandProps {
|
|
5
|
+
/** If set to true, will prevent form submission when navigating away. (default: false) */
|
|
6
|
+
isSubmitting?: boolean;
|
|
7
|
+
/** Callback that will be called when the form is invalid */
|
|
8
|
+
onFormInvalid?: () => void;
|
|
9
|
+
|
|
10
|
+
registerSaveCallback?: (callback: () => Promise<void>) => void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const SaveOnDemand: React.FC<SaveOnDemandProps> = ({
|
|
14
|
+
onFormInvalid,
|
|
15
|
+
isSubmitting,
|
|
16
|
+
registerSaveCallback,
|
|
17
|
+
}) => {
|
|
18
|
+
const { submitForm, dirty, isValid } = useFormikContext();
|
|
19
|
+
const [canSubmit, setCanSubmit] = useState<boolean>(true);
|
|
20
|
+
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
const saveCallback = async (): Promise<void> => {
|
|
23
|
+
if (!dirty) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (!isValid) {
|
|
28
|
+
onFormInvalid?.();
|
|
29
|
+
throw new Error('Form is not valid');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
if (!isSubmitting && canSubmit) {
|
|
34
|
+
setCanSubmit(false);
|
|
35
|
+
await submitForm();
|
|
36
|
+
}
|
|
37
|
+
} catch (error) {
|
|
38
|
+
setCanSubmit(true);
|
|
39
|
+
throw error;
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
registerSaveCallback?.(saveCallback);
|
|
44
|
+
}, [
|
|
45
|
+
canSubmit,
|
|
46
|
+
dirty,
|
|
47
|
+
isSubmitting,
|
|
48
|
+
isValid,
|
|
49
|
+
onFormInvalid,
|
|
50
|
+
registerSaveCallback,
|
|
51
|
+
submitForm,
|
|
52
|
+
]);
|
|
53
|
+
|
|
54
|
+
return null;
|
|
55
|
+
};
|
|
@@ -7,11 +7,7 @@ import {
|
|
|
7
7
|
useRef,
|
|
8
8
|
useState,
|
|
9
9
|
} from 'react';
|
|
10
|
-
import {
|
|
11
|
-
SaveIndicatorType,
|
|
12
|
-
setSaveIndicator,
|
|
13
|
-
showNotification,
|
|
14
|
-
} from '../../../initialize';
|
|
10
|
+
import { showNotification } from '../../../initialize';
|
|
15
11
|
import { Data } from '../../../types';
|
|
16
12
|
import { ErrorTypeToStationError } from '../../../utils/ErrorTypeToStationError';
|
|
17
13
|
import { ErrorType } from '../../models';
|
|
@@ -88,7 +84,6 @@ export const useDataProvider: FormStationDataProvider = <
|
|
|
88
84
|
|
|
89
85
|
try {
|
|
90
86
|
setIsFormSubmitting(true);
|
|
91
|
-
setSaveIndicator(SaveIndicatorType.Saving);
|
|
92
87
|
setStationError(undefined);
|
|
93
88
|
if (!initialData.loading && saveData) {
|
|
94
89
|
const response = await saveData(values, initialData, formikHelpers);
|
|
@@ -112,8 +107,6 @@ export const useDataProvider: FormStationDataProvider = <
|
|
|
112
107
|
),
|
|
113
108
|
);
|
|
114
109
|
|
|
115
|
-
setSaveIndicator(SaveIndicatorType.Dirty);
|
|
116
|
-
|
|
117
110
|
// We still throw the error, to make sure that navigation or action execution
|
|
118
111
|
// will not continue after a failed save.
|
|
119
112
|
throw error;
|
|
@@ -22,6 +22,7 @@ export enum IconName {
|
|
|
22
22
|
Error,
|
|
23
23
|
External,
|
|
24
24
|
File,
|
|
25
|
+
Filters,
|
|
25
26
|
ForwardOne,
|
|
26
27
|
ForwardTen,
|
|
27
28
|
Info,
|
|
@@ -34,9 +35,12 @@ export enum IconName {
|
|
|
34
35
|
Play,
|
|
35
36
|
Plus,
|
|
36
37
|
Publish,
|
|
38
|
+
QuickEdit,
|
|
39
|
+
QuickEditStation,
|
|
37
40
|
RemoveFilter,
|
|
38
41
|
Retry,
|
|
39
42
|
Replace,
|
|
43
|
+
Save,
|
|
40
44
|
Snapshot,
|
|
41
45
|
Stop,
|
|
42
46
|
Success,
|
|
@@ -331,6 +331,22 @@ const FileIcon: React.FC<{ className?: string }> = ({ className }) => (
|
|
|
331
331
|
</svg>
|
|
332
332
|
);
|
|
333
333
|
|
|
334
|
+
const FiltersIcon: React.FC<{ className?: string }> = ({ className }) => (
|
|
335
|
+
<svg
|
|
336
|
+
className={clsx(classes.icons, className)}
|
|
337
|
+
version="1.1"
|
|
338
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
339
|
+
viewBox="0 0 40 40"
|
|
340
|
+
>
|
|
341
|
+
<path
|
|
342
|
+
vectorEffect="non-scaling-stroke"
|
|
343
|
+
fill="none"
|
|
344
|
+
strokeWidth="2"
|
|
345
|
+
d="M5.7,4.7h28.6l-11.3,12v13.6l-6.5,5.1v-18.4L5.7,4.7Z"
|
|
346
|
+
/>
|
|
347
|
+
</svg>
|
|
348
|
+
);
|
|
349
|
+
|
|
334
350
|
const ForwardOne: React.FC<{ className?: string }> = ({ className }) => (
|
|
335
351
|
<svg
|
|
336
352
|
className={clsx(classes.icons, className)}
|
|
@@ -528,6 +544,40 @@ const PublishIcon: React.FC<{ className?: string }> = ({ className }) => (
|
|
|
528
544
|
</svg>
|
|
529
545
|
);
|
|
530
546
|
|
|
547
|
+
const QuickEditIcon: React.FC<{ className?: string }> = ({ className }) => (
|
|
548
|
+
<svg
|
|
549
|
+
className={clsx(classes.icons, className)}
|
|
550
|
+
version="1.1"
|
|
551
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
552
|
+
viewBox="0 0 40 40"
|
|
553
|
+
>
|
|
554
|
+
<path
|
|
555
|
+
vectorEffect="non-scaling-stroke"
|
|
556
|
+
fill="none"
|
|
557
|
+
strokeWidth="2"
|
|
558
|
+
d="M11.8,29.7l-8.8,2.6,2.6-8.8,15.8-15.8,6.2,6.2-15.8,15.8h0ZM27.5,19.7h10.5M22.7,24.3h8.4M18,29h6.2M5.6,23.5l6.2,6.2"
|
|
559
|
+
/>
|
|
560
|
+
</svg>
|
|
561
|
+
);
|
|
562
|
+
|
|
563
|
+
const QuickEditStationIcon: React.FC<{ className?: string }> = ({
|
|
564
|
+
className,
|
|
565
|
+
}) => (
|
|
566
|
+
<svg
|
|
567
|
+
className={clsx(classes.icons, className)}
|
|
568
|
+
version="1.1"
|
|
569
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
570
|
+
viewBox="0 0 40 40"
|
|
571
|
+
>
|
|
572
|
+
<path
|
|
573
|
+
vectorEffect="non-scaling-stroke"
|
|
574
|
+
fill="none"
|
|
575
|
+
strokeWidth="2"
|
|
576
|
+
d="M7.5,4.5h25v31H7.5V4.5ZM17.82,13.14h8.87M12.98,13.14h3.07M12.98,17.71h3.07M12.98,22.29h3.07M12.98,26.86h3.07M17.82,17.71h8.87M17.82,26.86h8.87M17.82,22.29h4.84"
|
|
577
|
+
/>
|
|
578
|
+
</svg>
|
|
579
|
+
);
|
|
580
|
+
|
|
531
581
|
const RemoveFilterIcon: React.FC<{ className?: string }> = ({ className }) => (
|
|
532
582
|
<svg
|
|
533
583
|
className={clsx(classes.icons, className)}
|
|
@@ -581,6 +631,30 @@ const RetryIcon: React.FC<{ className?: string }> = ({ className }) => (
|
|
|
581
631
|
</svg>
|
|
582
632
|
);
|
|
583
633
|
|
|
634
|
+
const SaveIcon: React.FC<{ className?: string }> = ({ className }) => (
|
|
635
|
+
<svg
|
|
636
|
+
className={clsx(classes.icons, className)}
|
|
637
|
+
version="1.1"
|
|
638
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
639
|
+
viewBox="0 0 40 40"
|
|
640
|
+
>
|
|
641
|
+
<path
|
|
642
|
+
vectorEffect="non-scaling-stroke"
|
|
643
|
+
fill="none"
|
|
644
|
+
strokeWidth="2"
|
|
645
|
+
d="M15.8,30.1c0,0-6.2,0.1-7.6-0.1c-3.3-0.4-5.8-3.3-5.6-6.7c-0.2-6,6.9-6.6,6.9-6.6
|
|
646
|
+
s-0.7-6.4,5-7.5c3.2-0.8,6.5,0.8,7.8,3.8c1.5-0.6,3.2-0.6,4.7,0.2c1.3,0.8,2.2,2.1,2.4,3.6c0,0,8-0.5,8.1,6.6c0,7.1-6.7,6.7-6.7,6.7
|
|
647
|
+
h-6.5"
|
|
648
|
+
/>
|
|
649
|
+
<path
|
|
650
|
+
vectorEffect="non-scaling-stroke"
|
|
651
|
+
fill="none"
|
|
652
|
+
strokeWidth="2"
|
|
653
|
+
d="M20.1,33V19.4 M15.7,23.8l4.3-4.3l4.3,4.3"
|
|
654
|
+
/>
|
|
655
|
+
</svg>
|
|
656
|
+
);
|
|
657
|
+
|
|
584
658
|
const SnapshotIcon: React.FC<{ className?: string }> = ({ className }) => (
|
|
585
659
|
<svg
|
|
586
660
|
className={clsx(classes.icons, className)}
|
|
@@ -768,6 +842,7 @@ export const Icons: React.FC<IconsProps> = ({ icon, className }) => {
|
|
|
768
842
|
[IconName.Error]: <ErrorIcon className={className} />,
|
|
769
843
|
[IconName.External]: <ExternalIcon className={className} />,
|
|
770
844
|
[IconName.File]: <FileIcon className={className} />,
|
|
845
|
+
[IconName.Filters]: <FiltersIcon className={className} />,
|
|
771
846
|
[IconName.ForwardOne]: <ForwardOne className={className} />,
|
|
772
847
|
[IconName.ForwardTen]: <ForwardTen className={className} />,
|
|
773
848
|
[IconName.Info]: <InfoIcon className={className} />,
|
|
@@ -780,9 +855,12 @@ export const Icons: React.FC<IconsProps> = ({ icon, className }) => {
|
|
|
780
855
|
[IconName.Play]: <PlayIcon className={className} />,
|
|
781
856
|
[IconName.Plus]: <PlusIcon className={className} />,
|
|
782
857
|
[IconName.Publish]: <PublishIcon className={className} />,
|
|
858
|
+
[IconName.QuickEdit]: <QuickEditIcon className={className} />,
|
|
859
|
+
[IconName.QuickEditStation]: <QuickEditStationIcon className={className} />,
|
|
783
860
|
[IconName.RemoveFilter]: <RemoveFilterIcon className={className} />,
|
|
784
861
|
[IconName.Replace]: <ReplaceIcon className={className} />,
|
|
785
862
|
[IconName.Retry]: <RetryIcon className={className} />,
|
|
863
|
+
[IconName.Save]: <SaveIcon className={className} />,
|
|
786
864
|
[IconName.Snapshot]: <SnapshotIcon className={className} />,
|
|
787
865
|
[IconName.Stop]: <StopIcon className={className} />,
|
|
788
866
|
[IconName.Success]: <SuccessIcon className={className} />,
|
|
@@ -1,10 +1,28 @@
|
|
|
1
1
|
import { mount, shallow } from 'enzyme';
|
|
2
2
|
import React from 'react';
|
|
3
3
|
import { act } from 'react-dom/test-utils';
|
|
4
|
+
import { noop } from '../../helpers/utils';
|
|
5
|
+
import { initializeUi } from '../../initialize';
|
|
4
6
|
import { Button } from '../Buttons';
|
|
5
7
|
import { InlineMenu } from './InlineMenu';
|
|
6
8
|
|
|
7
9
|
describe('InlineMenu', () => {
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
initializeUi({
|
|
12
|
+
showNotification: () => {
|
|
13
|
+
// not implemented
|
|
14
|
+
return -1;
|
|
15
|
+
},
|
|
16
|
+
addIndicator: () => {
|
|
17
|
+
return -1;
|
|
18
|
+
},
|
|
19
|
+
removeIndicator: noop,
|
|
20
|
+
on: noop,
|
|
21
|
+
setTitle: noop,
|
|
22
|
+
setSaveIndicator: noop,
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
8
26
|
it('renders the component without crashing', () => {
|
|
9
27
|
const wrapper = shallow(<InlineMenu />);
|
|
10
28
|
|
|
@@ -97,7 +97,7 @@ export interface ListProps<T extends Data> {
|
|
|
97
97
|
*
|
|
98
98
|
* This is not being used, if generateItemLink is set!
|
|
99
99
|
*/
|
|
100
|
-
onItemClicked?: (data: T) => void
|
|
100
|
+
onItemClicked?: (data: T) => void | Promise<void>;
|
|
101
101
|
/** Raised when item selection has changed */
|
|
102
102
|
onItemSelected?: (itemSelectEvent: ItemSelectEventArgs<T>) => void;
|
|
103
103
|
/** Raised when list has scrolled down to the item indicated by loadingTriggerOffset */
|
|
@@ -235,6 +235,23 @@ const ListRenderer = <T extends Data>(
|
|
|
235
235
|
}
|
|
236
236
|
};
|
|
237
237
|
|
|
238
|
+
const itemClickedHandler = async (data: T, index: number): Promise<void> => {
|
|
239
|
+
try {
|
|
240
|
+
await onItemClicked(data);
|
|
241
|
+
|
|
242
|
+
if (selectionMode === ListSelectMode.Single) {
|
|
243
|
+
setListItems((prevState) =>
|
|
244
|
+
prevState.map((item, i) => ({
|
|
245
|
+
selected: i === index,
|
|
246
|
+
data: item.data,
|
|
247
|
+
})),
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
} catch (error) {
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
|
|
238
255
|
const { sort, sortChangedHandler } = useSort(defaultSortOrder, onSortChanged);
|
|
239
256
|
|
|
240
257
|
useImperativeHandle(ref, () => ({
|
|
@@ -253,6 +270,13 @@ const ListRenderer = <T extends Data>(
|
|
|
253
270
|
items: [],
|
|
254
271
|
});
|
|
255
272
|
},
|
|
273
|
+
selectIndex: (index: number) => {
|
|
274
|
+
if (selectionMode === ListSelectMode.Single) {
|
|
275
|
+
itemClickedHandler(listItems[index].data, index);
|
|
276
|
+
} else {
|
|
277
|
+
itemSelectedHandler(true, index);
|
|
278
|
+
}
|
|
279
|
+
},
|
|
256
280
|
}));
|
|
257
281
|
|
|
258
282
|
return (
|
|
@@ -312,12 +336,12 @@ const ListRenderer = <T extends Data>(
|
|
|
312
336
|
showCheckMark={selectionMode === ListSelectMode.Single}
|
|
313
337
|
showItemCheckbox={selectionMode === ListSelectMode.Multi}
|
|
314
338
|
onItemClicked={
|
|
315
|
-
generateItemLink
|
|
339
|
+
generateItemLink
|
|
340
|
+
? generateItemLink(item.data)
|
|
341
|
+
: (data) => itemClickedHandler(data, index)
|
|
316
342
|
}
|
|
317
343
|
onTriggered={onTriggeredHandler}
|
|
318
|
-
onItemSelected={(checked) =>
|
|
319
|
-
itemSelectedHandler(checked, index);
|
|
320
|
-
}}
|
|
344
|
+
onItemSelected={(checked) => itemSelectedHandler(checked, index)}
|
|
321
345
|
inlineMenuActions={inlineMenuActions}
|
|
322
346
|
/>
|
|
323
347
|
))}
|
|
@@ -338,16 +338,6 @@ describe('ListRow', () => {
|
|
|
338
338
|
expect(row.hasClass('selected')).toBe(false);
|
|
339
339
|
});
|
|
340
340
|
|
|
341
|
-
it(`does not have the 'selected' class if showItemCheckbox prop is false`, () => {
|
|
342
|
-
const wrapper = mount(
|
|
343
|
-
<ListRow {...mockProps} itemSelected={true} showItemCheckbox={false} />,
|
|
344
|
-
);
|
|
345
|
-
|
|
346
|
-
const row = wrapper.find('.columnsRoot');
|
|
347
|
-
|
|
348
|
-
expect(row.hasClass('selected')).toBe(false);
|
|
349
|
-
});
|
|
350
|
-
|
|
351
341
|
it(`only has the 'selected' class if both itemSelect and showItemCheckbox props are true`, () => {
|
|
352
342
|
const wrapper = mount(
|
|
353
343
|
<ListRow {...mockProps} itemSelected={true} showItemCheckbox={true} />,
|
|
@@ -278,7 +278,7 @@ export const ListRow = <T extends Data>({
|
|
|
278
278
|
const Row = (
|
|
279
279
|
<div
|
|
280
280
|
className={clsx(classes.columnsRoot, {
|
|
281
|
-
[classes.selected]: itemSelected
|
|
281
|
+
[classes.selected]: itemSelected,
|
|
282
282
|
[classes.disabled]: isRowDisabled,
|
|
283
283
|
})}
|
|
284
284
|
style={customRootStyles}
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
grid-template-rows: 1fr;
|
|
10
10
|
|
|
11
11
|
@media only screen and (min-width: $XL-min-size) {
|
|
12
|
-
grid-template-columns: minmax(
|
|
12
|
+
grid-template-columns: minmax(25%, 1fr) max-content max-content;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
color: var(--page-header-color, $page-header-color);
|
|
@@ -53,7 +53,6 @@
|
|
|
53
53
|
.actions {
|
|
54
54
|
display: grid;
|
|
55
55
|
grid-auto-flow: column;
|
|
56
|
-
grid-auto-columns: minmax(0, min-content);
|
|
57
56
|
grid-gap: 1px;
|
|
58
57
|
}
|
|
59
58
|
|
|
@@ -148,14 +148,18 @@ export const All: StoryObj<typeof PageHeader> = {
|
|
|
148
148
|
icon: IconName.Bulk,
|
|
149
149
|
kind: 'group',
|
|
150
150
|
actions: bulkHeaderActions,
|
|
151
|
-
onActionsGroupToggled:
|
|
151
|
+
onActionsGroupToggled: async (): Promise<void> => {
|
|
152
|
+
action('onBulkActions1Toggled');
|
|
153
|
+
},
|
|
152
154
|
},
|
|
153
155
|
{
|
|
154
156
|
label: 'Bulk Actions 2',
|
|
155
157
|
icon: IconName.Bulk,
|
|
156
158
|
kind: 'group',
|
|
157
159
|
actions: [bulkHeaderActions[0]],
|
|
158
|
-
onActionsGroupToggled:
|
|
160
|
+
onActionsGroupToggled: async (): Promise<void> => {
|
|
161
|
+
action('onBulkActions2Toggled');
|
|
162
|
+
},
|
|
159
163
|
},
|
|
160
164
|
],
|
|
161
165
|
},
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import clsx from 'clsx';
|
|
2
|
-
import React from 'react';
|
|
2
|
+
import React, { useEffect, useState } from 'react';
|
|
3
3
|
import { useTabTitle } from '../../hooks/useTabTitle/useTabTitle';
|
|
4
|
-
import { useWindowSize } from '../../hooks/useWindowSize/useWindowSize';
|
|
5
4
|
import { PageHeaderProps } from './PageHeader.model';
|
|
6
5
|
import classes from './PageHeader.scss';
|
|
7
6
|
import { PageHeaderActionProps } from './PageHeaderAction';
|
|
8
7
|
import { PageHeaderAction } from './PageHeaderAction/PageHeaderAction';
|
|
9
8
|
import { PageHeaderActionsGroup } from './PageHeaderActionsGroup/PageHeaderActionsGroup';
|
|
10
9
|
import { PageHeaderActionsGroupContextProvider } from './PageHeaderActionsGroup/PageHeaderActionsGroupsContextProvider';
|
|
10
|
+
import { useElementWidthObserver } from './helpers/useElementWidthObserver';
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* Primary header for stations. Accepts a title, subtitle, actions, and actions groups.
|
|
@@ -29,29 +29,23 @@ export const PageHeader: React.FC<PageHeaderProps> = ({
|
|
|
29
29
|
setTabTitle = true,
|
|
30
30
|
}) => {
|
|
31
31
|
useTabTitle(title, setTabTitle);
|
|
32
|
-
const { width } =
|
|
33
|
-
const containerRef = React.useRef<HTMLDivElement>(null);
|
|
32
|
+
const { width, ref } = useElementWidthObserver<HTMLDivElement>();
|
|
34
33
|
|
|
35
|
-
const [availableActionSpace, setAvailableActionSpace] =
|
|
36
|
-
React.useState<number>(0);
|
|
34
|
+
const [availableActionSpace, setAvailableActionSpace] = useState<number>(0);
|
|
37
35
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
const maxActionsWidth = containerRef.current.clientWidth * 0.75;
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
// Use up to 75% of the container width for actions
|
|
38
|
+
const maxActionsWidth = width * 0.75;
|
|
42
39
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
Math.floor(maxActionsWidth / 120 - actions.length),
|
|
46
|
-
);
|
|
47
|
-
}
|
|
40
|
+
// Each action is 120px wide
|
|
41
|
+
setAvailableActionSpace(Math.floor(maxActionsWidth / 120 - actions.length));
|
|
48
42
|
}, [width, actions.length]);
|
|
49
43
|
|
|
50
44
|
return (
|
|
51
45
|
<div
|
|
52
46
|
className={clsx(classes.container, 'page-header-container', className)}
|
|
53
47
|
data-test-id="page-header"
|
|
54
|
-
ref={
|
|
48
|
+
ref={ref}
|
|
55
49
|
>
|
|
56
50
|
<div className={clsx(classes.titles)}>
|
|
57
51
|
<div className={clsx(classes.title)} data-test-id="page-header-title">
|
|
@@ -88,6 +88,13 @@
|
|
|
88
88
|
}
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
+
&.highlight {
|
|
92
|
+
background-color: var(
|
|
93
|
+
--page-header-action-context-hover-background-color,
|
|
94
|
+
$page-header-action-context-hover-background-color
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
91
98
|
&.active {
|
|
92
99
|
color: var(
|
|
93
100
|
--page-header-action-context-background-color,
|
|
@@ -119,6 +119,7 @@ const PageHeaderJSAction: React.FC<PageHeaderJsActionProps> = ({
|
|
|
119
119
|
{
|
|
120
120
|
[classes.context]: actionType === PageHeaderActionType.Context,
|
|
121
121
|
[classes.active]: actionType === PageHeaderActionType.Active,
|
|
122
|
+
[classes.highlight]: actionType === PageHeaderActionType.Hightlight,
|
|
122
123
|
[classes.hasConfirm]: confirmation,
|
|
123
124
|
},
|
|
124
125
|
'page-header-action-container',
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { mount, shallow } from 'enzyme';
|
|
2
2
|
import React from 'react';
|
|
3
|
+
import { act } from 'react-dom/test-utils';
|
|
3
4
|
import { noop } from '../../../helpers/utils';
|
|
4
5
|
import { PageHeaderAction } from '../PageHeaderAction/PageHeaderAction';
|
|
5
6
|
import {
|
|
@@ -45,16 +46,18 @@ describe('PageHeaderActionsGroup', () => {
|
|
|
45
46
|
expect(wrapper).toBeTruthy();
|
|
46
47
|
});
|
|
47
48
|
|
|
48
|
-
it(`'Group Actions' has the 'Context' actionType when closed and 'Active' actionType when open`, () => {
|
|
49
|
+
it(`'Group Actions' has the 'Context' actionType when closed and 'Active' actionType when open`, async () => {
|
|
49
50
|
const wrapper = mount(<PageHeaderActionsGroup {...defaultProps} />);
|
|
50
51
|
let groupActionsToggle = wrapper.find(PageHeaderAction).first();
|
|
51
|
-
// let action = wrapper.find(PageHeaderAction);
|
|
52
52
|
|
|
53
53
|
expect(groupActionsToggle.prop('actionType')).toBe(
|
|
54
54
|
PageHeaderActionType.Context,
|
|
55
55
|
);
|
|
56
56
|
|
|
57
|
-
|
|
57
|
+
await act(async () => {
|
|
58
|
+
await groupActionsToggle.prop('onClick')?.();
|
|
59
|
+
wrapper.update();
|
|
60
|
+
});
|
|
58
61
|
|
|
59
62
|
groupActionsToggle = wrapper.find(PageHeaderAction).first();
|
|
60
63
|
|
|
@@ -63,19 +66,23 @@ describe('PageHeaderActionsGroup', () => {
|
|
|
63
66
|
);
|
|
64
67
|
});
|
|
65
68
|
|
|
66
|
-
it(`renders all actions when 'Group Actions' is selected and there is enough available action slots`, () => {
|
|
69
|
+
it(`renders all actions when 'Group Actions' is selected and there is enough available action slots`, async () => {
|
|
67
70
|
const wrapper = mount(
|
|
68
71
|
<PageHeaderActionsGroup {...defaultProps} availableActionSpace={5} />,
|
|
69
72
|
);
|
|
70
73
|
const groupActionsToggle = wrapper.find(PageHeaderAction).first();
|
|
71
74
|
let actions = wrapper.find(PageHeaderAction);
|
|
72
75
|
|
|
73
|
-
|
|
76
|
+
await act(async () => {
|
|
77
|
+
await groupActionsToggle.prop('onClick')?.();
|
|
78
|
+
wrapper.update();
|
|
79
|
+
});
|
|
80
|
+
|
|
74
81
|
actions = wrapper.find(PageHeaderAction);
|
|
75
82
|
expect(actions).toHaveLength(5);
|
|
76
83
|
});
|
|
77
84
|
|
|
78
|
-
it(`raises onActionsGroupToggled`, () => {
|
|
85
|
+
it(`raises onActionsGroupToggled`, async () => {
|
|
79
86
|
const groupActionSpy = jest.fn();
|
|
80
87
|
const wrapper = mount(
|
|
81
88
|
<PageHeaderActionsGroup
|
|
@@ -84,7 +91,12 @@ describe('PageHeaderActionsGroup', () => {
|
|
|
84
91
|
/>,
|
|
85
92
|
);
|
|
86
93
|
const groupActionsToggle = wrapper.find(PageHeaderAction).first();
|
|
87
|
-
|
|
94
|
+
|
|
95
|
+
await act(async () => {
|
|
96
|
+
await groupActionsToggle.prop('onClick')?.();
|
|
97
|
+
wrapper.update();
|
|
98
|
+
});
|
|
99
|
+
|
|
88
100
|
expect(groupActionSpy).toHaveBeenCalledTimes(1);
|
|
89
101
|
expect(groupActionSpy).toHaveBeenCalledWith(true);
|
|
90
102
|
});
|