@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.
Files changed (100) 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/PageHeaderAction/PageHeaderAction.d.ts.map +1 -1
  40. package/dist/components/PageHeader/PageHeaderAction/PageHeaderAction.model.d.ts +2 -1
  41. package/dist/components/PageHeader/PageHeaderAction/PageHeaderAction.model.d.ts.map +1 -1
  42. package/dist/components/PageHeader/PageHeaderActionsGroup/PageHeaderActionsGroup.d.ts +1 -1
  43. package/dist/components/PageHeader/PageHeaderActionsGroup/PageHeaderActionsGroup.d.ts.map +1 -1
  44. package/dist/components/PageHeader/helpers/useElementWidthObserver.d.ts +6 -0
  45. package/dist/components/PageHeader/helpers/useElementWidthObserver.d.ts.map +1 -0
  46. package/dist/index.es.js +4 -4
  47. package/dist/index.es.js.map +1 -1
  48. package/dist/index.js +4 -4
  49. package/dist/index.js.map +1 -1
  50. package/dist/initialize.d.ts +1 -1
  51. package/dist/initialize.d.ts.map +1 -1
  52. package/package.json +4 -3
  53. package/src/components/EmptyStation/EmptyStation.spec.tsx +24 -0
  54. package/src/components/Explorer/ConditionalSplit/ConditionalSplit.tsx +23 -0
  55. package/src/components/Explorer/Explorer.model.ts +19 -1
  56. package/src/components/Explorer/Explorer.scss +4 -0
  57. package/src/components/Explorer/Explorer.spec.tsx +28 -3
  58. package/src/components/Explorer/Explorer.stories.tsx +90 -5
  59. package/src/components/Explorer/Explorer.tsx +149 -185
  60. package/src/components/Explorer/NavigationExplorer/NavigationExplorer.spec.tsx +26 -0
  61. package/src/components/Explorer/NavigationExplorer/NavigationExplorer.stories.tsx +2 -2
  62. package/src/components/Explorer/QuickEdit/QuickEditContext.tsx +16 -0
  63. package/src/components/Explorer/QuickEdit/useQuickEdit.spec.tsx +461 -0
  64. package/src/components/Explorer/QuickEdit/useQuickEdit.tsx +169 -0
  65. package/src/components/Explorer/SelectionExplorer/SelectionExplorer.spec.tsx +6 -0
  66. package/src/components/Explorer/SelectionExplorer/SelectionExplorer.stories.tsx +2 -2
  67. package/src/components/Explorer/{InMemoryDataProvider.ts → helpers/InMemoryDataProvider.ts} +4 -4
  68. package/src/components/Explorer/helpers/useActions.ts +203 -0
  69. package/src/components/Explorer/{useDataProvider.tsx → helpers/useDataProvider.tsx} +11 -11
  70. package/src/components/Explorer/helpers/useFilters.tsx +77 -0
  71. package/src/components/Explorer/{useStationMessage.tsx → helpers/useStationMessage.tsx} +8 -6
  72. package/src/components/Explorer/index.ts +10 -6
  73. package/src/components/FormStation/Create/Create.tsx +1 -0
  74. package/src/components/FormStation/FormStation.spec.tsx +62 -73
  75. package/src/components/FormStation/FormStation.tsx +31 -15
  76. package/src/components/FormStation/FormStationHeader/FormStationHeader.tsx +38 -18
  77. package/src/components/FormStation/SaveOnDemand/SaveOnDemand.tsx +55 -0
  78. package/src/components/FormStation/helpers/useDataProvider.ts +1 -8
  79. package/src/components/Icons/Icons.models.ts +4 -0
  80. package/src/components/Icons/Icons.tsx +78 -0
  81. package/src/components/InlineMenu/InlineMenu.spec.tsx +18 -0
  82. package/src/components/List/List.model.ts +5 -0
  83. package/src/components/List/List.tsx +29 -5
  84. package/src/components/List/ListRow/ListRow.spec.tsx +0 -10
  85. package/src/components/List/ListRow/ListRow.tsx +1 -1
  86. package/src/components/PageHeader/PageHeader.scss +1 -2
  87. package/src/components/PageHeader/PageHeader.stories.tsx +6 -2
  88. package/src/components/PageHeader/PageHeader.tsx +10 -16
  89. package/src/components/PageHeader/PageHeaderAction/PageHeaderAction.model.ts +1 -0
  90. package/src/components/PageHeader/PageHeaderAction/PageHeaderAction.scss +7 -0
  91. package/src/components/PageHeader/PageHeaderAction/PageHeaderAction.tsx +1 -0
  92. package/src/components/PageHeader/PageHeaderActionsGroup/PageHeaderActionsGroup.spec.tsx +19 -7
  93. package/src/components/PageHeader/PageHeaderActionsGroup/PageHeaderActionsGroup.tsx +19 -12
  94. package/src/components/PageHeader/helpers/useElementWidthObserver.tsx +30 -0
  95. package/src/initialize.ts +2 -2
  96. package/dist/components/Explorer/InMemoryDataProvider.d.ts.map +0 -1
  97. package/dist/components/Explorer/useDataProvider.d.ts.map +0 -1
  98. package/dist/components/Explorer/useStationMessage.d.ts +0 -15
  99. package/dist/components/Explorer/useStationMessage.d.ts.map +0 -1
  100. /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';
@@ -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
  });
@@ -1,8 +1,9 @@
1
1
  import clsx from 'clsx';
2
2
  import { Formik, FormikValues } from 'formik';
3
- import React, { PropsWithChildren } from 'react';
3
+ import React, { PropsWithChildren, useContext } from 'react';
4
4
  import { OptionalObjectSchema } from 'yup/lib/object';
5
5
  import { Data } from '../../types/data';
6
+ import { QuickEditContext } from '../Explorer/QuickEdit/QuickEditContext';
6
7
  import { StationMessage } from '../models';
7
8
  import {
8
9
  FormActionData,
@@ -14,6 +15,7 @@ import classes from './FormStation.scss';
14
15
  import { FormStationAction } from './FormStationActions';
15
16
  import { FormStationContentWrapper } from './FormStationContentWrapper';
16
17
  import { FormStationHeader } from './FormStationHeader';
18
+ import { SaveOnDemand } from './SaveOnDemand/SaveOnDemand';
17
19
  import { SaveOnNavigate } from './SaveOnNavigate/SaveOnNavigate';
18
20
  import { useDataProvider } from './helpers/useDataProvider';
19
21
  import { useValidationError } from './helpers/useValidationError';
@@ -30,10 +32,12 @@ export interface FormStationProps<
30
32
  subtitle?: string;
31
33
  /** If set to true, the actions panel is shown, even if no actions are defined. */
32
34
  alwaysShowActionsPanel?: boolean;
33
- // /** An array of the actions that should be executable on the page */
35
+ /** An array of the actions that should be executable on the page */
34
36
  actions?: FormActionData<TValues, TSubmitResponse>[];
35
37
  /** The width of the Actions container (as CSS width) */
36
38
  actionsWidth?: string;
39
+ /** If set to true, the save header action is shown. (default: true) */
40
+ showSaveHeaderAction?: boolean;
37
41
  /**
38
42
  * An object containing the initial data of the form.
39
43
  */
@@ -85,9 +89,12 @@ export const FormStation = <TValues extends Data, TSubmitResponse = unknown>({
85
89
  stationMessage,
86
90
  className = '',
87
91
  setTabTitle = true,
92
+ showSaveHeaderAction = true,
88
93
  }: PropsWithChildren<
89
94
  FormStationProps<TValues, TSubmitResponse>
90
95
  >): JSX.Element => {
96
+ const quickEditContext = useContext(QuickEditContext);
97
+
91
98
  const {
92
99
  onSubmit,
93
100
  stationError,
@@ -127,33 +134,42 @@ export const FormStation = <TValues extends Data, TSubmitResponse = unknown>({
127
134
  cancelNavigationUrl={cancelNavigationUrl}
128
135
  className={classes.header}
129
136
  setTabTitle={setTabTitle}
137
+ showSaveHeaderAction={showSaveHeaderAction}
130
138
  />
131
139
  <SaveOnNavigate
132
140
  isSubmitting={isFormSubmitting}
133
141
  onNavigationCancelled={setValidationError}
134
142
  />
143
+ {quickEditContext && (
144
+ <SaveOnDemand
145
+ isSubmitting={isFormSubmitting}
146
+ onFormInvalid={setValidationError}
147
+ registerSaveCallback={quickEditContext.registerSaveCallback}
148
+ />
149
+ )}
135
150
  <FormStationContentWrapper
136
151
  stationMessage={stationMessage}
137
152
  edgeToEdgeContent={edgeToEdgeContent}
138
- infoPanel={infoPanel}
153
+ infoPanel={!quickEditContext ? infoPanel : undefined}
139
154
  initialData={initialData}
140
155
  stationError={stationError}
141
156
  setStationError={setStationError}
142
157
  >
143
158
  {children}
144
159
  </FormStationContentWrapper>
145
- {(alwaysShowActionsPanel || (actions?.length ?? 0) > 0) && (
146
- <FormStationAction<TValues, TSubmitResponse>
147
- actions={actions}
148
- width={actionsWidth}
149
- validationSchema={validationSchema}
150
- setStationError={setStationError}
151
- setValidationError={setValidationError}
152
- submitResponse={lastSubmittedResponse}
153
- alwaysSubmitBeforeAction={alwaysSubmitBeforeAction}
154
- className={classes.actionsPanel}
155
- />
156
- )}
160
+ {(alwaysShowActionsPanel || (actions?.length ?? 0) > 0) &&
161
+ !quickEditContext && (
162
+ <FormStationAction<TValues, TSubmitResponse>
163
+ actions={actions}
164
+ width={actionsWidth}
165
+ validationSchema={validationSchema}
166
+ setStationError={setStationError}
167
+ setValidationError={setValidationError}
168
+ submitResponse={lastSubmittedResponse}
169
+ alwaysSubmitBeforeAction={alwaysSubmitBeforeAction}
170
+ className={classes.actionsPanel}
171
+ />
172
+ )}
157
173
  </>
158
174
  </Formik>
159
175
  </div>