@axinom/mosaic-ui 0.39.0-rc.4 → 0.39.1-feat-gs.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/FormStation/Create/Create.d.ts +1 -1
- package/dist/components/FormStation/Create/Create.d.ts.map +1 -1
- package/dist/components/FormStation/Details/Details.d.ts +2 -2
- package/dist/components/FormStation/Details/Details.d.ts.map +1 -1
- package/dist/components/FormStation/FormContentWrapper/FormContentWrapper.d.ts +11 -0
- package/dist/components/FormStation/FormContentWrapper/FormContentWrapper.d.ts.map +1 -0
- package/dist/components/FormStation/FormStation.d.ts +8 -15
- package/dist/components/FormStation/FormStation.d.ts.map +1 -1
- package/dist/components/FormStation/FormStation.models.d.ts +17 -3
- package/dist/components/FormStation/FormStation.models.d.ts.map +1 -1
- package/dist/components/FormStation/FormStationActions/FormStationActions.d.ts +21 -0
- package/dist/components/FormStation/FormStationActions/FormStationActions.d.ts.map +1 -0
- package/dist/components/FormStation/FormStationContext/FormStationContext.d.ts +13 -0
- package/dist/components/FormStation/FormStationContext/FormStationContext.d.ts.map +1 -0
- package/dist/components/FormStation/FormStationContext/FormStationContextProvider.d.ts +10 -0
- package/dist/components/FormStation/FormStationContext/FormStationContextProvider.d.ts.map +1 -0
- package/dist/components/FormStation/FormStationHeader/FormStationHeader.d.ts +12 -0
- package/dist/components/FormStation/FormStationHeader/FormStationHeader.d.ts.map +1 -0
- package/dist/components/FormStation/helpers/mergeData.d.ts +7 -0
- package/dist/components/FormStation/helpers/mergeData.d.ts.map +1 -0
- package/dist/components/FormStation/helpers/useChangeSets.d.ts +12 -0
- package/dist/components/FormStation/helpers/useChangeSets.d.ts.map +1 -0
- package/dist/components/FormStation/helpers/useDataProvider.d.ts +14 -0
- package/dist/components/FormStation/helpers/useDataProvider.d.ts.map +1 -0
- package/dist/components/FormStation/helpers/useDebouncedFormikValues.d.ts +7 -0
- package/dist/components/FormStation/helpers/useDebouncedFormikValues.d.ts.map +1 -0
- package/dist/components/FormStation/helpers/useUndo.d.ts +6 -0
- package/dist/components/FormStation/helpers/useUndo.d.ts.map +1 -0
- package/dist/components/FormStation/{useValidationError.d.ts → helpers/useValidationError.d.ts} +1 -1
- package/dist/components/FormStation/helpers/useValidationError.d.ts.map +1 -0
- package/dist/components/FormStation/index.d.ts +1 -1
- package/dist/components/FormStation/index.d.ts.map +1 -1
- package/dist/components/Utils/Postgraphile/getArrayDiff.d.ts.map +1 -1
- package/dist/components/Utils/Postgraphile/getFormDiff.d.ts.map +1 -1
- package/dist/helpers/testing.d.ts +4 -1
- package/dist/helpers/testing.d.ts.map +1 -1
- package/dist/index.es.js +4 -4
- package/dist/index.es.js.map +1 -1
- package/dist/index.js +4 -4
- package/dist/index.js.map +1 -1
- package/package.json +3 -4
- package/src/components/Actions/Action/Action.scss +1 -0
- package/src/components/FormElements/Tags/Tags.tsx +3 -3
- package/src/components/FormStation/Create/Create.stories.tsx +1 -9
- package/src/components/FormStation/Create/Create.tsx +4 -1
- package/src/components/FormStation/Details/Details.tsx +5 -2
- package/src/components/FormStation/FormContentWrapper/FormContentWrapper.scss +66 -0
- package/src/components/FormStation/FormContentWrapper/FormContentWrapper.tsx +77 -0
- package/src/components/FormStation/FormStation.models.ts +29 -3
- package/src/components/FormStation/FormStation.scss +0 -70
- package/src/components/FormStation/FormStation.spec.tsx +2 -1
- package/src/components/FormStation/FormStation.stories.tsx +20 -1
- package/src/components/FormStation/FormStation.tsx +68 -403
- package/src/components/FormStation/FormStationActions/FormStationActions.tsx +132 -0
- package/src/components/FormStation/FormStationContext/FormStationContext.ts +22 -0
- package/src/components/FormStation/FormStationContext/FormStationContextProvider.tsx +86 -0
- package/src/components/FormStation/FormStationHeader/FormStationHeader.tsx +85 -0
- package/src/components/FormStation/helpers/mergeData.ts +26 -0
- package/src/components/FormStation/helpers/useChangeSets.ts +70 -0
- package/src/components/FormStation/helpers/useDataProvider.ts +169 -0
- package/src/components/FormStation/helpers/useDebouncedFormikValues.ts +22 -0
- package/src/components/FormStation/helpers/useUndo.ts +43 -0
- package/src/components/FormStation/{useValidationError.tsx → helpers/useValidationError.tsx} +1 -1
- package/src/components/FormStation/index.ts +1 -5
- package/src/components/Utils/Postgraphile/getArrayDiff.ts +7 -6
- package/src/components/Utils/Postgraphile/getFormDiff.ts +2 -1
- package/dist/components/FormStation/StationErrorStateType.d.ts +0 -5
- package/dist/components/FormStation/StationErrorStateType.d.ts.map +0 -1
- package/dist/components/FormStation/useValidationError.d.ts.map +0 -1
- package/src/components/FormStation/StationErrorStateType.tsx +0 -5
|
@@ -1,51 +1,23 @@
|
|
|
1
1
|
import clsx from 'clsx';
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
Formik,
|
|
5
|
-
FormikHelpers,
|
|
6
|
-
FormikValues,
|
|
7
|
-
useFormikContext,
|
|
8
|
-
} from 'formik';
|
|
9
|
-
import React, {
|
|
10
|
-
PropsWithChildren,
|
|
11
|
-
useCallback,
|
|
12
|
-
useEffect,
|
|
13
|
-
useMemo,
|
|
14
|
-
useRef,
|
|
15
|
-
useState,
|
|
16
|
-
} from 'react';
|
|
17
|
-
import { useHistory } from 'react-router-dom';
|
|
2
|
+
import { Formik, FormikValues } from 'formik';
|
|
3
|
+
import React, { PropsWithChildren } from 'react';
|
|
18
4
|
import { OptionalObjectSchema } from 'yup/lib/object';
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
PageHeaderActionType,
|
|
29
|
-
PageHeaderProps,
|
|
30
|
-
} from '../PageHeader';
|
|
31
|
-
import { ErrorType, StationMessage } from '../models';
|
|
32
|
-
import { FormActionData, InitialFormData } from './FormStation.models';
|
|
5
|
+
import type { Data } from '../../types/data';
|
|
6
|
+
import type { StationMessage } from '../models';
|
|
7
|
+
import { FormContentWrapper } from './FormContentWrapper/FormContentWrapper';
|
|
8
|
+
import type {
|
|
9
|
+
FormActionData,
|
|
10
|
+
InitialFormData,
|
|
11
|
+
ObjectSchemaDefinition,
|
|
12
|
+
SaveDataFunction,
|
|
13
|
+
} from './FormStation.models';
|
|
33
14
|
import classes from './FormStation.scss';
|
|
15
|
+
import { FormStationActions } from './FormStationActions/FormStationActions';
|
|
16
|
+
import { FormStationContextProvider } from './FormStationContext/FormStationContextProvider';
|
|
17
|
+
import { FormStationHeader } from './FormStationHeader/FormStationHeader';
|
|
34
18
|
import { SaveOnNavigate } from './SaveOnNavigate/SaveOnNavigate';
|
|
35
|
-
import {
|
|
36
|
-
import { useValidationError } from './useValidationError';
|
|
37
|
-
|
|
38
|
-
export type ObjectSchemaDefinition<
|
|
39
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
40
|
-
T extends Data | null = any,
|
|
41
|
-
> = {
|
|
42
|
-
// TODO: Adding 'any' here since there are a couple of open issues regarding generics in the latest version of yup.
|
|
43
|
-
// https://github.com/jquense/yup/issues/1159
|
|
44
|
-
// https://github.com/jquense/yup/issues/1247
|
|
45
|
-
// Consider revisiting once 'yup' has addressed these issues.
|
|
46
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
47
|
-
[field in keyof T]: any;
|
|
48
|
-
};
|
|
19
|
+
import { useDataProvider } from './helpers/useDataProvider';
|
|
20
|
+
import { useValidationError } from './helpers/useValidationError';
|
|
49
21
|
|
|
50
22
|
export interface FormStationProps<
|
|
51
23
|
TValues extends Data = FormikValues,
|
|
@@ -89,28 +61,11 @@ export interface FormStationProps<
|
|
|
89
61
|
* Called whenever the form needs to be saved.
|
|
90
62
|
* This method needs to throw an exception in case the saving did not succeed.
|
|
91
63
|
*/
|
|
92
|
-
saveData:
|
|
93
|
-
/** The current values of the form */
|
|
94
|
-
values: TValues,
|
|
95
|
-
/** The initial values of the form */
|
|
96
|
-
initialData: InitialFormData<TValues>,
|
|
97
|
-
/** The Formik state helpers */
|
|
98
|
-
formikHelpers: FormikHelpers<TValues>,
|
|
99
|
-
) => Promise<TSubmitResponse> | void;
|
|
64
|
+
saveData: SaveDataFunction<TValues, TSubmitResponse>;
|
|
100
65
|
/** CSS Class name for additional styles */
|
|
101
66
|
className?: string;
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
interface FormActionProps<T, Y> extends Omit<ActionsProps, 'actions'> {
|
|
105
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
106
|
-
validationSchema?: OptionalObjectSchema<ObjectSchemaDefinition<any>>;
|
|
107
|
-
actions?: FormActionData<T, Y>[];
|
|
108
|
-
setStationError: (error: StationErrorStateType) => void;
|
|
109
|
-
setValidationError: () => void;
|
|
110
|
-
submitResponse?: React.MutableRefObject<Y | undefined>;
|
|
111
|
-
alwaysSubmitBeforeAction?: boolean;
|
|
112
|
-
className?: string;
|
|
113
|
-
isFormSubmitting?: boolean;
|
|
67
|
+
/** Periodically calls saveData when the form values change */
|
|
68
|
+
autosave?: boolean;
|
|
114
69
|
}
|
|
115
70
|
|
|
116
71
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
@@ -131,141 +86,30 @@ export const FormStation = <TValues extends Data, TSubmitResponse = unknown>({
|
|
|
131
86
|
alwaysSubmitBeforeAction = false,
|
|
132
87
|
stationMessage,
|
|
133
88
|
className = '',
|
|
89
|
+
autosave = false,
|
|
134
90
|
}: PropsWithChildren<
|
|
135
91
|
FormStationProps<TValues, TSubmitResponse>
|
|
136
92
|
>): JSX.Element => {
|
|
137
|
-
const
|
|
93
|
+
const currentValuesRef = React.useRef<TValues>({} as TValues);
|
|
138
94
|
|
|
139
|
-
const {
|
|
95
|
+
const {
|
|
96
|
+
onSubmit,
|
|
140
97
|
stationError,
|
|
141
98
|
setStationError,
|
|
99
|
+
isFormSubmitting,
|
|
100
|
+
lastSubmittedResponse,
|
|
101
|
+
initialValues,
|
|
102
|
+
} = useDataProvider<TValues, TSubmitResponse>(
|
|
103
|
+
initialData,
|
|
104
|
+
saveData,
|
|
105
|
+
currentValuesRef,
|
|
142
106
|
);
|
|
143
107
|
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
const onSubmit = useCallback(
|
|
148
|
-
async (
|
|
149
|
-
values: TValues,
|
|
150
|
-
formikHelpers: FormikHelpers<TValues>,
|
|
151
|
-
): Promise<void> => {
|
|
152
|
-
if (isFormSubmitting) {
|
|
153
|
-
return;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
try {
|
|
157
|
-
setIsFormSubmitting(true);
|
|
158
|
-
setSaveIndicator(SaveIndicatorType.Saving);
|
|
159
|
-
setStationError(undefined);
|
|
160
|
-
if (!initialData.loading && saveData) {
|
|
161
|
-
const response = await saveData(values, initialData, formikHelpers);
|
|
162
|
-
if (response) {
|
|
163
|
-
submitResponse.current = response;
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
} catch (error) {
|
|
167
|
-
setStationError(
|
|
168
|
-
ErrorTypeToStationError(
|
|
169
|
-
error as ErrorType,
|
|
170
|
-
'An error occurred when trying to save data.',
|
|
171
|
-
),
|
|
172
|
-
);
|
|
173
|
-
|
|
174
|
-
setSaveIndicator(SaveIndicatorType.Dirty);
|
|
175
|
-
|
|
176
|
-
// We still throw the error, to make sure that navigation or action execution
|
|
177
|
-
// will not continue after a failed save.
|
|
178
|
-
throw error;
|
|
179
|
-
} finally {
|
|
180
|
-
formikHelpers.setSubmitting(false);
|
|
181
|
-
setIsFormSubmitting(false);
|
|
182
|
-
}
|
|
183
|
-
},
|
|
184
|
-
[isFormSubmitting, initialData, saveData, setStationError],
|
|
108
|
+
const { setValidationError, validationWatcher } = useValidationError(
|
|
109
|
+
stationError,
|
|
110
|
+
setStationError,
|
|
185
111
|
);
|
|
186
112
|
|
|
187
|
-
useEffect(() => {
|
|
188
|
-
if (
|
|
189
|
-
initialData.error ||
|
|
190
|
-
(initialData.data === null && !initialData.loading) ||
|
|
191
|
-
initialData.entityNotFound
|
|
192
|
-
) {
|
|
193
|
-
const stationError = {
|
|
194
|
-
...ErrorTypeToStationError(
|
|
195
|
-
initialData.error,
|
|
196
|
-
'An error occurred when trying to load data.',
|
|
197
|
-
'Entity not found',
|
|
198
|
-
),
|
|
199
|
-
type: 'loading',
|
|
200
|
-
};
|
|
201
|
-
|
|
202
|
-
setStationError(stationError);
|
|
203
|
-
} else {
|
|
204
|
-
if (stationError?.type === 'loading') {
|
|
205
|
-
// Only clear the error if it is a loading error, which now seems to be cleared.
|
|
206
|
-
setStationError(undefined);
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
}, [
|
|
210
|
-
initialData.loading,
|
|
211
|
-
initialData.error,
|
|
212
|
-
initialData.entityNotFound,
|
|
213
|
-
initialData.data,
|
|
214
|
-
stationError?.type,
|
|
215
|
-
]);
|
|
216
|
-
|
|
217
|
-
const getContent = (): JSX.Element | undefined => {
|
|
218
|
-
if (initialData.loading) {
|
|
219
|
-
// TODO: Loading skeleton of the page
|
|
220
|
-
return;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
if (
|
|
224
|
-
initialData.error ||
|
|
225
|
-
initialData.data === null ||
|
|
226
|
-
initialData.entityNotFound
|
|
227
|
-
) {
|
|
228
|
-
// Error on loading - we can't show the form
|
|
229
|
-
return <div className={classes.loadingError}></div>;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
// Loading successful
|
|
233
|
-
return (
|
|
234
|
-
<>
|
|
235
|
-
<SaveOnNavigate
|
|
236
|
-
isSubmitting={isFormSubmitting}
|
|
237
|
-
onNavigationCancelled={setValidationError}
|
|
238
|
-
/>
|
|
239
|
-
<div className={classes.main}>
|
|
240
|
-
<div
|
|
241
|
-
className={clsx(classes.formWrapper, {
|
|
242
|
-
[classes.hasMessage]: stationMessage,
|
|
243
|
-
})}
|
|
244
|
-
>
|
|
245
|
-
{stationMessage && (
|
|
246
|
-
<MessageBar {...stationMessage}>{stationMessage.body}</MessageBar>
|
|
247
|
-
)}
|
|
248
|
-
<div
|
|
249
|
-
className={clsx(classes.formContainer, {
|
|
250
|
-
[classes.paddedContent]: !edgeToEdgeContent,
|
|
251
|
-
})}
|
|
252
|
-
>
|
|
253
|
-
<Form>
|
|
254
|
-
<>
|
|
255
|
-
{children}
|
|
256
|
-
{/* Adding a invisible text input here to prevent the browser from submitting on "Enter" when there is only a single text input field in the form
|
|
257
|
-
See: https://www.w3.org/MarkUp/html-spec/html-spec_8.html#SEC8.2 */}
|
|
258
|
-
<input type="text" style={{ display: 'none' }} />
|
|
259
|
-
</>
|
|
260
|
-
</Form>
|
|
261
|
-
</div>
|
|
262
|
-
</div>
|
|
263
|
-
{infoPanel}
|
|
264
|
-
</div>
|
|
265
|
-
</>
|
|
266
|
-
);
|
|
267
|
-
};
|
|
268
|
-
|
|
269
113
|
return (
|
|
270
114
|
<div
|
|
271
115
|
className={clsx(
|
|
@@ -278,229 +122,50 @@ export const FormStation = <TValues extends Data, TSubmitResponse = unknown>({
|
|
|
278
122
|
)}
|
|
279
123
|
>
|
|
280
124
|
<Formik
|
|
281
|
-
initialValues={
|
|
125
|
+
initialValues={initialValues ?? ({} as TValues)}
|
|
282
126
|
validationSchema={validationSchema}
|
|
283
127
|
onSubmit={onSubmit}
|
|
284
128
|
enableReinitialize={true}
|
|
285
129
|
>
|
|
286
130
|
<>
|
|
287
131
|
{validationWatcher}
|
|
288
|
-
<
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
onClose={() => setStationError(undefined)}
|
|
300
|
-
>
|
|
301
|
-
{stationError?.body}
|
|
302
|
-
</MessageBar>
|
|
303
|
-
</div>
|
|
304
|
-
)}
|
|
305
|
-
<div className={classes.children}>{getContent()}</div>
|
|
306
|
-
{alwaysShowActionsPanel || (actions?.length ?? 0) > 0 ? (
|
|
307
|
-
<FormStationAction<TValues, TSubmitResponse>
|
|
308
|
-
actions={actions}
|
|
309
|
-
width={actionsWidth}
|
|
310
|
-
validationSchema={validationSchema}
|
|
311
|
-
setStationError={setStationError}
|
|
312
|
-
setValidationError={setValidationError}
|
|
313
|
-
submitResponse={submitResponse}
|
|
314
|
-
alwaysSubmitBeforeAction={alwaysSubmitBeforeAction}
|
|
315
|
-
className={classes.actionsPanel}
|
|
132
|
+
<SaveOnNavigate isSubmitting={isFormSubmitting} />
|
|
133
|
+
<FormStationContextProvider<TValues>
|
|
134
|
+
autosave={autosave}
|
|
135
|
+
currentValuesRef={currentValuesRef}
|
|
136
|
+
>
|
|
137
|
+
<FormStationHeader
|
|
138
|
+
titleProperty={titleProperty as string}
|
|
139
|
+
defaultTitle={defaultTitle}
|
|
140
|
+
subtitle={subtitle}
|
|
141
|
+
cancelNavigationUrl={cancelNavigationUrl}
|
|
142
|
+
isFormSubmitting={isFormSubmitting}
|
|
316
143
|
/>
|
|
317
|
-
|
|
144
|
+
<FormContentWrapper
|
|
145
|
+
stationMessage={stationMessage}
|
|
146
|
+
edgeToEdgeContent={edgeToEdgeContent}
|
|
147
|
+
infoPanel={infoPanel}
|
|
148
|
+
initialData={initialData}
|
|
149
|
+
stationError={stationError}
|
|
150
|
+
setStationError={setStationError}
|
|
151
|
+
>
|
|
152
|
+
{children}
|
|
153
|
+
</FormContentWrapper>
|
|
154
|
+
{alwaysShowActionsPanel || (actions?.length ?? 0) > 0 ? (
|
|
155
|
+
<FormStationActions<TValues, TSubmitResponse>
|
|
156
|
+
actions={actions}
|
|
157
|
+
width={actionsWidth}
|
|
158
|
+
validationSchema={validationSchema}
|
|
159
|
+
setStationError={setStationError}
|
|
160
|
+
setValidationError={setValidationError}
|
|
161
|
+
submitResponse={lastSubmittedResponse}
|
|
162
|
+
alwaysSubmitBeforeAction={alwaysSubmitBeforeAction}
|
|
163
|
+
className={classes.actionsPanel}
|
|
164
|
+
/>
|
|
165
|
+
) : null}
|
|
166
|
+
</FormStationContextProvider>
|
|
318
167
|
</>
|
|
319
168
|
</Formik>
|
|
320
169
|
</div>
|
|
321
170
|
);
|
|
322
171
|
};
|
|
323
|
-
|
|
324
|
-
/**
|
|
325
|
-
* Saves the form before the action is performed.
|
|
326
|
-
*/
|
|
327
|
-
const FormStationAction = <T, Y>(
|
|
328
|
-
props: PropsWithChildren<FormActionProps<T, Y>>,
|
|
329
|
-
): JSX.Element => {
|
|
330
|
-
const {
|
|
331
|
-
actions,
|
|
332
|
-
validationSchema,
|
|
333
|
-
setStationError,
|
|
334
|
-
setValidationError,
|
|
335
|
-
submitResponse,
|
|
336
|
-
alwaysSubmitBeforeAction,
|
|
337
|
-
className = '',
|
|
338
|
-
} = props;
|
|
339
|
-
const {
|
|
340
|
-
submitForm,
|
|
341
|
-
resetForm,
|
|
342
|
-
values,
|
|
343
|
-
validateForm,
|
|
344
|
-
isValid,
|
|
345
|
-
dirty,
|
|
346
|
-
isSubmitting,
|
|
347
|
-
} = useFormikContext<T>();
|
|
348
|
-
|
|
349
|
-
const updatedActions = useMemo(() => {
|
|
350
|
-
return actions?.map((action) => {
|
|
351
|
-
const { onActionSelected } = action;
|
|
352
|
-
|
|
353
|
-
return isNavigationAction(action)
|
|
354
|
-
? action
|
|
355
|
-
: {
|
|
356
|
-
...action,
|
|
357
|
-
isDisabled: action.isDisabled || isSubmitting,
|
|
358
|
-
onActionSelected: () => {
|
|
359
|
-
(async () => {
|
|
360
|
-
//TODO: Busy indicator (disable form?)
|
|
361
|
-
if (dirty || alwaysSubmitBeforeAction) {
|
|
362
|
-
// We can't rely on 'isValid' alone, since that will only evaluate if the form is touched or 'submitForm' is called.
|
|
363
|
-
// On a create station though the from might not be touched but still being invalid.
|
|
364
|
-
// Also the validation on submitForm in here will not help, since the value of isValid is already bound to that method and will not change
|
|
365
|
-
// anymore while this method executes - even if it changes on the context!
|
|
366
|
-
if (
|
|
367
|
-
!isValid ||
|
|
368
|
-
(await validationSchema?.isValid(values)) === false
|
|
369
|
-
) {
|
|
370
|
-
setValidationError();
|
|
371
|
-
// 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)
|
|
372
|
-
validateForm();
|
|
373
|
-
return;
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
try {
|
|
377
|
-
await submitForm();
|
|
378
|
-
|
|
379
|
-
// Committing the changed values to the form to get the current version as the new "reset" state.
|
|
380
|
-
resetForm({ values });
|
|
381
|
-
} catch (error) {
|
|
382
|
-
// we will abort the action if saving is not successful
|
|
383
|
-
// a station error is already set by the "onSubmit"
|
|
384
|
-
return;
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
try {
|
|
389
|
-
const result =
|
|
390
|
-
onActionSelected &&
|
|
391
|
-
(await onActionSelected({
|
|
392
|
-
values,
|
|
393
|
-
submitResponse: submitResponse?.current,
|
|
394
|
-
}));
|
|
395
|
-
if (result !== undefined) {
|
|
396
|
-
setStationError(
|
|
397
|
-
ErrorTypeToStationError(
|
|
398
|
-
result,
|
|
399
|
-
'An error occurred when trying to execute the action.',
|
|
400
|
-
),
|
|
401
|
-
);
|
|
402
|
-
}
|
|
403
|
-
} catch (error) {
|
|
404
|
-
const stationError = ErrorTypeToStationError(
|
|
405
|
-
error as ErrorType,
|
|
406
|
-
'An error occurred when trying to execute the action.',
|
|
407
|
-
);
|
|
408
|
-
setStationError(stationError);
|
|
409
|
-
}
|
|
410
|
-
})();
|
|
411
|
-
},
|
|
412
|
-
};
|
|
413
|
-
});
|
|
414
|
-
}, [
|
|
415
|
-
actions,
|
|
416
|
-
alwaysSubmitBeforeAction,
|
|
417
|
-
dirty,
|
|
418
|
-
isSubmitting,
|
|
419
|
-
isValid,
|
|
420
|
-
resetForm,
|
|
421
|
-
setStationError,
|
|
422
|
-
setValidationError,
|
|
423
|
-
submitForm,
|
|
424
|
-
submitResponse,
|
|
425
|
-
validateForm,
|
|
426
|
-
validationSchema,
|
|
427
|
-
values,
|
|
428
|
-
]);
|
|
429
|
-
|
|
430
|
-
return (
|
|
431
|
-
<div className={className}>
|
|
432
|
-
<Actions actions={updatedActions} />
|
|
433
|
-
</div>
|
|
434
|
-
);
|
|
435
|
-
};
|
|
436
|
-
|
|
437
|
-
/**
|
|
438
|
-
* Handles showRefresh and cancel buttons based on form states
|
|
439
|
-
*/
|
|
440
|
-
const FormStationHeader: React.FC<
|
|
441
|
-
Omit<PageHeaderProps, 'title'> & {
|
|
442
|
-
titleProperty?: string;
|
|
443
|
-
defaultTitle?: string;
|
|
444
|
-
cancelNavigationUrl?: string;
|
|
445
|
-
}
|
|
446
|
-
> = ({ titleProperty, defaultTitle, subtitle, cancelNavigationUrl }) => {
|
|
447
|
-
const { dirty, resetForm, values } = useFormikContext<FormikValues>();
|
|
448
|
-
|
|
449
|
-
useEffect(() => {
|
|
450
|
-
// Set the save indicator to dirty depending on the form state
|
|
451
|
-
if (dirty) {
|
|
452
|
-
setSaveIndicator(SaveIndicatorType.Dirty);
|
|
453
|
-
} else {
|
|
454
|
-
setSaveIndicator(SaveIndicatorType.Inactive);
|
|
455
|
-
}
|
|
456
|
-
return () => {
|
|
457
|
-
// The form is not always considered "not dirty" after the save
|
|
458
|
-
// so this code will make sure that the indicator is set to inactive
|
|
459
|
-
// when the station is left.
|
|
460
|
-
setSaveIndicator(SaveIndicatorType.Inactive);
|
|
461
|
-
};
|
|
462
|
-
}, [dirty]);
|
|
463
|
-
|
|
464
|
-
const history = useHistory();
|
|
465
|
-
|
|
466
|
-
const title =
|
|
467
|
-
titleProperty && values[titleProperty] !== ''
|
|
468
|
-
? values[titleProperty]
|
|
469
|
-
: defaultTitle ?? '';
|
|
470
|
-
|
|
471
|
-
return (
|
|
472
|
-
<PageHeader
|
|
473
|
-
title={title}
|
|
474
|
-
subtitle={subtitle}
|
|
475
|
-
actions={[
|
|
476
|
-
...(dirty === true // add undo action if form as been altered
|
|
477
|
-
? [
|
|
478
|
-
{
|
|
479
|
-
label: 'Undo Changes',
|
|
480
|
-
icon: IconName.Undo,
|
|
481
|
-
actionType: PageHeaderActionType.Context,
|
|
482
|
-
onClick: () => {
|
|
483
|
-
resetForm();
|
|
484
|
-
},
|
|
485
|
-
},
|
|
486
|
-
]
|
|
487
|
-
: []),
|
|
488
|
-
...(cancelNavigationUrl // add cancel action if applicable
|
|
489
|
-
? [
|
|
490
|
-
{
|
|
491
|
-
label: 'Cancel',
|
|
492
|
-
icon: IconName.X,
|
|
493
|
-
onClick: () => {
|
|
494
|
-
resetForm();
|
|
495
|
-
// If the form has errors, Navigation needs to be wrapped in a promise or timeout.
|
|
496
|
-
Promise.resolve().then(() =>
|
|
497
|
-
history.push(cancelNavigationUrl),
|
|
498
|
-
);
|
|
499
|
-
},
|
|
500
|
-
},
|
|
501
|
-
]
|
|
502
|
-
: []),
|
|
503
|
-
]}
|
|
504
|
-
/>
|
|
505
|
-
);
|
|
506
|
-
};
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { useFormikContext } from 'formik';
|
|
2
|
+
import React, { PropsWithChildren, useMemo } from 'react';
|
|
3
|
+
import { OptionalObjectSchema } from 'yup/lib/object';
|
|
4
|
+
import { ErrorTypeToStationError } from '../../../utils/ErrorTypeToStationError';
|
|
5
|
+
import { Actions, ActionsProps } from '../../Actions';
|
|
6
|
+
import { isNavigationAction } from '../../Actions/Action/Action';
|
|
7
|
+
import { ErrorType, StationError } from '../../models';
|
|
8
|
+
import { FormActionData, ObjectSchemaDefinition } from '../FormStation.models';
|
|
9
|
+
|
|
10
|
+
interface FormActionProps<T, Y> extends Omit<ActionsProps, 'actions'> {
|
|
11
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
12
|
+
validationSchema?: OptionalObjectSchema<ObjectSchemaDefinition<any>>;
|
|
13
|
+
actions?: FormActionData<T, Y>[];
|
|
14
|
+
setStationError: (error: StationError) => void;
|
|
15
|
+
setValidationError: () => void;
|
|
16
|
+
submitResponse?: React.MutableRefObject<Y | undefined>;
|
|
17
|
+
alwaysSubmitBeforeAction?: boolean;
|
|
18
|
+
className?: string;
|
|
19
|
+
isFormSubmitting?: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Saves the form before the action is performed.
|
|
24
|
+
*/
|
|
25
|
+
export const FormStationActions = <T, Y>(
|
|
26
|
+
props: PropsWithChildren<FormActionProps<T, Y>>,
|
|
27
|
+
): JSX.Element => {
|
|
28
|
+
const {
|
|
29
|
+
actions,
|
|
30
|
+
validationSchema,
|
|
31
|
+
setStationError,
|
|
32
|
+
submitResponse,
|
|
33
|
+
alwaysSubmitBeforeAction,
|
|
34
|
+
className = '',
|
|
35
|
+
} = props;
|
|
36
|
+
const {
|
|
37
|
+
submitForm,
|
|
38
|
+
resetForm,
|
|
39
|
+
values,
|
|
40
|
+
validateForm,
|
|
41
|
+
isValid,
|
|
42
|
+
dirty,
|
|
43
|
+
isSubmitting,
|
|
44
|
+
} = useFormikContext<T>();
|
|
45
|
+
|
|
46
|
+
const updatedActions = useMemo(() => {
|
|
47
|
+
return actions?.map((action) => {
|
|
48
|
+
const { onActionSelected } = action;
|
|
49
|
+
|
|
50
|
+
return isNavigationAction(action)
|
|
51
|
+
? action
|
|
52
|
+
: {
|
|
53
|
+
...action,
|
|
54
|
+
isDisabled: action.isDisabled || isSubmitting,
|
|
55
|
+
onActionSelected: () => {
|
|
56
|
+
(async () => {
|
|
57
|
+
//TODO: Busy indicator (disable form?)
|
|
58
|
+
if (dirty || alwaysSubmitBeforeAction) {
|
|
59
|
+
// We can't rely on 'isValid' alone, since that will only evaluate if the form is touched or 'submitForm' is called.
|
|
60
|
+
// On a create station though the from might not be touched but still being invalid.
|
|
61
|
+
// Also the validation on submitForm in here will not help, since the value of isValid is already bound to that method and will not change
|
|
62
|
+
// anymore while this method executes - even if it changes on the context!
|
|
63
|
+
if (
|
|
64
|
+
!isValid ||
|
|
65
|
+
(await validationSchema?.isValid(values)) === false
|
|
66
|
+
) {
|
|
67
|
+
// eslint-disable-next-line no-console
|
|
68
|
+
console.log('form invalid, action not performed');
|
|
69
|
+
// 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)
|
|
70
|
+
validateForm();
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
await submitForm();
|
|
76
|
+
|
|
77
|
+
// Committing the changed values to the form to get the current version as the new "reset" state.
|
|
78
|
+
resetForm({ values });
|
|
79
|
+
} catch (error) {
|
|
80
|
+
// we will abort the action if saving is not successful
|
|
81
|
+
// a station error is already set by the "onSubmit"
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
const result =
|
|
88
|
+
onActionSelected &&
|
|
89
|
+
(await onActionSelected({
|
|
90
|
+
values,
|
|
91
|
+
submitResponse: submitResponse?.current,
|
|
92
|
+
}));
|
|
93
|
+
if (result !== undefined) {
|
|
94
|
+
setStationError(
|
|
95
|
+
ErrorTypeToStationError(
|
|
96
|
+
result,
|
|
97
|
+
'An error occurred when trying to execute the action.',
|
|
98
|
+
),
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
} catch (error) {
|
|
102
|
+
const stationError = ErrorTypeToStationError(
|
|
103
|
+
error as ErrorType,
|
|
104
|
+
'An error occurred when trying to execute the action.',
|
|
105
|
+
);
|
|
106
|
+
setStationError(stationError);
|
|
107
|
+
}
|
|
108
|
+
})();
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
});
|
|
112
|
+
}, [
|
|
113
|
+
actions,
|
|
114
|
+
alwaysSubmitBeforeAction,
|
|
115
|
+
dirty,
|
|
116
|
+
isSubmitting,
|
|
117
|
+
isValid,
|
|
118
|
+
resetForm,
|
|
119
|
+
setStationError,
|
|
120
|
+
submitForm,
|
|
121
|
+
submitResponse,
|
|
122
|
+
validateForm,
|
|
123
|
+
validationSchema,
|
|
124
|
+
values,
|
|
125
|
+
]);
|
|
126
|
+
|
|
127
|
+
return (
|
|
128
|
+
<div className={className}>
|
|
129
|
+
<Actions actions={updatedActions} />
|
|
130
|
+
</div>
|
|
131
|
+
);
|
|
132
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { FormikValues } from 'formik';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { noop } from '../../../helpers/utils';
|
|
4
|
+
import { ChangeSet } from '../helpers/useChangeSets';
|
|
5
|
+
|
|
6
|
+
export interface FormStationContextType {
|
|
7
|
+
changeSets: ChangeSet<FormikValues>[];
|
|
8
|
+
pushChangeSet: (value: never) => void;
|
|
9
|
+
popChangeSet: () => ChangeSet<FormikValues> | undefined;
|
|
10
|
+
clearChangeSets: () => FormikValues;
|
|
11
|
+
lastUndoneValue: React.MutableRefObject<FormikValues>;
|
|
12
|
+
autosave: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const FormStationContext = React.createContext<FormStationContextType>({
|
|
16
|
+
changeSets: [],
|
|
17
|
+
pushChangeSet: noop,
|
|
18
|
+
popChangeSet: () => ({ prev: {}, next: {} }),
|
|
19
|
+
clearChangeSets: () => ({}),
|
|
20
|
+
lastUndoneValue: { current: {} },
|
|
21
|
+
autosave: false,
|
|
22
|
+
});
|