@axinom/mosaic-ui 0.33.0-rc.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@axinom/mosaic-ui",
3
- "version": "0.33.0-rc.0",
3
+ "version": "0.33.0-rc.1",
4
4
  "description": "UI components for building Axinom Mosaic applications",
5
5
  "author": "Axinom",
6
6
  "license": "PROPRIETARY",
@@ -32,7 +32,7 @@
32
32
  "build-storybook": "storybook build"
33
33
  },
34
34
  "dependencies": {
35
- "@axinom/mosaic-core": "^0.4.6-rc.0",
35
+ "@axinom/mosaic-core": "^0.4.6-rc.1",
36
36
  "@faker-js/faker": "^7.4.0",
37
37
  "@popperjs/core": "^2.9.2",
38
38
  "clsx": "^1.1.0",
@@ -102,5 +102,5 @@
102
102
  "publishConfig": {
103
103
  "access": "public"
104
104
  },
105
- "gitHead": "32853a01e5b6de8cb3712ea5e1bd75780df8d59d"
105
+ "gitHead": "aa98a4a89a48ff7650a40acfec9a5b24c90d9c01"
106
106
  }
@@ -28,10 +28,12 @@ import {
28
28
  PageHeaderActionType,
29
29
  PageHeaderProps,
30
30
  } from '../PageHeader';
31
- import { ErrorType, StationError, StationMessage } from '../models';
31
+ import { ErrorType, StationMessage } from '../models';
32
32
  import { FormActionData, InitialFormData } from './FormStation.models';
33
33
  import classes from './FormStation.scss';
34
34
  import { SaveOnNavigate } from './SaveOnNavigate/SaveOnNavigate';
35
+ import { StationErrorStateType } from './StationErrorStateType';
36
+ import { useValidationError } from './useValidationError';
35
37
 
36
38
  export type ObjectSchemaDefinition<
37
39
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -103,7 +105,8 @@ interface FormActionProps<T, Y> extends Omit<ActionsProps, 'actions'> {
103
105
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
104
106
  validationSchema?: OptionalObjectSchema<ObjectSchemaDefinition<any>>;
105
107
  actions?: FormActionData<T, Y>[];
106
- setStationError: (error: StationError) => void;
108
+ setStationError: (error: StationErrorStateType) => void;
109
+ setValidationError: () => void;
107
110
  submitResponse?: React.MutableRefObject<Y | undefined>;
108
111
  alwaysSubmitBeforeAction?: boolean;
109
112
  className?: string;
@@ -131,8 +134,12 @@ export const FormStation = <TValues extends Data, TSubmitResponse = unknown>({
131
134
  }: PropsWithChildren<
132
135
  FormStationProps<TValues, TSubmitResponse>
133
136
  >): JSX.Element => {
134
- const [stationError, setStationError] = useState<StationError>();
135
- const [isLoadingError, setIsLoadingError] = useState(false);
137
+ const [stationError, setStationError] = useState<StationErrorStateType>();
138
+
139
+ const { setValidationError, validationWatcher } = useValidationError(
140
+ stationError,
141
+ setStationError,
142
+ );
136
143
 
137
144
  const submitResponse = useRef<TSubmitResponse>();
138
145
  const [isFormSubmitting, setIsFormSubmitting] = useState<boolean>(false);
@@ -182,19 +189,20 @@ export const FormStation = <TValues extends Data, TSubmitResponse = unknown>({
182
189
  (initialData.data === null && !initialData.loading) ||
183
190
  initialData.entityNotFound
184
191
  ) {
185
- const stationError = ErrorTypeToStationError(
186
- initialData.error,
187
- 'An error occurred when trying to load data.',
188
- 'Entity not found',
189
- );
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
+ };
190
200
 
191
201
  setStationError(stationError);
192
- setIsLoadingError(true);
193
202
  } else {
194
- if (isLoadingError === true) {
203
+ if (stationError?.type === 'loading') {
195
204
  // Only clear the error if it is a loading error, which now seems to be cleared.
196
205
  setStationError(undefined);
197
- setIsLoadingError(false);
198
206
  }
199
207
  }
200
208
  }, [
@@ -202,7 +210,7 @@ export const FormStation = <TValues extends Data, TSubmitResponse = unknown>({
202
210
  initialData.error,
203
211
  initialData.entityNotFound,
204
212
  initialData.data,
205
- isLoadingError,
213
+ stationError?.type,
206
214
  ]);
207
215
 
208
216
  const getContent = (): JSX.Element | undefined => {
@@ -223,7 +231,10 @@ export const FormStation = <TValues extends Data, TSubmitResponse = unknown>({
223
231
  // Loading successful
224
232
  return (
225
233
  <>
226
- <SaveOnNavigate isSubmitting={isFormSubmitting} />
234
+ <SaveOnNavigate
235
+ isSubmitting={isFormSubmitting}
236
+ onNavigationCancelled={setValidationError}
237
+ />
227
238
  <div className={classes.main}>
228
239
  <div
229
240
  className={clsx(classes.formWrapper, {
@@ -272,6 +283,7 @@ export const FormStation = <TValues extends Data, TSubmitResponse = unknown>({
272
283
  enableReinitialize={true}
273
284
  >
274
285
  <>
286
+ {validationWatcher}
275
287
  <FormStationHeader
276
288
  titleProperty={titleProperty as string}
277
289
  defaultTitle={defaultTitle}
@@ -296,6 +308,7 @@ export const FormStation = <TValues extends Data, TSubmitResponse = unknown>({
296
308
  width={actionsWidth}
297
309
  validationSchema={validationSchema}
298
310
  setStationError={setStationError}
311
+ setValidationError={setValidationError}
299
312
  submitResponse={submitResponse}
300
313
  alwaysSubmitBeforeAction={alwaysSubmitBeforeAction}
301
314
  className={classes.actionsPanel}
@@ -317,6 +330,7 @@ const FormStationAction = <T, Y>(
317
330
  actions,
318
331
  validationSchema,
319
332
  setStationError,
333
+ setValidationError,
320
334
  submitResponse,
321
335
  alwaysSubmitBeforeAction,
322
336
  className = '',
@@ -352,8 +366,7 @@ const FormStationAction = <T, Y>(
352
366
  !isValid ||
353
367
  (await validationSchema?.isValid(values)) === false
354
368
  ) {
355
- // eslint-disable-next-line no-console
356
- console.log('form invalid, action not performed');
369
+ setValidationError();
357
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)
358
371
  validateForm();
359
372
  return;
@@ -405,6 +418,7 @@ const FormStationAction = <T, Y>(
405
418
  isValid,
406
419
  resetForm,
407
420
  setStationError,
421
+ setValidationError,
408
422
  submitForm,
409
423
  submitResponse,
410
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,5 @@
1
+ import { StationError } from '../models';
2
+
3
+ export interface StationErrorStateType extends StationError {
4
+ type?: string;
5
+ }
@@ -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
+ }