@axinom/mosaic-ui 0.51.0-rc.1 → 0.51.0-rc.11

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 (95) hide show
  1. package/dist/components/Explorer/ConditionalSplit/ConditionalSplit.d.ts +8 -0
  2. package/dist/components/Explorer/ConditionalSplit/ConditionalSplit.d.ts.map +1 -0
  3. package/dist/components/Explorer/Explorer.d.ts +3 -1
  4. package/dist/components/Explorer/Explorer.d.ts.map +1 -1
  5. package/dist/components/Explorer/Explorer.model.d.ts +18 -1
  6. package/dist/components/Explorer/Explorer.model.d.ts.map +1 -1
  7. package/dist/components/Explorer/QuickEdit/QuickEditContext.d.ts +11 -0
  8. package/dist/components/Explorer/QuickEdit/QuickEditContext.d.ts.map +1 -0
  9. package/dist/components/Explorer/QuickEdit/useQuickEdit.d.ts +22 -0
  10. package/dist/components/Explorer/QuickEdit/useQuickEdit.d.ts.map +1 -0
  11. package/dist/components/Explorer/{InMemoryDataProvider.d.ts → helpers/InMemoryDataProvider.d.ts} +3 -3
  12. package/dist/components/Explorer/helpers/InMemoryDataProvider.d.ts.map +1 -0
  13. package/dist/components/Explorer/helpers/useActions.d.ts +31 -0
  14. package/dist/components/Explorer/helpers/useActions.d.ts.map +1 -0
  15. package/dist/components/Explorer/{useDataProvider.d.ts → helpers/useDataProvider.d.ts} +6 -6
  16. package/dist/components/Explorer/helpers/useDataProvider.d.ts.map +1 -0
  17. package/dist/components/Explorer/helpers/useFilters.d.ts +21 -0
  18. package/dist/components/Explorer/helpers/useFilters.d.ts.map +1 -0
  19. package/dist/components/Explorer/helpers/useStationMessage.d.ts +17 -0
  20. package/dist/components/Explorer/helpers/useStationMessage.d.ts.map +1 -0
  21. package/dist/components/Explorer/index.d.ts +2 -1
  22. package/dist/components/Explorer/index.d.ts.map +1 -1
  23. package/dist/components/FormStation/Create/Create.d.ts.map +1 -1
  24. package/dist/components/FormStation/FormStation.d.ts +4 -1
  25. package/dist/components/FormStation/FormStation.d.ts.map +1 -1
  26. package/dist/components/FormStation/FormStationHeader/FormStationHeader.d.ts +1 -0
  27. package/dist/components/FormStation/FormStationHeader/FormStationHeader.d.ts.map +1 -1
  28. package/dist/components/FormStation/SaveOnDemand/SaveOnDemand.d.ts +11 -0
  29. package/dist/components/FormStation/SaveOnDemand/SaveOnDemand.d.ts.map +1 -0
  30. package/dist/components/FormStation/helpers/useDataProvider.d.ts.map +1 -1
  31. package/dist/components/Icons/Icons.d.ts.map +1 -1
  32. package/dist/components/Icons/Icons.models.d.ts +28 -24
  33. package/dist/components/Icons/Icons.models.d.ts.map +1 -1
  34. package/dist/components/List/List.d.ts +1 -1
  35. package/dist/components/List/List.d.ts.map +1 -1
  36. package/dist/components/List/List.model.d.ts +4 -0
  37. package/dist/components/List/List.model.d.ts.map +1 -1
  38. package/dist/components/PageHeader/PageHeader.d.ts.map +1 -1
  39. package/dist/components/PageHeader/PageHeaderActionsGroup/PageHeaderActionsGroup.d.ts +1 -1
  40. package/dist/components/PageHeader/PageHeaderActionsGroup/PageHeaderActionsGroup.d.ts.map +1 -1
  41. package/dist/components/PageHeader/helpers/useElementWidthObserver.d.ts +6 -0
  42. package/dist/components/PageHeader/helpers/useElementWidthObserver.d.ts.map +1 -0
  43. package/dist/index.es.js +4 -4
  44. package/dist/index.es.js.map +1 -1
  45. package/dist/index.js +4 -4
  46. package/dist/index.js.map +1 -1
  47. package/dist/initialize.d.ts +1 -1
  48. package/dist/initialize.d.ts.map +1 -1
  49. package/package.json +5 -4
  50. package/src/components/EmptyStation/EmptyStation.spec.tsx +24 -0
  51. package/src/components/Explorer/ConditionalSplit/ConditionalSplit.tsx +23 -0
  52. package/src/components/Explorer/Explorer.model.ts +19 -1
  53. package/src/components/Explorer/Explorer.scss +4 -0
  54. package/src/components/Explorer/Explorer.spec.tsx +28 -3
  55. package/src/components/Explorer/Explorer.stories.tsx +90 -5
  56. package/src/components/Explorer/Explorer.tsx +149 -185
  57. package/src/components/Explorer/NavigationExplorer/NavigationExplorer.spec.tsx +26 -0
  58. package/src/components/Explorer/NavigationExplorer/NavigationExplorer.stories.tsx +2 -2
  59. package/src/components/Explorer/QuickEdit/QuickEditContext.tsx +16 -0
  60. package/src/components/Explorer/QuickEdit/useQuickEdit.spec.tsx +461 -0
  61. package/src/components/Explorer/QuickEdit/useQuickEdit.tsx +169 -0
  62. package/src/components/Explorer/SelectionExplorer/SelectionExplorer.spec.tsx +6 -0
  63. package/src/components/Explorer/SelectionExplorer/SelectionExplorer.stories.tsx +2 -2
  64. package/src/components/Explorer/{InMemoryDataProvider.ts → helpers/InMemoryDataProvider.ts} +4 -4
  65. package/src/components/Explorer/helpers/useActions.ts +203 -0
  66. package/src/components/Explorer/{useDataProvider.tsx → helpers/useDataProvider.tsx} +11 -11
  67. package/src/components/Explorer/helpers/useFilters.tsx +77 -0
  68. package/src/components/Explorer/{useStationMessage.tsx → helpers/useStationMessage.tsx} +8 -6
  69. package/src/components/Explorer/index.ts +10 -6
  70. package/src/components/Filters/Filter/Filter.scss +2 -1
  71. package/src/components/FormStation/Create/Create.tsx +1 -0
  72. package/src/components/FormStation/FormStation.spec.tsx +62 -73
  73. package/src/components/FormStation/FormStation.tsx +31 -15
  74. package/src/components/FormStation/FormStationHeader/FormStationHeader.tsx +38 -18
  75. package/src/components/FormStation/SaveOnDemand/SaveOnDemand.tsx +55 -0
  76. package/src/components/FormStation/helpers/useDataProvider.ts +1 -8
  77. package/src/components/Icons/Icons.models.ts +4 -0
  78. package/src/components/Icons/Icons.tsx +78 -0
  79. package/src/components/InlineMenu/InlineMenu.spec.tsx +18 -0
  80. package/src/components/List/List.model.ts +5 -0
  81. package/src/components/List/List.tsx +29 -5
  82. package/src/components/List/ListRow/ListRow.spec.tsx +0 -10
  83. package/src/components/List/ListRow/ListRow.tsx +1 -1
  84. package/src/components/PageHeader/PageHeader.scss +1 -1
  85. package/src/components/PageHeader/PageHeader.stories.tsx +6 -2
  86. package/src/components/PageHeader/PageHeader.tsx +7 -12
  87. package/src/components/PageHeader/PageHeaderActionsGroup/PageHeaderActionsGroup.spec.tsx +19 -7
  88. package/src/components/PageHeader/PageHeaderActionsGroup/PageHeaderActionsGroup.tsx +9 -4
  89. package/src/components/PageHeader/helpers/useElementWidthObserver.tsx +30 -0
  90. package/src/initialize.ts +2 -2
  91. package/dist/components/Explorer/InMemoryDataProvider.d.ts.map +0 -1
  92. package/dist/components/Explorer/useDataProvider.d.ts.map +0 -1
  93. package/dist/components/Explorer/useStationMessage.d.ts +0 -15
  94. package/dist/components/Explorer/useStationMessage.d.ts.map +0 -1
  95. /package/src/components/Explorer/{InMemoryDataProvider.spec.ts → helpers/InMemoryDataProvider.spec.ts} +0 -0
@@ -0,0 +1,203 @@
1
+ import { useMemo } from 'react';
2
+ import { showNotification } from '../../../initialize';
3
+ import { Data } from '../../../types';
4
+ import { ErrorTypeToStationError } from '../../../utils/ErrorTypeToStationError';
5
+ import { IconName } from '../../Icons';
6
+ import { ItemSelectEventArgs } from '../../List';
7
+ import { PageHeaderActionItemProps } from '../../PageHeader';
8
+ import {
9
+ PageHeaderActionProps,
10
+ PageHeaderActionType,
11
+ PageHeaderJsActionProps,
12
+ isPageHeaderNavigationAction,
13
+ } from '../../PageHeader/PageHeaderAction';
14
+ import { ErrorType } from '../../models';
15
+ import { ExplorerBulkAction, ItemSelection } from '../Explorer.model';
16
+ import { ResultCounts } from './useDataProvider';
17
+ import { StationMessage } from './useStationMessage';
18
+
19
+ interface UseActionsProps<T extends Data> {
20
+ actions?: PageHeaderActionProps[];
21
+ bulkActions?: ExplorerBulkAction<T>[];
22
+ quickEditAction?: PageHeaderActionItemProps;
23
+ openBulkActionsOnStart: boolean | undefined;
24
+ filtersVisible: boolean;
25
+ hasFilters: boolean;
26
+ resultCount: ResultCounts;
27
+ activeFilterCount: number;
28
+ itemSelection: ItemSelectEventArgs<T>;
29
+ getBulkActionSelection: () => ItemSelection<T>;
30
+ onReloadData: () => void;
31
+ setStationMessage: (
32
+ value: React.SetStateAction<StationMessage | undefined>,
33
+ ) => void;
34
+ onBulkActionsToggled: (expanded: boolean) => void;
35
+ setIsBulkOpen: (value: React.SetStateAction<boolean>) => void;
36
+ toggleFiltersVisible: () => void;
37
+ }
38
+
39
+ interface UseActionsReturnType {
40
+ readonly actions: PageHeaderActionItemProps[];
41
+ }
42
+
43
+ export const useActions = <T extends Data>({
44
+ getBulkActionSelection,
45
+ onReloadData,
46
+ setStationMessage,
47
+ actions,
48
+ itemSelection,
49
+ onBulkActionsToggled,
50
+ openBulkActionsOnStart,
51
+ resultCount,
52
+ setIsBulkOpen,
53
+ hasFilters,
54
+ filtersVisible,
55
+ activeFilterCount,
56
+ toggleFiltersVisible,
57
+ quickEditAction,
58
+ bulkActions,
59
+ }: UseActionsProps<T>): UseActionsReturnType => {
60
+ const bulkActionItems: PageHeaderJsActionProps[] = useMemo(
61
+ () =>
62
+ (bulkActions ?? []).map((action) => ({
63
+ ...action,
64
+ onClick: async () => {
65
+ if (action.showStartedNotification !== false) {
66
+ showNotification({
67
+ title: `Bulk Action '${action.label}' Started`,
68
+ });
69
+ }
70
+
71
+ try {
72
+ const result = await action.onClick(getBulkActionSelection());
73
+ if (result) {
74
+ const message = errMsg(result);
75
+ showNotification({
76
+ title:
77
+ typeof message.title === 'string'
78
+ ? message.title
79
+ : 'An error occurred',
80
+ body: message.body,
81
+ options: {
82
+ type: 'error',
83
+ },
84
+ });
85
+ } else {
86
+ if (action.reloadData) {
87
+ onReloadData();
88
+ }
89
+ }
90
+ } catch (error) {
91
+ setStationMessage(
92
+ errMsg(
93
+ error,
94
+ 'An error occurred when trying to execute the bulk operation.',
95
+ ),
96
+ );
97
+ }
98
+ },
99
+ })),
100
+ [bulkActions, getBulkActionSelection, onReloadData, setStationMessage],
101
+ );
102
+
103
+ const pageHeaderActions: PageHeaderActionItemProps[] = useMemo(() => {
104
+ const headerActions: PageHeaderActionItemProps[] = [];
105
+
106
+ if (hasFilters) {
107
+ headerActions.push({
108
+ label:
109
+ activeFilterCount > 0 ? `Filters (${activeFilterCount})` : 'Filters',
110
+ icon: IconName.Filters,
111
+ kind: 'action',
112
+ actionType: filtersVisible
113
+ ? PageHeaderActionType.Active
114
+ : PageHeaderActionType.Context,
115
+ onClick: async () => {
116
+ toggleFiltersVisible();
117
+ },
118
+ });
119
+
120
+ headerActions.push({ kind: 'spacer' });
121
+ }
122
+
123
+ if (bulkActions && bulkActions.length > 0) {
124
+ headerActions.push({
125
+ label: 'Bulk Actions',
126
+ icon: IconName.Bulk,
127
+ kind: 'group',
128
+ actions: bulkActionItems,
129
+ openActionsGroupOnStart: openBulkActionsOnStart,
130
+ onActionsGroupToggled: async (isOpen) => {
131
+ setIsBulkOpen(isOpen);
132
+ onBulkActionsToggled(isOpen);
133
+ },
134
+ groupActionsDisabled:
135
+ itemSelection.items?.length === 0 || resultCount?.filtered === 0,
136
+ });
137
+ headerActions.push({ kind: 'spacer' });
138
+ }
139
+
140
+ if (quickEditAction) {
141
+ headerActions.push(quickEditAction);
142
+ }
143
+
144
+ if (actions && actions.length > 0) {
145
+ headerActions.push({ kind: 'spacer' });
146
+
147
+ actions?.forEach((action) => {
148
+ headerActions.push({
149
+ ...(isPageHeaderNavigationAction(action)
150
+ ? action
151
+ : {
152
+ ...action,
153
+ onClick: async () => {
154
+ try {
155
+ const result = await action.onClick();
156
+ if (result) {
157
+ setStationMessage(errMsg(result));
158
+ }
159
+ } catch (error) {
160
+ setStationMessage(
161
+ errMsg(
162
+ error,
163
+ 'An error occurred when trying to execute the operation.',
164
+ ),
165
+ );
166
+ }
167
+ },
168
+ }),
169
+ kind: 'action',
170
+ });
171
+ });
172
+ }
173
+
174
+ return headerActions;
175
+ }, [
176
+ actions,
177
+ activeFilterCount,
178
+ bulkActionItems,
179
+ bulkActions,
180
+ filtersVisible,
181
+ hasFilters,
182
+ itemSelection.items?.length,
183
+ onBulkActionsToggled,
184
+ openBulkActionsOnStart,
185
+ quickEditAction,
186
+ resultCount?.filtered,
187
+ setIsBulkOpen,
188
+ setStationMessage,
189
+ toggleFiltersVisible,
190
+ ]);
191
+
192
+ return {
193
+ actions: pageHeaderActions,
194
+ };
195
+ };
196
+
197
+ const errMsg = (err: unknown | ErrorType, msg?: string): StationMessage => {
198
+ return {
199
+ ...ErrorTypeToStationError(err as ErrorType, msg),
200
+ canClose: true,
201
+ type: 'error',
202
+ };
203
+ };
@@ -7,19 +7,20 @@ import {
7
7
  useRef,
8
8
  useState,
9
9
  } from 'react';
10
- import { useUpdatingRef } from '../../hooks/useUpdatingRef/useUpdatingRef';
11
- import { Data } from '../../types/data';
12
- import { ErrorTypeToStationError } from '../../utils/ErrorTypeToStationError';
13
- import { FilterValues } from '../Filters/Filters.model';
14
- import { SortData } from '../List/List.model';
15
- import { ErrorType } from '../models';
10
+ import { useUpdatingRef } from '../../../hooks/useUpdatingRef/useUpdatingRef';
11
+ import { Data } from '../../../types/data';
12
+ import { ErrorTypeToStationError } from '../../../utils/ErrorTypeToStationError';
13
+ import { FilterValues } from '../../Filters/Filters.model';
14
+ import { SortData } from '../../List/List.model';
15
+ import { ErrorType } from '../../models';
16
16
  import {
17
17
  ExplorerDataProvider,
18
18
  ExplorerDataProviderConnection,
19
- } from './Explorer.model';
19
+ } from '../Explorer.model';
20
20
  import { StationMessage } from './useStationMessage';
21
21
 
22
22
  interface DataProviderReturnType<T> {
23
+ readonly data: T[];
23
24
  readonly isLoading: boolean;
24
25
  readonly resultCount: ResultCounts;
25
26
  readonly hasMoreData: boolean;
@@ -27,7 +28,6 @@ interface DataProviderReturnType<T> {
27
28
  readonly onSortChanged: (sort: SortData<T>) => void;
28
29
  readonly onFiltersChange: (filters: FilterValues<T>) => void;
29
30
  readonly onRequestMoreData: () => void;
30
- readonly data: T[];
31
31
  }
32
32
 
33
33
  export interface ResultCounts {
@@ -38,12 +38,12 @@ export interface ResultCounts {
38
38
  interface DataProviderArgumentType<T extends Data> {
39
39
  dataProvider: ExplorerDataProvider<T>;
40
40
  explorerRef: React.ForwardedRef<ExplorerDataProviderConnection<T>>;
41
- setStationMessage: React.Dispatch<
42
- React.SetStateAction<StationMessage | undefined>
43
- >;
44
41
  defaultSortOrder?: SortData<T>;
45
42
  filters?: FilterValues<T>;
46
43
  keyProperty?: keyof T;
44
+ setStationMessage: React.Dispatch<
45
+ React.SetStateAction<StationMessage | undefined>
46
+ >;
47
47
  }
48
48
 
49
49
  export function useDataProvider<T extends Data>({
@@ -0,0 +1,77 @@
1
+ import React, { useEffect, useState } from 'react';
2
+ import { useExpand } from '../../../hooks';
3
+ import { Data } from '../../../types';
4
+ import { FilterType, FilterValues } from '../../Filters';
5
+ import { Filters } from '../../Filters/Filters';
6
+ import { getState, storeState } from '../../Utils/State/GlobalState';
7
+ import { ExplorerStateOptions } from '../Explorer.model';
8
+
9
+ interface UseFiltersProps<T extends Data> {
10
+ stationKey: string;
11
+ globalStateOptions: ExplorerStateOptions;
12
+ filterOptions?: FilterType<T>[];
13
+ defaultFilterValues?: FilterValues<T>;
14
+ onFiltersChange: (filters: FilterValues<T>) => void;
15
+ }
16
+
17
+ interface UseFiltersReturnType<T extends Data> {
18
+ readonly Filters: JSX.Element;
19
+ readonly activeFilters: FilterValues<T>;
20
+ readonly filtersVisible: boolean;
21
+ readonly toggleFiltersVisible: () => void;
22
+ readonly collapseFilters: () => void;
23
+ }
24
+
25
+ export const useFilters = <T extends Data>({
26
+ stationKey,
27
+ globalStateOptions,
28
+ filterOptions,
29
+ defaultFilterValues,
30
+ onFiltersChange,
31
+ }: UseFiltersProps<T>): UseFiltersReturnType<T> => {
32
+ const { isExpanded, toggleExpanded, collapse } = useExpand(true);
33
+ const [activeFilters, setActiveFilters] = useState<FilterValues<T>>(
34
+ getState<FilterValues<T>>(stationKey, 'filters') ??
35
+ defaultFilterValues ??
36
+ {},
37
+ );
38
+
39
+ useEffect(() => {
40
+ if (
41
+ globalStateOptions.filters &&
42
+ activeFilters !== defaultFilterValues &&
43
+ Object.keys(activeFilters).length > 0
44
+ ) {
45
+ storeState(stationKey, 'filters', activeFilters);
46
+ } else {
47
+ storeState(stationKey, 'filters', undefined);
48
+ }
49
+ }, [
50
+ activeFilters,
51
+ defaultFilterValues,
52
+ globalStateOptions.filters,
53
+ stationKey,
54
+ ]);
55
+
56
+ const FilterComponent = (
57
+ <>
58
+ {isExpanded && (
59
+ <Filters<T>
60
+ options={filterOptions}
61
+ defaultValues={activeFilters}
62
+ onFiltersChange={(args) => {
63
+ onFiltersChange(args);
64
+ setActiveFilters(args);
65
+ }}
66
+ />
67
+ )}
68
+ </>
69
+ );
70
+ return {
71
+ Filters: FilterComponent,
72
+ activeFilters,
73
+ filtersVisible: isExpanded,
74
+ toggleFiltersVisible: toggleExpanded,
75
+ collapseFilters: collapse,
76
+ };
77
+ };
@@ -1,5 +1,5 @@
1
1
  import React, { useState } from 'react';
2
- import { MessageBar, MessageBarProps } from '../MessageBar/MessageBar';
2
+ import { MessageBar, MessageBarProps } from '../../MessageBar/MessageBar';
3
3
 
4
4
  export interface StationMessage {
5
5
  type: MessageBarProps['type'];
@@ -9,13 +9,15 @@ export interface StationMessage {
9
9
  onRetry?: MessageBarProps['onRetry'];
10
10
  }
11
11
 
12
- export const useStationMessage = (): {
13
- stationMessage: StationMessage | undefined;
14
- setStationMessage: React.Dispatch<
12
+ interface UseStationMessageReturnType {
13
+ readonly StationMessage: JSX.Element;
14
+ readonly stationMessage: StationMessage | undefined;
15
+ readonly setStationMessage: React.Dispatch<
15
16
  React.SetStateAction<StationMessage | undefined>
16
17
  >;
17
- StationMessage: JSX.Element;
18
- } => {
18
+ }
19
+
20
+ export const useStationMessage = (): UseStationMessageReturnType => {
19
21
  const [stationMessage, setStationMessage] = useState<StationMessage>();
20
22
 
21
23
  const StationMessage = (
@@ -6,17 +6,21 @@ export {
6
6
  ItemSelection,
7
7
  PageIdentifier,
8
8
  } from './Explorer.model';
9
- export {
10
- createInMemoryDataProvider,
11
- findAnywhereInString,
12
- findAnywhereInStringCaseInsensitive,
13
- findExact,
14
- } from './InMemoryDataProvider';
15
9
  export {
16
10
  NavigationExplorer,
17
11
  NavigationExplorerProps,
18
12
  } from './NavigationExplorer/NavigationExplorer';
13
+ export {
14
+ QuickEditContext,
15
+ QuickEditContextType,
16
+ } from './QuickEdit/QuickEditContext';
19
17
  export {
20
18
  SelectionExplorer,
21
19
  SelectionExplorerProps,
22
20
  } from './SelectionExplorer/SelectionExplorer';
21
+ export {
22
+ createInMemoryDataProvider,
23
+ findAnywhereInString,
24
+ findAnywhereInStringCaseInsensitive,
25
+ findExact,
26
+ } from './helpers/InMemoryDataProvider';
@@ -73,7 +73,7 @@ $input-inactive: 1px solid var(--input-border-color, $input-border-color);
73
73
  font-size: var(--filter-font-size, $filter-font-size);
74
74
  display: grid;
75
75
  grid-template-columns: auto auto;
76
- grid-template-rows: 50px;
76
+ grid-template-rows: auto;
77
77
  place-content: space-between;
78
78
  place-items: center;
79
79
 
@@ -92,6 +92,7 @@ $input-inactive: 1px solid var(--input-border-color, $input-border-color);
92
92
 
93
93
  span {
94
94
  padding-left: 30px;
95
+ padding-bottom: 10px;
95
96
  }
96
97
  }
97
98
  }
@@ -76,6 +76,7 @@ export const Create = <TValues extends Data, TSubmitResponse = unknown>(
76
76
  },
77
77
  ]}
78
78
  alwaysSubmitBeforeAction={true}
79
+ showSaveHeaderAction={false}
79
80
  ></FormStation>
80
81
  );
81
82
  };
@@ -5,7 +5,7 @@ import { act } from 'react-dom/test-utils';
5
5
  import { MemoryRouter, Route } from 'react-router-dom';
6
6
  import * as Yup from 'yup';
7
7
  import { noop } from '../../helpers/utils';
8
- import { SaveIndicatorType, setSaveIndicator } from '../../initialize';
8
+ import { initializeUi } from '../../initialize';
9
9
  import { ActionData, Actions } from '../Actions';
10
10
  import { Action } from '../Actions/Action';
11
11
  import { MessageBar } from '../MessageBar/MessageBar';
@@ -31,7 +31,7 @@ const defaultProps = {
31
31
  };
32
32
 
33
33
  // Temporarily disable console.log() from FormStation until proper error handling
34
- jest.spyOn(console, 'log').mockImplementation(() => null);
34
+ // jest.spyOn(console, 'log').mockImplementation(() => null);
35
35
  jest.spyOn(console, 'error').mockImplementation(() => null);
36
36
 
37
37
  const MakeDirty: React.FC = () => {
@@ -45,8 +45,28 @@ const MakeDirty: React.FC = () => {
45
45
  };
46
46
 
47
47
  describe('Details', () => {
48
+ global.ResizeObserver = jest.fn().mockImplementation(() => ({
49
+ observe: jest.fn(),
50
+ unobserve: jest.fn(),
51
+ disconnect: jest.fn(),
52
+ }));
53
+
48
54
  beforeEach(() => {
49
55
  jest.clearAllMocks();
56
+
57
+ initializeUi({
58
+ showNotification: () => {
59
+ // not implemented
60
+ return -1;
61
+ },
62
+ addIndicator: () => {
63
+ return -1;
64
+ },
65
+ removeIndicator: noop,
66
+ on: () => noop,
67
+ setTitle: noop,
68
+ setSaveIndicator: noop,
69
+ });
50
70
  });
51
71
 
52
72
  it('renders the component without crashing', () => {
@@ -260,7 +280,7 @@ describe('Details', () => {
260
280
  expect(header.prop('title')).toBe('default');
261
281
  });
262
282
 
263
- describe('Reset and cancel operations', () => {
283
+ describe('Reset Save and Cancel operations', () => {
264
284
  const ChangeValue: React.FC = () => {
265
285
  const context = useFormikContext();
266
286
  useEffect(() => {
@@ -298,13 +318,49 @@ describe('Details', () => {
298
318
  });
299
319
 
300
320
  const headerActions = wrapper.find(PageHeaderAction);
301
- expect(headerActions).toHaveLength(1);
321
+ expect(headerActions).toHaveLength(2);
302
322
 
303
323
  headerActions.at(0).simulate('click');
304
324
 
305
325
  expect(value).toBe('initial');
306
326
  });
307
327
 
328
+ it('allows saving of a dirty form', async () => {
329
+ let path: string;
330
+
331
+ const wrapper = mount(
332
+ <MemoryRouter>
333
+ <FormStation
334
+ {...defaultProps}
335
+ initialData={{ loading: false, data: { something: 'initial' } }}
336
+ >
337
+ <ChangeValue />
338
+ </FormStation>
339
+ <Route
340
+ path="*"
341
+ render={({ location }) => {
342
+ path = location.pathname;
343
+ return null;
344
+ }}
345
+ />
346
+ </MemoryRouter>,
347
+ );
348
+ await act(async () => {
349
+ wrapper.update();
350
+ });
351
+
352
+ const headerActions = wrapper.find(PageHeaderAction);
353
+ expect(headerActions).toHaveLength(2);
354
+
355
+ headerActions.at(1).simulate('click');
356
+
357
+ await act(async () => {
358
+ wrapper.update();
359
+ });
360
+
361
+ expect(path!).toBe('/');
362
+ });
363
+
308
364
  it('allows cancellation if wanted', async () => {
309
365
  let path: string;
310
366
 
@@ -336,9 +392,9 @@ describe('Details', () => {
336
392
  });
337
393
 
338
394
  const headerActions = wrapper.find(PageHeaderAction);
339
- expect(headerActions).toHaveLength(2);
395
+ expect(headerActions).toHaveLength(3);
340
396
 
341
- headerActions.at(1).simulate('click');
397
+ headerActions.at(2).simulate('click');
342
398
 
343
399
  await act(async () => {
344
400
  wrapper.update();
@@ -652,72 +708,5 @@ describe('Details', () => {
652
708
  isDisabled = action.prop('action').isDisabled;
653
709
  expect(isDisabled).toBe(false);
654
710
  });
655
-
656
- it('sets the global busy state when submitting', async () => {
657
- jest.useFakeTimers();
658
- const spy = jest.fn();
659
- const sampleActions = mockActions(spy);
660
- const onSubmit = (): Promise<{ id: number }> =>
661
- new Promise((resolve) =>
662
- setTimeout(() => {
663
- resolve({ id: 3 });
664
- }, 1000),
665
- );
666
-
667
- const wrapper = mount(
668
- <MemoryRouter>
669
- <FormStation
670
- {...defaultProps}
671
- actions={sampleActions}
672
- saveData={onSubmit}
673
- initialData={{ loading: false, data: {} }}
674
- >
675
- <MakeDirty />
676
- </FormStation>
677
- </MemoryRouter>,
678
- );
679
- expect(setSaveIndicator).toHaveBeenNthCalledWith(
680
- 1,
681
- SaveIndicatorType.Inactive,
682
- ); // 1. inactive
683
- expect(setSaveIndicator).toHaveBeenNthCalledWith(
684
- 3,
685
- SaveIndicatorType.Dirty,
686
- ); // 3. dirty
687
-
688
- // submit form
689
- const actionSelected = wrapper
690
- .find(Action)
691
- .prop('action').onActionSelected;
692
-
693
- await act(async () => {
694
- actionSelected && actionSelected();
695
- });
696
-
697
- // place form into 'submitting' state
698
- await act(async () => {
699
- jest.advanceTimersByTime(500);
700
- });
701
-
702
- wrapper.update();
703
-
704
- expect(setSaveIndicator).toHaveBeenNthCalledWith(
705
- 4,
706
- SaveIndicatorType.Saving,
707
- );
708
-
709
- // complete form submission
710
- await act(async () => {
711
- jest.runAllTimers();
712
- });
713
- wrapper.update();
714
-
715
- expect(setSaveIndicator).toHaveBeenNthCalledWith(
716
- 5,
717
- SaveIndicatorType.Inactive,
718
- );
719
-
720
- console.warn((setSaveIndicator as jest.Mock).mock.calls);
721
- });
722
711
  });
723
712
  });