@axinom/mosaic-ui 0.32.0 → 0.33.0-rc.1
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/FormStation/SaveOnNavigate/SaveOnNavigate.d.ts +2 -0
- package/dist/components/FormStation/SaveOnNavigate/SaveOnNavigate.d.ts.map +1 -1
- package/dist/components/FormStation/SaveOnNavigate/handleNavigationAttempt.d.ts +1 -1
- package/dist/components/FormStation/SaveOnNavigate/handleNavigationAttempt.d.ts.map +1 -1
- package/dist/components/FormStation/StationErrorStateType.d.ts +5 -0
- package/dist/components/FormStation/StationErrorStateType.d.ts.map +1 -0
- package/dist/components/FormStation/useValidationError.d.ts +15 -0
- package/dist/components/FormStation/useValidationError.d.ts.map +1 -0
- 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 +34 -16
- package/src/components/FormStation/SaveOnNavigate/SaveOnNavigate.tsx +5 -0
- package/src/components/FormStation/SaveOnNavigate/handleNavigationAttempt.spec.ts +21 -0
- package/src/components/FormStation/SaveOnNavigate/handleNavigationAttempt.ts +2 -0
- package/src/components/FormStation/StationErrorStateType.tsx +5 -0
- package/src/components/FormStation/useValidationError.tsx +59 -0
- 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
|
@@ -5,6 +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 { hideSaveIndicator, showSaveIndicator } from '../../initialize';
|
|
8
9
|
import { ActionData, Actions } from '../Actions';
|
|
9
10
|
import { Action } from '../Actions/Action';
|
|
10
11
|
import { MessageBar } from '../MessageBar/MessageBar';
|
|
@@ -12,6 +13,8 @@ import { PageHeader, PageHeaderAction } from '../PageHeader';
|
|
|
12
13
|
import { FormStation, ObjectSchemaDefinition } from './FormStation';
|
|
13
14
|
import { SaveOnNavigate } from './SaveOnNavigate/SaveOnNavigate';
|
|
14
15
|
|
|
16
|
+
jest.mock('../../initialize');
|
|
17
|
+
|
|
15
18
|
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
|
16
19
|
|
|
17
20
|
const mockActions = (onActionSelected: jest.Mock): ActionData[] => [
|
|
@@ -41,6 +44,10 @@ const MakeDirty: React.FC = () => {
|
|
|
41
44
|
};
|
|
42
45
|
|
|
43
46
|
describe('Details', () => {
|
|
47
|
+
beforeEach(() => {
|
|
48
|
+
jest.clearAllMocks();
|
|
49
|
+
});
|
|
50
|
+
|
|
44
51
|
it('renders the component without crashing', () => {
|
|
45
52
|
const wrapper = shallow(<FormStation {...defaultProps} />);
|
|
46
53
|
|
|
@@ -102,7 +109,7 @@ describe('Details', () => {
|
|
|
102
109
|
.find(Action)
|
|
103
110
|
.prop('action').onActionSelected;
|
|
104
111
|
await act(async () => {
|
|
105
|
-
actionSelected();
|
|
112
|
+
actionSelected && actionSelected();
|
|
106
113
|
});
|
|
107
114
|
|
|
108
115
|
expect(spy).toHaveBeenCalledTimes(1);
|
|
@@ -148,7 +155,7 @@ describe('Details', () => {
|
|
|
148
155
|
.find(Action)
|
|
149
156
|
.prop('action').onActionSelected;
|
|
150
157
|
await act(async () => {
|
|
151
|
-
actionSelected();
|
|
158
|
+
actionSelected && actionSelected();
|
|
152
159
|
});
|
|
153
160
|
|
|
154
161
|
expect(spy).not.toHaveBeenCalled();
|
|
@@ -176,7 +183,7 @@ describe('Details', () => {
|
|
|
176
183
|
.find(Action)
|
|
177
184
|
.prop('action').onActionSelected;
|
|
178
185
|
await act(async () => {
|
|
179
|
-
actionSelected();
|
|
186
|
+
actionSelected && actionSelected();
|
|
180
187
|
});
|
|
181
188
|
|
|
182
189
|
expect(spy).not.toHaveBeenCalled();
|
|
@@ -201,7 +208,7 @@ describe('Details', () => {
|
|
|
201
208
|
.find(Action)
|
|
202
209
|
.prop('action').onActionSelected;
|
|
203
210
|
await act(async () => {
|
|
204
|
-
actionSelected();
|
|
211
|
+
actionSelected && actionSelected();
|
|
205
212
|
});
|
|
206
213
|
|
|
207
214
|
expect(saveSpy).not.toHaveBeenCalled();
|
|
@@ -421,7 +428,7 @@ describe('Details', () => {
|
|
|
421
428
|
.find(Action)
|
|
422
429
|
.prop('action').onActionSelected;
|
|
423
430
|
await act(async () => {
|
|
424
|
-
actionSelected();
|
|
431
|
+
actionSelected && actionSelected();
|
|
425
432
|
});
|
|
426
433
|
|
|
427
434
|
wrapper.update();
|
|
@@ -473,7 +480,7 @@ describe('Details', () => {
|
|
|
473
480
|
.find(Action)
|
|
474
481
|
.prop('action').onActionSelected;
|
|
475
482
|
await act(async () => {
|
|
476
|
-
actionSelected();
|
|
483
|
+
actionSelected && actionSelected();
|
|
477
484
|
});
|
|
478
485
|
|
|
479
486
|
wrapper.update();
|
|
@@ -534,7 +541,7 @@ describe('Details', () => {
|
|
|
534
541
|
.find(Action)
|
|
535
542
|
.prop('action').onActionSelected;
|
|
536
543
|
await act(async () => {
|
|
537
|
-
actionSelected();
|
|
544
|
+
actionSelected && actionSelected();
|
|
538
545
|
});
|
|
539
546
|
|
|
540
547
|
// place form into 'submitting' state
|
|
@@ -611,7 +618,7 @@ describe('Details', () => {
|
|
|
611
618
|
.find(Action)
|
|
612
619
|
.prop('action').onActionSelected;
|
|
613
620
|
await act(async () => {
|
|
614
|
-
actionSelected();
|
|
621
|
+
actionSelected && actionSelected();
|
|
615
622
|
});
|
|
616
623
|
|
|
617
624
|
// place form into 'submitting' state
|
|
@@ -644,5 +651,56 @@ describe('Details', () => {
|
|
|
644
651
|
isDisabled = action.prop('action').isDisabled;
|
|
645
652
|
expect(isDisabled).toBe(false);
|
|
646
653
|
});
|
|
654
|
+
|
|
655
|
+
it('sets the global busy state when submitting', async () => {
|
|
656
|
+
jest.useFakeTimers();
|
|
657
|
+
const spy = jest.fn();
|
|
658
|
+
const sampleActions = mockActions(spy);
|
|
659
|
+
const onSubmit = (): Promise<{ id: number }> =>
|
|
660
|
+
new Promise((resolve) =>
|
|
661
|
+
setTimeout(() => {
|
|
662
|
+
resolve({ id: 3 });
|
|
663
|
+
}, 1000),
|
|
664
|
+
);
|
|
665
|
+
|
|
666
|
+
const wrapper = mount(
|
|
667
|
+
<MemoryRouter>
|
|
668
|
+
<FormStation
|
|
669
|
+
{...defaultProps}
|
|
670
|
+
actions={sampleActions}
|
|
671
|
+
saveData={onSubmit}
|
|
672
|
+
initialData={{ loading: false, data: {} }}
|
|
673
|
+
>
|
|
674
|
+
<MakeDirty />
|
|
675
|
+
</FormStation>
|
|
676
|
+
</MemoryRouter>,
|
|
677
|
+
);
|
|
678
|
+
|
|
679
|
+
// submit form
|
|
680
|
+
const actionSelected = wrapper
|
|
681
|
+
.find(Action)
|
|
682
|
+
.prop('action').onActionSelected;
|
|
683
|
+
|
|
684
|
+
await act(async () => {
|
|
685
|
+
actionSelected && actionSelected();
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
// place form into 'submitting' state
|
|
689
|
+
await act(async () => {
|
|
690
|
+
jest.advanceTimersByTime(500);
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
wrapper.update();
|
|
694
|
+
|
|
695
|
+
expect(showSaveIndicator).toHaveBeenCalledTimes(1);
|
|
696
|
+
|
|
697
|
+
// complete form submission
|
|
698
|
+
await act(async () => {
|
|
699
|
+
jest.runAllTimers();
|
|
700
|
+
});
|
|
701
|
+
wrapper.update();
|
|
702
|
+
|
|
703
|
+
expect(hideSaveIndicator).toHaveBeenCalledTimes(1);
|
|
704
|
+
});
|
|
647
705
|
});
|
|
648
706
|
});
|
|
@@ -16,21 +16,24 @@ import React, {
|
|
|
16
16
|
} from 'react';
|
|
17
17
|
import { useHistory } from 'react-router-dom';
|
|
18
18
|
import { OptionalObjectSchema } from 'yup/lib/object';
|
|
19
|
+
import { hideSaveIndicator, showSaveIndicator } from '../../initialize';
|
|
19
20
|
import { Data } from '../../types/data';
|
|
20
21
|
import { ErrorTypeToStationError } from '../../utils/ErrorTypeToStationError';
|
|
21
22
|
import { Actions, ActionsProps } from '../Actions';
|
|
22
23
|
import { isNavigationAction } from '../Actions/Action/Action';
|
|
23
24
|
import { IconName } from '../Icons';
|
|
24
25
|
import { MessageBar } from '../MessageBar';
|
|
25
|
-
import { ErrorType, StationError, StationMessage } from '../models';
|
|
26
26
|
import {
|
|
27
27
|
PageHeader,
|
|
28
28
|
PageHeaderActionType,
|
|
29
29
|
PageHeaderProps,
|
|
30
30
|
} from '../PageHeader';
|
|
31
|
+
import { ErrorType, StationMessage } from '../models';
|
|
31
32
|
import { FormActionData, InitialFormData } from './FormStation.models';
|
|
32
33
|
import classes from './FormStation.scss';
|
|
33
34
|
import { SaveOnNavigate } from './SaveOnNavigate/SaveOnNavigate';
|
|
35
|
+
import { StationErrorStateType } from './StationErrorStateType';
|
|
36
|
+
import { useValidationError } from './useValidationError';
|
|
34
37
|
|
|
35
38
|
export type ObjectSchemaDefinition<
|
|
36
39
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
@@ -102,7 +105,8 @@ interface FormActionProps<T, Y> extends Omit<ActionsProps, 'actions'> {
|
|
|
102
105
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
103
106
|
validationSchema?: OptionalObjectSchema<ObjectSchemaDefinition<any>>;
|
|
104
107
|
actions?: FormActionData<T, Y>[];
|
|
105
|
-
setStationError: (error:
|
|
108
|
+
setStationError: (error: StationErrorStateType) => void;
|
|
109
|
+
setValidationError: () => void;
|
|
106
110
|
submitResponse?: React.MutableRefObject<Y | undefined>;
|
|
107
111
|
alwaysSubmitBeforeAction?: boolean;
|
|
108
112
|
className?: string;
|
|
@@ -130,8 +134,12 @@ export const FormStation = <TValues extends Data, TSubmitResponse = unknown>({
|
|
|
130
134
|
}: PropsWithChildren<
|
|
131
135
|
FormStationProps<TValues, TSubmitResponse>
|
|
132
136
|
>): JSX.Element => {
|
|
133
|
-
const [stationError, setStationError] = useState<
|
|
134
|
-
|
|
137
|
+
const [stationError, setStationError] = useState<StationErrorStateType>();
|
|
138
|
+
|
|
139
|
+
const { setValidationError, validationWatcher } = useValidationError(
|
|
140
|
+
stationError,
|
|
141
|
+
setStationError,
|
|
142
|
+
);
|
|
135
143
|
|
|
136
144
|
const submitResponse = useRef<TSubmitResponse>();
|
|
137
145
|
const [isFormSubmitting, setIsFormSubmitting] = useState<boolean>(false);
|
|
@@ -144,8 +152,10 @@ export const FormStation = <TValues extends Data, TSubmitResponse = unknown>({
|
|
|
144
152
|
if (isFormSubmitting) {
|
|
145
153
|
return;
|
|
146
154
|
}
|
|
155
|
+
|
|
147
156
|
try {
|
|
148
157
|
setIsFormSubmitting(true);
|
|
158
|
+
showSaveIndicator();
|
|
149
159
|
setStationError(undefined);
|
|
150
160
|
if (!initialData.loading && saveData) {
|
|
151
161
|
const response = await saveData(values, initialData, formikHelpers);
|
|
@@ -167,6 +177,7 @@ export const FormStation = <TValues extends Data, TSubmitResponse = unknown>({
|
|
|
167
177
|
} finally {
|
|
168
178
|
formikHelpers.setSubmitting(false);
|
|
169
179
|
setIsFormSubmitting(false);
|
|
180
|
+
hideSaveIndicator();
|
|
170
181
|
}
|
|
171
182
|
},
|
|
172
183
|
[isFormSubmitting, initialData, saveData, setStationError],
|
|
@@ -178,19 +189,20 @@ export const FormStation = <TValues extends Data, TSubmitResponse = unknown>({
|
|
|
178
189
|
(initialData.data === null && !initialData.loading) ||
|
|
179
190
|
initialData.entityNotFound
|
|
180
191
|
) {
|
|
181
|
-
const stationError =
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
192
|
+
const stationError = {
|
|
193
|
+
...ErrorTypeToStationError(
|
|
194
|
+
initialData.error,
|
|
195
|
+
'An error occurred when trying to load data.',
|
|
196
|
+
'Entity not found',
|
|
197
|
+
),
|
|
198
|
+
type: 'loading',
|
|
199
|
+
};
|
|
186
200
|
|
|
187
201
|
setStationError(stationError);
|
|
188
|
-
setIsLoadingError(true);
|
|
189
202
|
} else {
|
|
190
|
-
if (
|
|
203
|
+
if (stationError?.type === 'loading') {
|
|
191
204
|
// Only clear the error if it is a loading error, which now seems to be cleared.
|
|
192
205
|
setStationError(undefined);
|
|
193
|
-
setIsLoadingError(false);
|
|
194
206
|
}
|
|
195
207
|
}
|
|
196
208
|
}, [
|
|
@@ -198,7 +210,7 @@ export const FormStation = <TValues extends Data, TSubmitResponse = unknown>({
|
|
|
198
210
|
initialData.error,
|
|
199
211
|
initialData.entityNotFound,
|
|
200
212
|
initialData.data,
|
|
201
|
-
|
|
213
|
+
stationError?.type,
|
|
202
214
|
]);
|
|
203
215
|
|
|
204
216
|
const getContent = (): JSX.Element | undefined => {
|
|
@@ -219,7 +231,10 @@ export const FormStation = <TValues extends Data, TSubmitResponse = unknown>({
|
|
|
219
231
|
// Loading successful
|
|
220
232
|
return (
|
|
221
233
|
<>
|
|
222
|
-
<SaveOnNavigate
|
|
234
|
+
<SaveOnNavigate
|
|
235
|
+
isSubmitting={isFormSubmitting}
|
|
236
|
+
onNavigationCancelled={setValidationError}
|
|
237
|
+
/>
|
|
223
238
|
<div className={classes.main}>
|
|
224
239
|
<div
|
|
225
240
|
className={clsx(classes.formWrapper, {
|
|
@@ -268,6 +283,7 @@ export const FormStation = <TValues extends Data, TSubmitResponse = unknown>({
|
|
|
268
283
|
enableReinitialize={true}
|
|
269
284
|
>
|
|
270
285
|
<>
|
|
286
|
+
{validationWatcher}
|
|
271
287
|
<FormStationHeader
|
|
272
288
|
titleProperty={titleProperty as string}
|
|
273
289
|
defaultTitle={defaultTitle}
|
|
@@ -292,6 +308,7 @@ export const FormStation = <TValues extends Data, TSubmitResponse = unknown>({
|
|
|
292
308
|
width={actionsWidth}
|
|
293
309
|
validationSchema={validationSchema}
|
|
294
310
|
setStationError={setStationError}
|
|
311
|
+
setValidationError={setValidationError}
|
|
295
312
|
submitResponse={submitResponse}
|
|
296
313
|
alwaysSubmitBeforeAction={alwaysSubmitBeforeAction}
|
|
297
314
|
className={classes.actionsPanel}
|
|
@@ -313,6 +330,7 @@ const FormStationAction = <T, Y>(
|
|
|
313
330
|
actions,
|
|
314
331
|
validationSchema,
|
|
315
332
|
setStationError,
|
|
333
|
+
setValidationError,
|
|
316
334
|
submitResponse,
|
|
317
335
|
alwaysSubmitBeforeAction,
|
|
318
336
|
className = '',
|
|
@@ -348,8 +366,7 @@ const FormStationAction = <T, Y>(
|
|
|
348
366
|
!isValid ||
|
|
349
367
|
(await validationSchema?.isValid(values)) === false
|
|
350
368
|
) {
|
|
351
|
-
|
|
352
|
-
console.log('form invalid, action not performed');
|
|
369
|
+
setValidationError();
|
|
353
370
|
// Making sure that the fields will actually show the validation messages (they won't if the from was not touched yet - e.g. on a create station)
|
|
354
371
|
validateForm();
|
|
355
372
|
return;
|
|
@@ -401,6 +418,7 @@ const FormStationAction = <T, Y>(
|
|
|
401
418
|
isValid,
|
|
402
419
|
resetForm,
|
|
403
420
|
setStationError,
|
|
421
|
+
setValidationError,
|
|
404
422
|
submitForm,
|
|
405
423
|
submitResponse,
|
|
406
424
|
validateForm,
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { useFormikContext } from 'formik';
|
|
2
2
|
import React, { useState } from 'react';
|
|
3
|
+
import { noop } from '../../../helpers/utils';
|
|
3
4
|
import { NavigationAPI, useReactRouterPause } from '../../../hooks';
|
|
4
5
|
import { handleNavigationAttempt } from './handleNavigationAttempt';
|
|
5
6
|
|
|
6
7
|
interface SaveOnNavigateProps {
|
|
7
8
|
/** If set to true, will prevent form submission when navigating away. (default: false) */
|
|
8
9
|
isSubmitting?: boolean;
|
|
10
|
+
/** Callback that will be called when a navigation attempt was cancelled */
|
|
11
|
+
onNavigationCancelled?: () => void;
|
|
9
12
|
}
|
|
10
13
|
|
|
11
14
|
/**
|
|
@@ -13,6 +16,7 @@ interface SaveOnNavigateProps {
|
|
|
13
16
|
*/
|
|
14
17
|
export const SaveOnNavigate: React.FC<SaveOnNavigateProps> = ({
|
|
15
18
|
isSubmitting = false,
|
|
19
|
+
onNavigationCancelled = noop,
|
|
16
20
|
}) => {
|
|
17
21
|
const { dirty, isValid, submitForm } = useFormikContext();
|
|
18
22
|
const [canSubmit, setCanSubmit] = useState<boolean>(true);
|
|
@@ -28,6 +32,7 @@ export const SaveOnNavigate: React.FC<SaveOnNavigateProps> = ({
|
|
|
28
32
|
isSubmitting,
|
|
29
33
|
canSubmit,
|
|
30
34
|
setCanSubmit,
|
|
35
|
+
onNavigationCancelled,
|
|
31
36
|
),
|
|
32
37
|
},
|
|
33
38
|
dirty,
|
|
@@ -20,6 +20,7 @@ describe('handleNavigationAttempt', () => {
|
|
|
20
20
|
const isSubmitting = false;
|
|
21
21
|
const canSubmit = true;
|
|
22
22
|
const setCanSubmit = jest.fn();
|
|
23
|
+
const onNavigationCancelled = jest.fn();
|
|
23
24
|
|
|
24
25
|
// Act
|
|
25
26
|
await handleNavigationAttempt(
|
|
@@ -30,6 +31,7 @@ describe('handleNavigationAttempt', () => {
|
|
|
30
31
|
isSubmitting,
|
|
31
32
|
canSubmit,
|
|
32
33
|
setCanSubmit,
|
|
34
|
+
onNavigationCancelled,
|
|
33
35
|
);
|
|
34
36
|
|
|
35
37
|
// Assert
|
|
@@ -38,6 +40,7 @@ describe('handleNavigationAttempt', () => {
|
|
|
38
40
|
expect(navigation.cancel).not.toHaveBeenCalled();
|
|
39
41
|
expect(submitForm).not.toHaveBeenCalled();
|
|
40
42
|
expect(setCanSubmit).not.toHaveBeenCalled();
|
|
43
|
+
expect(onNavigationCancelled).not.toHaveBeenCalled();
|
|
41
44
|
});
|
|
42
45
|
|
|
43
46
|
it(`blocks navigation when 'isValid' is 'false' by calling 'navigation.cancel()'`, async () => {
|
|
@@ -48,6 +51,7 @@ describe('handleNavigationAttempt', () => {
|
|
|
48
51
|
const isSubmitting = false;
|
|
49
52
|
const canSubmit = true;
|
|
50
53
|
const setCanSubmit = jest.fn();
|
|
54
|
+
const onNavigationCancelled = jest.fn();
|
|
51
55
|
|
|
52
56
|
// Act
|
|
53
57
|
await handleNavigationAttempt(
|
|
@@ -58,6 +62,7 @@ describe('handleNavigationAttempt', () => {
|
|
|
58
62
|
isSubmitting,
|
|
59
63
|
canSubmit,
|
|
60
64
|
setCanSubmit,
|
|
65
|
+
onNavigationCancelled,
|
|
61
66
|
);
|
|
62
67
|
|
|
63
68
|
// Assert
|
|
@@ -66,6 +71,7 @@ describe('handleNavigationAttempt', () => {
|
|
|
66
71
|
expect(navigation.cancel).toHaveBeenCalledTimes(1);
|
|
67
72
|
expect(submitForm).not.toHaveBeenCalled();
|
|
68
73
|
expect(setCanSubmit).not.toHaveBeenCalled();
|
|
74
|
+
expect(onNavigationCancelled).toHaveBeenCalled();
|
|
69
75
|
});
|
|
70
76
|
|
|
71
77
|
it(`pauses further navigation when 'dirty' is 'true' and 'isValid' is 'true' by calling 'navigation.pause()'`, async () => {
|
|
@@ -76,6 +82,7 @@ describe('handleNavigationAttempt', () => {
|
|
|
76
82
|
const isSubmitting = true;
|
|
77
83
|
const canSubmit = true;
|
|
78
84
|
const setCanSubmit = jest.fn();
|
|
85
|
+
const onNavigationCancelled = jest.fn();
|
|
79
86
|
|
|
80
87
|
// Act
|
|
81
88
|
await handleNavigationAttempt(
|
|
@@ -86,6 +93,7 @@ describe('handleNavigationAttempt', () => {
|
|
|
86
93
|
isSubmitting,
|
|
87
94
|
canSubmit,
|
|
88
95
|
setCanSubmit,
|
|
96
|
+
onNavigationCancelled,
|
|
89
97
|
);
|
|
90
98
|
|
|
91
99
|
// Assert
|
|
@@ -94,6 +102,7 @@ describe('handleNavigationAttempt', () => {
|
|
|
94
102
|
expect(navigation.cancel).not.toHaveBeenCalled();
|
|
95
103
|
expect(submitForm).not.toHaveBeenCalled();
|
|
96
104
|
expect(setCanSubmit).not.toHaveBeenCalled();
|
|
105
|
+
expect(onNavigationCancelled).not.toHaveBeenCalled();
|
|
97
106
|
});
|
|
98
107
|
|
|
99
108
|
it(`while 'isSubmitting' is 'true' navigation should continue to be paused by not calling 'navigation.resume()'`, async () => {
|
|
@@ -104,6 +113,7 @@ describe('handleNavigationAttempt', () => {
|
|
|
104
113
|
const isSubmitting = true;
|
|
105
114
|
const canSubmit = true;
|
|
106
115
|
const setCanSubmit = jest.fn();
|
|
116
|
+
const onNavigationCancelled = jest.fn();
|
|
107
117
|
|
|
108
118
|
// Act
|
|
109
119
|
await handleNavigationAttempt(
|
|
@@ -114,6 +124,7 @@ describe('handleNavigationAttempt', () => {
|
|
|
114
124
|
isSubmitting,
|
|
115
125
|
canSubmit,
|
|
116
126
|
setCanSubmit,
|
|
127
|
+
onNavigationCancelled,
|
|
117
128
|
);
|
|
118
129
|
|
|
119
130
|
// Assert
|
|
@@ -122,6 +133,7 @@ describe('handleNavigationAttempt', () => {
|
|
|
122
133
|
expect(navigation.cancel).not.toHaveBeenCalled();
|
|
123
134
|
expect(submitForm).not.toHaveBeenCalled();
|
|
124
135
|
expect(setCanSubmit).not.toHaveBeenCalled();
|
|
136
|
+
expect(onNavigationCancelled).not.toHaveBeenCalled();
|
|
125
137
|
});
|
|
126
138
|
|
|
127
139
|
it(`while 'canSubmit' is 'false' navigation should continue to be paused by not calling 'navigation.resume()'`, async () => {
|
|
@@ -132,6 +144,7 @@ describe('handleNavigationAttempt', () => {
|
|
|
132
144
|
const isSubmitting = false;
|
|
133
145
|
const canSubmit = false;
|
|
134
146
|
const setCanSubmit = jest.fn();
|
|
147
|
+
const onNavigationCancelled = jest.fn();
|
|
135
148
|
|
|
136
149
|
// Act
|
|
137
150
|
await handleNavigationAttempt(
|
|
@@ -142,6 +155,7 @@ describe('handleNavigationAttempt', () => {
|
|
|
142
155
|
isSubmitting,
|
|
143
156
|
canSubmit,
|
|
144
157
|
setCanSubmit,
|
|
158
|
+
onNavigationCancelled,
|
|
145
159
|
);
|
|
146
160
|
|
|
147
161
|
// Assert
|
|
@@ -150,6 +164,7 @@ describe('handleNavigationAttempt', () => {
|
|
|
150
164
|
expect(navigation.cancel).not.toHaveBeenCalled();
|
|
151
165
|
expect(submitForm).not.toHaveBeenCalled();
|
|
152
166
|
expect(setCanSubmit).not.toHaveBeenCalled();
|
|
167
|
+
expect(onNavigationCancelled).not.toHaveBeenCalled();
|
|
153
168
|
});
|
|
154
169
|
|
|
155
170
|
it(`navigation can resume after the submitForm promise was successfully resolved by calling 'navigation.resume()'`, async () => {
|
|
@@ -160,6 +175,7 @@ describe('handleNavigationAttempt', () => {
|
|
|
160
175
|
const isSubmitting = false;
|
|
161
176
|
const canSubmit = true;
|
|
162
177
|
const setCanSubmit = jest.fn();
|
|
178
|
+
const onNavigationCancelled = jest.fn();
|
|
163
179
|
|
|
164
180
|
// Act
|
|
165
181
|
await handleNavigationAttempt(
|
|
@@ -170,6 +186,7 @@ describe('handleNavigationAttempt', () => {
|
|
|
170
186
|
isSubmitting,
|
|
171
187
|
canSubmit,
|
|
172
188
|
setCanSubmit,
|
|
189
|
+
onNavigationCancelled,
|
|
173
190
|
);
|
|
174
191
|
|
|
175
192
|
// Assert
|
|
@@ -179,6 +196,7 @@ describe('handleNavigationAttempt', () => {
|
|
|
179
196
|
expect(submitForm).toHaveBeenCalledTimes(1);
|
|
180
197
|
expect(setCanSubmit).toHaveBeenCalledTimes(1);
|
|
181
198
|
expect(setCanSubmit).toHaveBeenCalledWith(false);
|
|
199
|
+
expect(onNavigationCancelled).not.toHaveBeenCalledWith(false);
|
|
182
200
|
});
|
|
183
201
|
|
|
184
202
|
it(`navigation is cancelled when the 'submitForm' promise was rejected by calling 'navigation.cancel()'`, async () => {
|
|
@@ -191,6 +209,7 @@ describe('handleNavigationAttempt', () => {
|
|
|
191
209
|
const isSubmitting = false;
|
|
192
210
|
const canSubmit = true;
|
|
193
211
|
const setCanSubmit = jest.fn();
|
|
212
|
+
const onNavigationCancelled = jest.fn();
|
|
194
213
|
|
|
195
214
|
// Act
|
|
196
215
|
await handleNavigationAttempt(
|
|
@@ -201,6 +220,7 @@ describe('handleNavigationAttempt', () => {
|
|
|
201
220
|
isSubmitting,
|
|
202
221
|
canSubmit,
|
|
203
222
|
setCanSubmit,
|
|
223
|
+
onNavigationCancelled,
|
|
204
224
|
);
|
|
205
225
|
|
|
206
226
|
// Assert
|
|
@@ -211,5 +231,6 @@ describe('handleNavigationAttempt', () => {
|
|
|
211
231
|
expect(setCanSubmit).toHaveBeenCalledTimes(2);
|
|
212
232
|
expect(setCanSubmit).toHaveBeenNthCalledWith(1, false);
|
|
213
233
|
expect(setCanSubmit).toHaveBeenNthCalledWith(2, true);
|
|
234
|
+
expect(onNavigationCancelled).not.toHaveBeenCalled();
|
|
214
235
|
});
|
|
215
236
|
});
|
|
@@ -10,6 +10,7 @@ export const handleNavigationAttempt = async (
|
|
|
10
10
|
isSubmitting: boolean,
|
|
11
11
|
canSubmit: boolean,
|
|
12
12
|
setCanSubmit: React.Dispatch<React.SetStateAction<boolean>>,
|
|
13
|
+
onNavigationCancelled: () => void,
|
|
13
14
|
): Promise<void> => {
|
|
14
15
|
if (!dirty) {
|
|
15
16
|
// Form values didn't change, just navigate away
|
|
@@ -20,6 +21,7 @@ export const handleNavigationAttempt = async (
|
|
|
20
21
|
// Form is invalid, cancel navigation
|
|
21
22
|
// TODO: Add a message here (probably connected to "Error handling")
|
|
22
23
|
navigation.cancel();
|
|
24
|
+
onNavigationCancelled();
|
|
23
25
|
return;
|
|
24
26
|
}
|
|
25
27
|
// Form values have changed and form is valid, attempt save
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { useFormikContext } from 'formik';
|
|
2
|
+
import React, { useCallback, useEffect, useState } from 'react';
|
|
3
|
+
import { StationErrorStateType } from './StationErrorStateType';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Component that watches for changes in the form validation state
|
|
7
|
+
* and calls the callback with the current state.
|
|
8
|
+
*/
|
|
9
|
+
const ValidationWatcher: React.FC<{
|
|
10
|
+
callback: (isValid: boolean) => void;
|
|
11
|
+
}> = ({ callback: isValid }) => {
|
|
12
|
+
const formik = useFormikContext();
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
isValid(formik.isValid);
|
|
15
|
+
}, [formik.isValid, isValid]);
|
|
16
|
+
return null;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Cares for showing (and removing) validation errors.
|
|
20
|
+
* @param stationError the currently showing error
|
|
21
|
+
* @param setStationError the setter for the currently showing error
|
|
22
|
+
* @returns
|
|
23
|
+
* `setValidationError` - call this method to set the validation error.
|
|
24
|
+
* `validationWatcher` - place this JSX somewhere inside the <Formik /> component children.
|
|
25
|
+
*/
|
|
26
|
+
export function useValidationError(
|
|
27
|
+
stationError: StationErrorStateType | undefined,
|
|
28
|
+
setStationError: React.Dispatch<
|
|
29
|
+
React.SetStateAction<StationErrorStateType | undefined>
|
|
30
|
+
>,
|
|
31
|
+
): {
|
|
32
|
+
setValidationError: () => void;
|
|
33
|
+
validationWatcher: JSX.Element;
|
|
34
|
+
} {
|
|
35
|
+
// Track validation state of the from using the validationWatcher
|
|
36
|
+
const [isValid, setIsValid] = useState<boolean>(true);
|
|
37
|
+
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
// if the form became valid, remove the validation error
|
|
40
|
+
if (isValid && stationError && stationError.type === 'validation') {
|
|
41
|
+
setStationError(undefined);
|
|
42
|
+
}
|
|
43
|
+
}, [isValid, setStationError, stationError]);
|
|
44
|
+
|
|
45
|
+
const setValidationError = useCallback((): void => {
|
|
46
|
+
// set the validation error
|
|
47
|
+
setStationError({
|
|
48
|
+
title: 'Please fix the errors in the form to proceed.',
|
|
49
|
+
type: 'validation',
|
|
50
|
+
});
|
|
51
|
+
}, [setStationError]);
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
setValidationError,
|
|
55
|
+
validationWatcher: (
|
|
56
|
+
<ValidationWatcher callback={(valid) => setIsValid(valid)} />
|
|
57
|
+
),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
@@ -7,12 +7,19 @@
|
|
|
7
7
|
grid-template-columns: 1fr;
|
|
8
8
|
|
|
9
9
|
&.hasTitle {
|
|
10
|
-
grid-template-rows: max-content
|
|
10
|
+
grid-template-rows: max-content;
|
|
11
|
+
|
|
12
|
+
padding-top: 6px;
|
|
13
|
+
padding-bottom: 6px;
|
|
14
|
+
|
|
15
|
+
&.expanded {
|
|
16
|
+
grid-template-rows: max-content max-content;
|
|
17
|
+
}
|
|
11
18
|
}
|
|
12
19
|
|
|
13
20
|
padding: 30px;
|
|
14
21
|
|
|
15
|
-
row-gap:
|
|
22
|
+
row-gap: 20px;
|
|
16
23
|
|
|
17
24
|
.title,
|
|
18
25
|
.main {
|
|
@@ -37,4 +44,29 @@
|
|
|
37
44
|
--infopanel-background-color,
|
|
38
45
|
$infopanel-background-color
|
|
39
46
|
);
|
|
47
|
+
|
|
48
|
+
.collapsibleSection {
|
|
49
|
+
& > div:first-child {
|
|
50
|
+
background-color: unset;
|
|
51
|
+
|
|
52
|
+
div:last-child {
|
|
53
|
+
margin-left: -18px;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
svg {
|
|
57
|
+
margin-left: -34px;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
& > div:last-child {
|
|
62
|
+
border-bottom: unset;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
& > div > div:last-child {
|
|
66
|
+
background-color: var(
|
|
67
|
+
--infopanel-background-color,
|
|
68
|
+
$infopanel-background-color
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
40
72
|
}
|