@firecms/core 3.0.0-canary.186 → 3.0.0-canary.187
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/core/EntityEditView.d.ts +4 -15
- package/dist/core/EntityForm.d.ts +43 -0
- package/dist/form/components/FormEntry.d.ts +6 -0
- package/dist/form/components/FormLayout.d.ts +5 -0
- package/dist/form/components/index.d.ts +2 -0
- package/dist/form/field_bindings/ArrayCustomShapedFieldBinding.d.ts +1 -1
- package/dist/form/field_bindings/MapFieldBinding.d.ts +1 -1
- package/dist/index.es.js +11960 -11691
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +11938 -11669
- package/dist/index.umd.js.map +1 -1
- package/dist/types/fields.d.ts +11 -0
- package/dist/types/navigation.d.ts +1 -0
- package/dist/types/plugins.d.ts +1 -1
- package/dist/types/properties.d.ts +1 -1
- package/package.json +5 -5
- package/src/components/EntityCollectionTable/internal/popup_field/PopupFormField.tsx +4 -2
- package/src/core/EntityEditView.tsx +210 -1049
- package/src/core/EntityForm.tsx +952 -0
- package/src/core/NavigationRoutes.tsx +16 -12
- package/src/form/components/FormEntry.tsx +22 -0
- package/src/form/components/FormLayout.tsx +16 -0
- package/src/form/components/index.tsx +2 -0
- package/src/form/field_bindings/ArrayCustomShapedFieldBinding.tsx +0 -2
- package/src/form/field_bindings/MapFieldBinding.tsx +0 -2
- package/src/routes/FireCMSRoute.tsx +7 -2
- package/src/types/fields.tsx +16 -0
- package/src/types/navigation.ts +1 -0
- package/src/types/plugins.tsx +1 -1
- package/src/types/properties.ts +1 -1
- package/src/util/entity_cache.ts +10 -1
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
SideEntityController,
|
|
17
17
|
User
|
|
18
18
|
} from "../types";
|
|
19
|
-
import equal from "react-fast-compare"
|
|
19
|
+
import equal from "react-fast-compare";
|
|
20
20
|
|
|
21
21
|
import {
|
|
22
22
|
CircularProgressCenter,
|
|
@@ -76,19 +76,20 @@ import {
|
|
|
76
76
|
} from "@firecms/ui";
|
|
77
77
|
import { Formex, FormexController, getIn, setIn, useCreateFormex } from "@firecms/formex";
|
|
78
78
|
import { useAnalyticsController } from "../hooks/useAnalyticsController";
|
|
79
|
-
import {
|
|
80
|
-
import { LabelWithIconAndTooltip, PropertyFieldBinding } from "../form";
|
|
79
|
+
import { FormEntry, FormLayout, LabelWithIconAndTooltip, PropertyFieldBinding } from "../form";
|
|
81
80
|
import { ValidationError } from "yup";
|
|
82
81
|
import { getEntityFromCache, removeEntityFromCache, saveEntityToCache } from "../util/entity_cache";
|
|
83
82
|
import { CustomIdField } from "../form/components/CustomIdField";
|
|
84
83
|
import { ErrorFocus } from "../form/components/ErrorFocus";
|
|
84
|
+
import { CustomFieldValidator, getYupEntitySchema } from "../form/validation";
|
|
85
|
+
import { EntityForm, FormLayoutInner } from "./EntityForm";
|
|
85
86
|
|
|
86
87
|
const MAIN_TAB_VALUE = "main_##Q$SC^#S6";
|
|
87
88
|
|
|
88
89
|
export type OnUpdateParams = {
|
|
89
90
|
entity: Entity<any>,
|
|
90
91
|
status: EntityStatus,
|
|
91
|
-
path: string
|
|
92
|
+
path: string,
|
|
92
93
|
entityId?: string;
|
|
93
94
|
selectedTab?: string;
|
|
94
95
|
collection: EntityCollection<any>
|
|
@@ -120,8 +121,6 @@ export interface EntityEditViewProps<M extends Record<string, any>> {
|
|
|
120
121
|
/**
|
|
121
122
|
* This is the default view that is used as the content of a side panel when
|
|
122
123
|
* an entity is opened.
|
|
123
|
-
* You probably don't want to use this view directly since it is bound to the
|
|
124
|
-
* side panel.
|
|
125
124
|
*/
|
|
126
125
|
export function EntityEditView<M extends Record<string, any>, USER extends User>({
|
|
127
126
|
entityId,
|
|
@@ -141,67 +140,94 @@ export function EntityEditView<M extends Record<string, any>, USER extends User>
|
|
|
141
140
|
useCache: false
|
|
142
141
|
});
|
|
143
142
|
|
|
144
|
-
const cachedValues = entityId
|
|
143
|
+
const cachedValues = entityId
|
|
144
|
+
? getEntityFromCache(props.path + "/" + entityId)
|
|
145
|
+
: getEntityFromCache(props.path + "#new");
|
|
146
|
+
|
|
147
|
+
const authController = useAuthController();
|
|
148
|
+
|
|
149
|
+
const initialStatus = props.copy ? "copy" : (entityId ? "existing" : "new");
|
|
150
|
+
const [status, setStatus] = useState<EntityStatus>(initialStatus);
|
|
145
151
|
|
|
146
|
-
|
|
147
|
-
|
|
152
|
+
const canEdit = useMemo(() => {
|
|
153
|
+
if (status === "new" || status === "copy") {
|
|
154
|
+
return true;
|
|
155
|
+
} else {
|
|
156
|
+
return entity ? canEditEntity(props.collection, authController, props.path, entity ?? null) : undefined;
|
|
157
|
+
}
|
|
158
|
+
}, [authController, entity, status]);
|
|
159
|
+
|
|
160
|
+
if ((dataLoading && !cachedValues) || (!entity || canEdit === undefined) && (status === "existing" || status === "copy")) {
|
|
161
|
+
return <CircularProgressCenter/>;
|
|
148
162
|
}
|
|
149
163
|
|
|
150
164
|
if (entityId && !entity && !cachedValues) {
|
|
151
165
|
console.error(`Entity with id ${entityId} not found in collection ${props.path}`);
|
|
152
166
|
}
|
|
153
167
|
|
|
168
|
+
if (!canEdit) {
|
|
169
|
+
return <div className={"flex flex-col"}>
|
|
170
|
+
<Typography className={"mt-16 mb-8 mx-8"} variant={"h4"}>
|
|
171
|
+
{props.collection.singularName ?? props.collection.name}
|
|
172
|
+
</Typography>
|
|
173
|
+
<EntityView
|
|
174
|
+
className={"px-8"}
|
|
175
|
+
entity={entity as Entity<M>}
|
|
176
|
+
path={props.path}
|
|
177
|
+
collection={props.collection}/>
|
|
178
|
+
</div>
|
|
179
|
+
}
|
|
180
|
+
|
|
154
181
|
return <EntityEditViewInner<M> {...props}
|
|
155
182
|
entityId={entityId}
|
|
156
183
|
entity={entity}
|
|
157
184
|
cachedDirtyValues={cachedValues as Partial<M>}
|
|
158
185
|
dataLoading={dataLoading}
|
|
186
|
+
status={status}
|
|
187
|
+
setStatus={setStatus}
|
|
159
188
|
/>;
|
|
160
189
|
}
|
|
161
190
|
|
|
162
|
-
function FormLayout({
|
|
163
|
-
id,
|
|
164
|
-
formex,
|
|
165
|
-
children,
|
|
166
|
-
className
|
|
167
|
-
}: {
|
|
168
|
-
id?: string,
|
|
169
|
-
formex: FormexController<any>,
|
|
170
|
-
children: React.ReactNode,
|
|
171
|
-
className?: string
|
|
172
|
-
}) {
|
|
173
191
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
className={"self-end sticky top-4 z-10"}>
|
|
190
|
-
<Chip size={"small"}>
|
|
191
|
-
<CheckIcon size={"smallest"}/>
|
|
192
|
-
</Chip>
|
|
193
|
-
</Tooltip>}
|
|
194
|
-
|
|
195
|
-
{children}
|
|
192
|
+
function SecondaryForm<M extends object>({
|
|
193
|
+
collection,
|
|
194
|
+
className,
|
|
195
|
+
customView,
|
|
196
|
+
entity,
|
|
197
|
+
formContext,
|
|
198
|
+
forceActionsAtTheBottom
|
|
199
|
+
}: {
|
|
200
|
+
className?: string,
|
|
201
|
+
customView: EntityCustomView,
|
|
202
|
+
formContext: FormContext<M>,
|
|
203
|
+
collection: EntityCollection<M>,
|
|
204
|
+
forceActionsAtTheBottom?: boolean,
|
|
205
|
+
entity: Entity<M> | undefined
|
|
206
|
+
}) {
|
|
196
207
|
|
|
197
|
-
|
|
208
|
+
if (!customView.Builder) {
|
|
209
|
+
console.error("customView.Builder is not defined");
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
198
212
|
|
|
199
|
-
|
|
213
|
+
return <FormLayoutInner
|
|
214
|
+
className={className}
|
|
215
|
+
forceActionsAtTheBottom={forceActionsAtTheBottom}
|
|
216
|
+
formContext={formContext}>
|
|
217
|
+
<ErrorBoundary>
|
|
218
|
+
{formContext && <customView.Builder
|
|
219
|
+
collection={collection}
|
|
220
|
+
entity={entity}
|
|
221
|
+
modifiedValues={formContext.formex.values ?? entity?.values}
|
|
222
|
+
formContext={formContext}
|
|
223
|
+
/>}
|
|
224
|
+
</ErrorBoundary>
|
|
225
|
+
</FormLayoutInner>;
|
|
200
226
|
}
|
|
201
227
|
|
|
202
228
|
export function EntityEditViewInner<M extends Record<string, any>>({
|
|
203
229
|
path,
|
|
204
|
-
entityId
|
|
230
|
+
entityId,
|
|
205
231
|
selectedTab: selectedTabProp,
|
|
206
232
|
copy,
|
|
207
233
|
collection,
|
|
@@ -214,79 +240,32 @@ export function EntityEditViewInner<M extends Record<string, any>>({
|
|
|
214
240
|
cachedDirtyValues,
|
|
215
241
|
dataLoading,
|
|
216
242
|
layout = "side_panel",
|
|
217
|
-
barActions
|
|
243
|
+
barActions,
|
|
244
|
+
status,
|
|
245
|
+
setStatus,
|
|
218
246
|
}: EntityEditViewProps<M> & {
|
|
219
247
|
entity?: Entity<M>,
|
|
220
248
|
cachedDirtyValues?: Partial<M>, // dirty cached entity in memory
|
|
221
249
|
dataLoading: boolean,
|
|
250
|
+
status: EntityStatus,
|
|
251
|
+
setStatus: (status: EntityStatus) => void,
|
|
222
252
|
}) {
|
|
223
253
|
|
|
224
|
-
const largeLayout = useLargeLayout();
|
|
225
|
-
if (collection.customId && collection.formAutoSave) {
|
|
226
|
-
console.warn(`The collection ${collection.path} has customId and formAutoSave enabled. This is not supported and formAutoSave will be ignored`);
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
const [saving, setSaving] = useState(false);
|
|
230
|
-
/**
|
|
231
|
-
* These are the values that are being saved. They are debounced.
|
|
232
|
-
* We use this only when autoSave is enabled.
|
|
233
|
-
*/
|
|
234
|
-
const [valuesToBeSaved, setValuesToBeSaved] = useState<EntityValues<M> | undefined>(undefined);
|
|
235
|
-
useDebouncedCallback(valuesToBeSaved, () => {
|
|
236
|
-
if (valuesToBeSaved)
|
|
237
|
-
saveEntity({
|
|
238
|
-
entityId: usedEntity?.id,
|
|
239
|
-
collection,
|
|
240
|
-
path,
|
|
241
|
-
values: valuesToBeSaved,
|
|
242
|
-
closeAfterSave: false
|
|
243
|
-
});
|
|
244
|
-
}, false, 2000);
|
|
245
|
-
|
|
246
|
-
const inputCollection = collection;
|
|
247
|
-
|
|
248
|
-
const authController = useAuthController();
|
|
249
|
-
const dataSource = useDataSource(collection);
|
|
250
|
-
const sideEntityController = useSideEntityController();
|
|
251
|
-
const snackbarController = useSnackbarController();
|
|
252
|
-
const customizationController = useCustomizationController();
|
|
253
254
|
const context = useFireCMSContext();
|
|
254
255
|
|
|
255
|
-
const
|
|
256
|
-
|
|
257
|
-
const analyticsController = useAnalyticsController();
|
|
258
|
-
|
|
259
|
-
const initialResolvedCollection = useMemo(() => resolveCollection({
|
|
260
|
-
collection: inputCollection,
|
|
261
|
-
path,
|
|
262
|
-
values: entity?.values,
|
|
263
|
-
propertyConfigs: customizationController.propertyConfigs
|
|
264
|
-
}), [entity?.values, path, customizationController.propertyConfigs]);
|
|
265
|
-
|
|
266
|
-
const initialStatus = copy ? "copy" : (entityIdProp ? "existing" : "new");
|
|
267
|
-
const [status, setStatus] = useState<EntityStatus>(initialStatus);
|
|
268
|
-
|
|
269
|
-
const mustSetCustomId: boolean = (status === "new" || status === "copy") &&
|
|
270
|
-
(Boolean(initialResolvedCollection.customId) && initialResolvedCollection.customId !== "optional");
|
|
256
|
+
const [usedEntity, setUsedEntity] = useState<Entity<M> | undefined>(entity);
|
|
271
257
|
|
|
272
|
-
|
|
273
|
-
if (
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
} else {
|
|
277
|
-
return dataSource.generateEntityId(path, collection);
|
|
278
|
-
}
|
|
279
|
-
} else {
|
|
280
|
-
return entityIdProp;
|
|
281
|
-
}
|
|
282
|
-
}, [entityIdProp, status]);
|
|
258
|
+
useEffect(() => {
|
|
259
|
+
if (entity)
|
|
260
|
+
setUsedEntity(entity);
|
|
261
|
+
}, [entity]);
|
|
283
262
|
|
|
284
|
-
|
|
263
|
+
// Instead of using a ref (which does not trigger re-render), we use state for the form context.
|
|
264
|
+
const [formContext, setFormContext] = useState<FormContext<M> | undefined>(undefined);
|
|
285
265
|
|
|
286
|
-
const
|
|
287
|
-
const [savingError, setSavingError] = React.useState<Error | undefined>();
|
|
266
|
+
const largeLayout = useLargeLayout();
|
|
288
267
|
|
|
289
|
-
const
|
|
268
|
+
const customizationController = useCustomizationController();
|
|
290
269
|
|
|
291
270
|
const defaultSelectedView = useMemo(() => resolveDefaultSelectedView(
|
|
292
271
|
collection ? collection.defaultSelectedView : undefined,
|
|
@@ -310,373 +289,100 @@ export function EntityEditViewInner<M extends Record<string, any>>({
|
|
|
310
289
|
const subcollectionsCount = subcollections?.length ?? 0;
|
|
311
290
|
const customViews = collection.entityViews;
|
|
312
291
|
const customViewsCount = customViews?.length ?? 0;
|
|
313
|
-
const autoSave = collection.formAutoSave && !collection.customId;
|
|
314
|
-
|
|
315
292
|
const hasAdditionalViews = customViewsCount > 0 || subcollectionsCount > 0;
|
|
316
293
|
|
|
317
|
-
const [usedEntity, setUsedEntity] = useState<Entity<M> | undefined>(entity);
|
|
318
|
-
|
|
319
|
-
const baseDataSourceValuesRef = useRef<Partial<EntityValues<M>> | null>(cachedDirtyValues ?? getDataSourceEntityValues(initialResolvedCollection, status, usedEntity));
|
|
320
|
-
|
|
321
|
-
useEffect(() => {
|
|
322
|
-
if (entity)
|
|
323
|
-
setUsedEntity(entity);
|
|
324
|
-
}, [entity]);
|
|
325
|
-
|
|
326
|
-
const canEdit = useMemo(() => {
|
|
327
|
-
if (status === "new" || status === "copy") {
|
|
328
|
-
return true;
|
|
329
|
-
} else {
|
|
330
|
-
return usedEntity ? canEditEntity(collection, authController, path, usedEntity ?? null) : false;
|
|
331
|
-
}
|
|
332
|
-
}, [authController, usedEntity, status]);
|
|
333
|
-
|
|
334
|
-
const readOnly = !canEdit;
|
|
335
|
-
|
|
336
|
-
const onPreSaveHookError = useCallback((e: Error) => {
|
|
337
|
-
setSaving(false);
|
|
338
|
-
snackbarController.open({
|
|
339
|
-
type: "error",
|
|
340
|
-
message: "Error before saving: " + e?.message
|
|
341
|
-
});
|
|
342
|
-
console.error(e);
|
|
343
|
-
}, [snackbarController]);
|
|
344
|
-
|
|
345
|
-
const onSaveSuccessHookError = useCallback((e: Error) => {
|
|
346
|
-
setSaving(false);
|
|
347
|
-
snackbarController.open({
|
|
348
|
-
type: "error",
|
|
349
|
-
message: "Error after saving (entity is saved): " + e?.message
|
|
350
|
-
});
|
|
351
|
-
console.error(e);
|
|
352
|
-
}, [snackbarController]);
|
|
353
|
-
|
|
354
|
-
function clearDirtyCache() {
|
|
355
|
-
if (status === "new" || status === "copy") {
|
|
356
|
-
removeEntityFromCache(path + "#new");
|
|
357
|
-
} else {
|
|
358
|
-
removeEntityFromCache(path + "/" + entityId);
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
const onSaveSuccess = (updatedEntity: Entity<M>, closeAfterSave: boolean) => {
|
|
363
|
-
|
|
364
|
-
clearDirtyCache();
|
|
365
|
-
|
|
366
|
-
onValuesModified?.(false);
|
|
367
|
-
|
|
368
|
-
setSaving(false);
|
|
369
|
-
if (!autoSave)
|
|
370
|
-
snackbarController.open({
|
|
371
|
-
type: "success",
|
|
372
|
-
message: `${collection.singularName ?? collection.name}: Saved correctly`
|
|
373
|
-
});
|
|
374
|
-
|
|
375
|
-
setUsedEntity(updatedEntity);
|
|
376
|
-
setStatus("existing");
|
|
377
|
-
setEntityId(updatedEntity.id);
|
|
378
|
-
|
|
379
|
-
if (onSaved) {
|
|
380
|
-
onSaved({
|
|
381
|
-
entity: updatedEntity,
|
|
382
|
-
status,
|
|
383
|
-
path,
|
|
384
|
-
entityId: updatedEntity.id,
|
|
385
|
-
selectedTab: MAIN_TAB_VALUE === selectedTab ? undefined : selectedTab,
|
|
386
|
-
collection
|
|
387
|
-
});
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
if (closeAfterSave) {
|
|
391
|
-
onClose?.();
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
};
|
|
395
|
-
|
|
396
|
-
const onSaveFailure = useCallback((e: Error) => {
|
|
397
|
-
|
|
398
|
-
setSaving(false);
|
|
399
|
-
snackbarController.open({
|
|
400
|
-
type: "error",
|
|
401
|
-
message: "Error saving: " + e?.message
|
|
402
|
-
});
|
|
403
|
-
|
|
404
|
-
console.error("Error saving entity", path, entityId);
|
|
405
|
-
console.error(e);
|
|
406
|
-
}, [entityId, path, snackbarController]);
|
|
407
|
-
|
|
408
|
-
const saveEntity = ({
|
|
409
|
-
values,
|
|
410
|
-
previousValues,
|
|
411
|
-
closeAfterSave,
|
|
412
|
-
entityId,
|
|
413
|
-
collection,
|
|
414
|
-
path
|
|
415
|
-
}: {
|
|
416
|
-
collection: EntityCollection<M>,
|
|
417
|
-
path: string,
|
|
418
|
-
entityId: string | undefined,
|
|
419
|
-
values: M,
|
|
420
|
-
previousValues?: M,
|
|
421
|
-
closeAfterSave: boolean,
|
|
422
|
-
}) => {
|
|
423
|
-
setSaving(true);
|
|
424
|
-
return saveEntityWithCallbacks({
|
|
425
|
-
path,
|
|
426
|
-
entityId,
|
|
427
|
-
values,
|
|
428
|
-
previousValues,
|
|
429
|
-
collection,
|
|
430
|
-
status,
|
|
431
|
-
dataSource,
|
|
432
|
-
context,
|
|
433
|
-
onSaveSuccess: (updatedEntity: Entity<M>) => onSaveSuccess(updatedEntity, closeAfterSave),
|
|
434
|
-
onSaveFailure,
|
|
435
|
-
onPreSaveHookError,
|
|
436
|
-
onSaveSuccessHookError
|
|
437
|
-
}).then();
|
|
438
|
-
};
|
|
439
|
-
|
|
440
|
-
const onSaveEntityRequest = async ({
|
|
441
|
-
collection,
|
|
442
|
-
path,
|
|
443
|
-
entityId,
|
|
444
|
-
values,
|
|
445
|
-
previousValues,
|
|
446
|
-
closeAfterSave,
|
|
447
|
-
autoSave
|
|
448
|
-
}: EntityFormSaveParams<M>): Promise<void> => {
|
|
449
|
-
if (!status)
|
|
450
|
-
return;
|
|
451
|
-
|
|
452
|
-
if (autoSave) {
|
|
453
|
-
setValuesToBeSaved(values);
|
|
454
|
-
} else {
|
|
455
|
-
return saveEntity({
|
|
456
|
-
collection,
|
|
457
|
-
path,
|
|
458
|
-
entityId,
|
|
459
|
-
values,
|
|
460
|
-
previousValues,
|
|
461
|
-
closeAfterSave
|
|
462
|
-
});
|
|
463
|
-
}
|
|
464
|
-
};
|
|
465
|
-
|
|
466
|
-
const onSubmit = (values: EntityValues<M>, formexController: FormexController<EntityValues<M>>) => {
|
|
467
|
-
|
|
468
|
-
if (mustSetCustomId && !entityId) {
|
|
469
|
-
console.error("Missing custom Id");
|
|
470
|
-
setEntityIdError(true);
|
|
471
|
-
formexController.setSubmitting(false);
|
|
472
|
-
return;
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
setSavingError(undefined);
|
|
476
|
-
setEntityIdError(false);
|
|
477
|
-
|
|
478
|
-
if (status === "existing") {
|
|
479
|
-
if (!entity?.id) throw Error("Form misconfiguration when saving, no id for existing entity");
|
|
480
|
-
} else if (status === "new" || status === "copy") {
|
|
481
|
-
if (inputCollection.customId) {
|
|
482
|
-
if (inputCollection.customId !== "optional" && !entityId) {
|
|
483
|
-
throw Error("Form misconfiguration when saving, entityId should be set");
|
|
484
|
-
}
|
|
485
|
-
}
|
|
486
|
-
} else {
|
|
487
|
-
throw Error("New FormType added, check EntityForm");
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
return save(values)
|
|
491
|
-
?.then(_ => {
|
|
492
|
-
formexController.resetForm({
|
|
493
|
-
values,
|
|
494
|
-
submitCount: 0,
|
|
495
|
-
touched: {}
|
|
496
|
-
});
|
|
497
|
-
})
|
|
498
|
-
.finally(() => {
|
|
499
|
-
formexController.setSubmitting(false);
|
|
500
|
-
});
|
|
501
|
-
|
|
502
|
-
};
|
|
503
|
-
|
|
504
|
-
const formex: FormexController<M> = useCreateFormex<M>({
|
|
505
|
-
initialValues: baseDataSourceValuesRef.current as M,
|
|
506
|
-
initialDirty: Boolean(cachedDirtyValues),
|
|
507
|
-
onSubmit,
|
|
508
|
-
validation: (values) => {
|
|
509
|
-
return validationSchema?.validate(values, { abortEarly: false })
|
|
510
|
-
.then(() => {
|
|
511
|
-
return {};
|
|
512
|
-
})
|
|
513
|
-
.catch((e: any) => {
|
|
514
|
-
// const errors: Record<string, string> = {};
|
|
515
|
-
// e.inner.forEach((error: any) => {
|
|
516
|
-
// errors[error.path] = error.message;
|
|
517
|
-
// });
|
|
518
|
-
return yupToFormErrors(e);
|
|
519
|
-
});
|
|
520
|
-
}
|
|
521
|
-
});
|
|
522
|
-
|
|
523
|
-
const resolvedCollection = resolveCollection<M>({
|
|
524
|
-
collection: inputCollection,
|
|
525
|
-
path,
|
|
526
|
-
entityId,
|
|
527
|
-
values: formex.values,
|
|
528
|
-
previousValues: formex.initialValues,
|
|
529
|
-
propertyConfigs: customizationController.propertyConfigs
|
|
530
|
-
});
|
|
531
|
-
|
|
532
|
-
const lastSavedValues = useRef<EntityValues<M> | undefined>(entity?.values);
|
|
533
|
-
|
|
534
|
-
const save = (values: EntityValues<M>): Promise<void> => {
|
|
535
|
-
lastSavedValues.current = values;
|
|
536
|
-
return onSaveEntityRequest({
|
|
537
|
-
collection: resolvedCollection,
|
|
538
|
-
path,
|
|
539
|
-
entityId,
|
|
540
|
-
values,
|
|
541
|
-
previousValues: entity?.values,
|
|
542
|
-
closeAfterSave: closeAfterSaveRef.current,
|
|
543
|
-
autoSave: autoSave ?? false
|
|
544
|
-
}).then(_ => {
|
|
545
|
-
const eventName: CMSAnalyticsEvent = status === "new"
|
|
546
|
-
? "new_entity_saved"
|
|
547
|
-
: (status === "copy" ? "entity_copied" : (status === "existing" ? "entity_edited" : "unmapped_event"));
|
|
548
|
-
analyticsController.onAnalyticsEvent?.(eventName, { path });
|
|
549
|
-
}).catch(e => {
|
|
550
|
-
console.error(e);
|
|
551
|
-
setSavingError(e);
|
|
552
|
-
}).finally(() => {
|
|
553
|
-
closeAfterSaveRef.current = false;
|
|
554
|
-
});
|
|
555
|
-
};
|
|
556
|
-
|
|
557
|
-
const formContext: FormContext<M> = {
|
|
558
|
-
// @ts-ignore
|
|
559
|
-
setFieldValue: useCallback(formex.setFieldValue, []),
|
|
560
|
-
values: formex.values,
|
|
561
|
-
collection: resolvedCollection,
|
|
562
|
-
entityId: entityId as string,
|
|
563
|
-
path,
|
|
564
|
-
save,
|
|
565
|
-
formex
|
|
566
|
-
};
|
|
567
|
-
|
|
568
294
|
const resolvedEntityViews = customViews ? customViews
|
|
569
295
|
.map(e => resolveEntityView(e, customizationController.entityViews))
|
|
570
296
|
.filter(Boolean) as EntityCustomView[]
|
|
571
297
|
: [];
|
|
572
298
|
|
|
573
299
|
const selectedEntityView = resolvedEntityViews.find(e => e.key === selectedTab);
|
|
574
|
-
const
|
|
575
|
-
const actionsAtTheBottom = !largeLayout || layout === "side_panel" || shouldShowEntityActions === "bottom";
|
|
300
|
+
const actionsAtTheBottom = !largeLayout || layout === "side_panel" || selectedEntityView?.includeActions === "bottom";
|
|
576
301
|
|
|
577
|
-
const secondaryForms: React.ReactNode[] | undefined = customViews && resolvedEntityViews
|
|
302
|
+
const secondaryForms: React.ReactNode[] | undefined = formContext && customViews && resolvedEntityViews
|
|
578
303
|
.filter(e => e.includeActions)
|
|
579
|
-
.map(
|
|
580
|
-
(customView
|
|
581
|
-
|
|
582
|
-
return null;
|
|
583
|
-
const Builder = customView.Builder;
|
|
584
|
-
if (!Builder) {
|
|
585
|
-
console.error("customView.Builder is not defined");
|
|
586
|
-
return null;
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
return <FormLayout
|
|
590
|
-
key={`custom_view_${customView.key}`}
|
|
591
|
-
className={selectedTab !== customView.key ? "hidden" : ""}
|
|
592
|
-
formex={formex}>
|
|
593
|
-
<ErrorBoundary>
|
|
594
|
-
{formContext && <Builder
|
|
595
|
-
collection={collection}
|
|
596
|
-
entity={usedEntity}
|
|
597
|
-
modifiedValues={formex.values ?? usedEntity?.values}
|
|
598
|
-
formContext={formContext}
|
|
599
|
-
/>}
|
|
600
|
-
</ErrorBoundary>
|
|
601
|
-
</FormLayout>
|
|
304
|
+
.map((customView) => {
|
|
305
|
+
if (!customView || !formContext)
|
|
306
|
+
return null;
|
|
602
307
|
|
|
308
|
+
if (!customView.Builder) {
|
|
309
|
+
console.error("customView.Builder is not defined");
|
|
310
|
+
return null;
|
|
603
311
|
}
|
|
604
|
-
|
|
312
|
+
|
|
313
|
+
return <SecondaryForm key={`custom_view_${customView.key}`}
|
|
314
|
+
className={selectedTab !== customView.key ? "hidden" : ""}
|
|
315
|
+
customView={customView}
|
|
316
|
+
formContext={formContext}
|
|
317
|
+
collection={collection}
|
|
318
|
+
forceActionsAtTheBottom={!largeLayout || layout === "side_panel" || customView.includeActions === "bottom"}
|
|
319
|
+
entity={usedEntity}/>;
|
|
320
|
+
|
|
321
|
+
}).filter(Boolean);
|
|
605
322
|
|
|
606
323
|
const customViewsView: React.ReactNode[] | undefined = customViews && resolvedEntityViews
|
|
607
324
|
.filter(e => !e.includeActions)
|
|
608
|
-
.map(
|
|
609
|
-
(customView
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
return null;
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
return <div
|
|
619
|
-
className={cls(defaultBorderMixin,
|
|
620
|
-
"relative flex-1 w-full h-full overflow-auto",
|
|
621
|
-
{
|
|
622
|
-
"hidden": selectedTab !== customView.key
|
|
623
|
-
})}
|
|
624
|
-
key={`custom_view_${customView.key}`}
|
|
625
|
-
role="tabpanel">
|
|
626
|
-
<ErrorBoundary>
|
|
627
|
-
{formContext && <Builder
|
|
628
|
-
collection={collection}
|
|
629
|
-
entity={usedEntity}
|
|
630
|
-
modifiedValues={formex.values ?? usedEntity?.values}
|
|
631
|
-
formContext={formContext}
|
|
632
|
-
/>}
|
|
633
|
-
</ErrorBoundary>
|
|
634
|
-
</div>;
|
|
325
|
+
.map((customView) => {
|
|
326
|
+
if (!customView)
|
|
327
|
+
return null;
|
|
328
|
+
const Builder = customView.Builder;
|
|
329
|
+
if (!Builder) {
|
|
330
|
+
console.error("customView.Builder is not defined");
|
|
331
|
+
return null;
|
|
635
332
|
}
|
|
636
|
-
).filter(Boolean);
|
|
637
|
-
|
|
638
|
-
const globalLoading = (dataLoading && !usedEntity) ||
|
|
639
|
-
((!usedEntity || readOnly === undefined) && (status === "existing" || status === "copy"));
|
|
640
|
-
|
|
641
|
-
const subCollectionsViews = subcollections && subcollections.map(
|
|
642
|
-
(subcollection, colIndex) => {
|
|
643
|
-
const subcollectionId = subcollection.id ?? subcollection.path;
|
|
644
|
-
const fullPath = usedEntity ? `${path}/${usedEntity?.id}/${removeInitialAndTrailingSlashes(subcollectionId)}` : undefined;
|
|
645
|
-
if (selectedTab !== subcollectionId) return null;
|
|
646
|
-
return (
|
|
647
|
-
<div
|
|
648
|
-
className={"relative flex-1 h-full overflow-auto w-full"}
|
|
649
|
-
key={`subcol_${subcollectionId}`}
|
|
650
|
-
role="tabpanel">
|
|
651
|
-
|
|
652
|
-
{globalLoading || saving && <CircularProgressCenter/>}
|
|
653
|
-
|
|
654
|
-
{!globalLoading &&
|
|
655
|
-
(usedEntity && fullPath
|
|
656
|
-
? <EntityCollectionView
|
|
657
|
-
fullPath={fullPath}
|
|
658
|
-
parentCollectionIds={[...parentCollectionIds, collection.id]}
|
|
659
|
-
isSubCollection={true}
|
|
660
|
-
updateUrl={false}
|
|
661
|
-
{...subcollection}
|
|
662
|
-
openEntityMode={layout}/>
|
|
663
|
-
: <div
|
|
664
|
-
className="flex items-center justify-center w-full h-full p-3">
|
|
665
|
-
<Typography variant={"label"}>
|
|
666
|
-
You need to save your entity before
|
|
667
|
-
adding additional collections
|
|
668
|
-
</Typography>
|
|
669
|
-
</div>)
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
</div>
|
|
673
|
-
);
|
|
674
|
-
}
|
|
675
|
-
).filter(Boolean);
|
|
676
333
|
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
334
|
+
return <div
|
|
335
|
+
className={cls(defaultBorderMixin,
|
|
336
|
+
"relative flex-1 w-full h-full overflow-auto",
|
|
337
|
+
{ "hidden": selectedTab !== customView.key }
|
|
338
|
+
)}
|
|
339
|
+
key={`custom_view_${customView.key}`}
|
|
340
|
+
role="tabpanel">
|
|
341
|
+
<ErrorBoundary>
|
|
342
|
+
{formContext && <Builder
|
|
343
|
+
collection={collection}
|
|
344
|
+
entity={usedEntity}
|
|
345
|
+
modifiedValues={formContext.formex.values ?? usedEntity?.values}
|
|
346
|
+
formContext={formContext}
|
|
347
|
+
/>}
|
|
348
|
+
</ErrorBoundary>
|
|
349
|
+
</div>;
|
|
350
|
+
}).filter(Boolean);
|
|
351
|
+
|
|
352
|
+
const globalLoading = dataLoading && !usedEntity;
|
|
353
|
+
|
|
354
|
+
const subCollectionsViews = subcollections && subcollections.map((subcollection) => {
|
|
355
|
+
const subcollectionId = subcollection.id ?? subcollection.path;
|
|
356
|
+
const fullPath = usedEntity ? `${path}/${usedEntity?.id}/${removeInitialAndTrailingSlashes(subcollectionId)}` : undefined;
|
|
357
|
+
if (selectedTab !== subcollectionId) return null;
|
|
358
|
+
return (
|
|
359
|
+
<div
|
|
360
|
+
className={"relative flex-1 h-full overflow-auto w-full"}
|
|
361
|
+
key={`subcol_${subcollectionId}`}
|
|
362
|
+
role="tabpanel">
|
|
363
|
+
|
|
364
|
+
{globalLoading && <CircularProgressCenter/>}
|
|
365
|
+
|
|
366
|
+
{!globalLoading &&
|
|
367
|
+
(usedEntity && fullPath
|
|
368
|
+
? <EntityCollectionView
|
|
369
|
+
fullPath={fullPath}
|
|
370
|
+
parentCollectionIds={[...parentCollectionIds, collection.id]}
|
|
371
|
+
isSubCollection={true}
|
|
372
|
+
updateUrl={false}
|
|
373
|
+
{...subcollection}
|
|
374
|
+
openEntityMode={layout}/>
|
|
375
|
+
: <div className="flex items-center justify-center w-full h-full p-3">
|
|
376
|
+
<Typography variant={"label"}>
|
|
377
|
+
You need to save your entity before
|
|
378
|
+
adding additional collections
|
|
379
|
+
</Typography>
|
|
380
|
+
</div>)
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
</div>
|
|
384
|
+
);
|
|
385
|
+
}).filter(Boolean);
|
|
680
386
|
|
|
681
387
|
const onSideTabClick = (value: string) => {
|
|
682
388
|
setSelectedTab(value);
|
|
@@ -690,355 +396,47 @@ export function EntityEditViewInner<M extends Record<string, any>>({
|
|
|
690
396
|
}
|
|
691
397
|
};
|
|
692
398
|
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
const actionProps: PluginFormActionProps = {
|
|
715
|
-
entityId,
|
|
716
|
-
path,
|
|
717
|
-
status,
|
|
718
|
-
collection: inputCollection,
|
|
719
|
-
context,
|
|
720
|
-
currentEntityId: entityId,
|
|
721
|
-
formContext,
|
|
722
|
-
layout
|
|
723
|
-
};
|
|
724
|
-
pluginActions.push(...plugins.map((plugin, i) => (
|
|
725
|
-
plugin.form?.Actions
|
|
726
|
-
? <plugin.form.Actions
|
|
727
|
-
key={`actions_${plugin.key}`} {...actionProps}/>
|
|
728
|
-
: null
|
|
729
|
-
)).filter(Boolean));
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
const titlePropertyKey = getEntityTitlePropertyKey(resolvedCollection, customizationController.propertyConfigs);
|
|
733
|
-
const title = formex.values && titlePropertyKey ? getValueInPath(formex.values, titlePropertyKey) : undefined;
|
|
734
|
-
|
|
735
|
-
const onIdUpdate = inputCollection.callbacks?.onIdUpdate;
|
|
736
|
-
|
|
737
|
-
const doOnIdUpdate = useCallback(async () => {
|
|
738
|
-
if (onIdUpdate && formex.values && (status === "new" || status === "copy")) {
|
|
739
|
-
setCustomIdLoading(true);
|
|
740
|
-
try {
|
|
741
|
-
const updatedId = await onIdUpdate({
|
|
742
|
-
collection: resolvedCollection,
|
|
743
|
-
path,
|
|
744
|
-
entityId,
|
|
745
|
-
values: formex.values,
|
|
746
|
-
context
|
|
747
|
-
});
|
|
748
|
-
setEntityId(updatedId);
|
|
749
|
-
} catch (e) {
|
|
750
|
-
onIdUpdateError?.(e);
|
|
751
|
-
console.error(e);
|
|
752
|
-
}
|
|
753
|
-
setCustomIdLoading(false);
|
|
754
|
-
}
|
|
755
|
-
}, [entityId, formex.values, status]);
|
|
756
|
-
|
|
757
|
-
useEffect(() => {
|
|
758
|
-
doOnIdUpdate();
|
|
759
|
-
}, [doOnIdUpdate]);
|
|
760
|
-
|
|
761
|
-
const [underlyingChanges, setUnderlyingChanges] = useState<Partial<EntityValues<M>>>({});
|
|
762
|
-
|
|
763
|
-
const uniqueFieldValidator: CustomFieldValidator = useCallback(({
|
|
764
|
-
name,
|
|
765
|
-
value,
|
|
766
|
-
property
|
|
767
|
-
}) => dataSource.checkUniqueField(path, name, value, entityId, collection),
|
|
768
|
-
[dataSource, path, entityId]);
|
|
769
|
-
|
|
770
|
-
const validationSchema = useMemo(() => entityId
|
|
771
|
-
? getYupEntitySchema(
|
|
772
|
-
entityId,
|
|
773
|
-
resolvedCollection.properties,
|
|
774
|
-
uniqueFieldValidator)
|
|
775
|
-
: undefined,
|
|
776
|
-
[entityId, resolvedCollection.properties, uniqueFieldValidator]);
|
|
777
|
-
|
|
778
|
-
const getActionsForEntity = useCallback(({
|
|
779
|
-
entity,
|
|
780
|
-
customEntityActions
|
|
781
|
-
}: {
|
|
782
|
-
entity?: Entity<M>,
|
|
783
|
-
customEntityActions?: EntityAction[]
|
|
784
|
-
}): EntityAction[] => {
|
|
785
|
-
const createEnabled = canCreateEntity(inputCollection, authController, path, null);
|
|
786
|
-
const deleteEnabled = entity ? canDeleteEntity(inputCollection, authController, path, entity) : true;
|
|
787
|
-
const actions: EntityAction[] = [];
|
|
788
|
-
if (createEnabled)
|
|
789
|
-
actions.push(copyEntityAction);
|
|
790
|
-
if (deleteEnabled)
|
|
791
|
-
actions.push(deleteEntityAction);
|
|
792
|
-
if (customEntityActions)
|
|
793
|
-
return mergeEntityActions(actions, customEntityActions);
|
|
794
|
-
return actions;
|
|
795
|
-
}, [authController, inputCollection, path]);
|
|
796
|
-
|
|
797
|
-
const deferredValues = useDeferredValue(formex.values);
|
|
798
|
-
const modified = formex.dirty;
|
|
799
|
-
|
|
800
|
-
useEffect(() => {
|
|
801
|
-
const key = (status === "new" || status === "copy") ? path + "#new" : path + "/" + entityId;
|
|
802
|
-
if (modified) {
|
|
803
|
-
saveEntityToCache(key, deferredValues);
|
|
804
|
-
}
|
|
805
|
-
}, [deferredValues, modified]);
|
|
806
|
-
|
|
807
|
-
useEffect(() => {
|
|
808
|
-
if (!autoSave) {
|
|
809
|
-
onValuesModified?.(modified);
|
|
810
|
-
}
|
|
811
|
-
}, [modified]);
|
|
812
|
-
|
|
813
|
-
useOnAutoSave(autoSave, formex, lastSavedValues, save);
|
|
814
|
-
|
|
815
|
-
useEffect(() => {
|
|
816
|
-
if (!autoSave && !formex.isSubmitting && underlyingChanges && entity) {
|
|
817
|
-
// we update the form fields from the Firestore data
|
|
818
|
-
// if they were not touched
|
|
819
|
-
Object.entries(underlyingChanges).forEach(([key, value]) => {
|
|
820
|
-
const formValue = formex.values[key];
|
|
821
|
-
if (!equal(value, formValue) && !formex.touched[key]) {
|
|
822
|
-
console.debug("Updated value from the datasource:", key, value);
|
|
823
|
-
formex.setFieldValue(key, value !== undefined ? value : null);
|
|
824
|
-
}
|
|
825
|
-
});
|
|
826
|
-
}
|
|
827
|
-
}, [formex.isSubmitting, autoSave, underlyingChanges, entity, formex.values, formex.touched, formex.setFieldValue]);
|
|
828
|
-
|
|
829
|
-
const formFieldKeys = getFormFieldKeys(resolvedCollection);
|
|
830
|
-
const resolvedProperties = formFieldKeys.map(key => resolvedCollection.properties[key]);
|
|
831
|
-
|
|
832
|
-
const formFields = (
|
|
833
|
-
<div className={"flex flex-wrap gap-x-4 w-full space-y-8"}>
|
|
834
|
-
{formFieldKeys
|
|
835
|
-
.map((key) => {
|
|
836
|
-
|
|
837
|
-
const property = resolvedCollection.properties[key];
|
|
838
|
-
if (property) {
|
|
839
|
-
|
|
840
|
-
const underlyingValueHasChanged: boolean =
|
|
841
|
-
!!underlyingChanges &&
|
|
842
|
-
Object.keys(underlyingChanges).includes(key) &&
|
|
843
|
-
formex.touched[key];
|
|
844
|
-
|
|
845
|
-
const disabled = (!autoSave && formex.isSubmitting) || isReadOnly(property) || Boolean(property.disabled);
|
|
846
|
-
const hidden = isHidden(property);
|
|
847
|
-
if (hidden) return null;
|
|
848
|
-
const widthPercentage = property.widthPercentage ?? 100;
|
|
849
|
-
const cmsFormFieldProps: PropertyFieldBindingProps<any, M> = {
|
|
850
|
-
propertyKey: key,
|
|
851
|
-
disabled,
|
|
852
|
-
property,
|
|
853
|
-
includeDescription: property.description || property.longDescription,
|
|
854
|
-
underlyingValueHasChanged: underlyingValueHasChanged && !autoSave,
|
|
855
|
-
context: formContext,
|
|
856
|
-
partOfArray: false,
|
|
857
|
-
minimalistView: false,
|
|
858
|
-
autoFocus: false
|
|
859
|
-
};
|
|
860
|
-
|
|
861
|
-
return (
|
|
862
|
-
<div id={`form_field_${key}`}
|
|
863
|
-
className={"relative"}
|
|
864
|
-
style={{ width: widthPercentage === 100 ? "100%" : `calc(${widthPercentage}% - 8px)` }}
|
|
865
|
-
key={`field_${resolvedCollection.name}_${key}`}>
|
|
866
|
-
<ErrorBoundary>
|
|
867
|
-
<PropertyFieldBinding {...cmsFormFieldProps}/>
|
|
868
|
-
</ErrorBoundary>
|
|
869
|
-
</div>
|
|
870
|
-
);
|
|
871
|
-
}
|
|
872
|
-
|
|
873
|
-
const additionalField = resolvedCollection.additionalFields?.find(f => f.key === key);
|
|
874
|
-
if (additionalField && entity) {
|
|
875
|
-
const Builder = additionalField.Builder;
|
|
876
|
-
if (!Builder && !additionalField.value) {
|
|
877
|
-
throw new Error("When using additional fields you need to provide a Builder or a value");
|
|
878
|
-
}
|
|
879
|
-
|
|
880
|
-
const child = Builder
|
|
881
|
-
? <Builder entity={entity} context={context}/>
|
|
882
|
-
: <div className={"w-full"}>
|
|
883
|
-
{additionalField.value?.({
|
|
884
|
-
entity,
|
|
885
|
-
context
|
|
886
|
-
})?.toString()}
|
|
887
|
-
</div>;
|
|
888
|
-
|
|
889
|
-
return (
|
|
890
|
-
<div key={`additional_${key}`} className={"w-full"}>
|
|
891
|
-
<LabelWithIconAndTooltip
|
|
892
|
-
propertyKey={key}
|
|
893
|
-
icon={<NotesIcon size={"small"}/>}
|
|
894
|
-
title={additionalField.name}
|
|
895
|
-
className={"text-text-secondary dark:text-text-secondary-dark ml-3.5"}/>
|
|
896
|
-
<div
|
|
897
|
-
className={cls(paperMixin, "w-full min-h-14 p-4 md:p-6 overflow-x-scroll no-scrollbar")}>
|
|
898
|
-
|
|
899
|
-
<ErrorBoundary>
|
|
900
|
-
{child}
|
|
901
|
-
</ErrorBoundary>
|
|
902
|
-
|
|
903
|
-
</div>
|
|
904
|
-
</div>
|
|
905
|
-
);
|
|
906
|
-
}
|
|
907
|
-
|
|
908
|
-
console.warn(`Property ${key} not found in collection ${resolvedCollection.name} in properties or additional fields. Skipping.`);
|
|
909
|
-
return null;
|
|
910
|
-
})
|
|
911
|
-
.filter(Boolean)}
|
|
912
|
-
|
|
913
|
-
</div>);
|
|
914
|
-
|
|
915
|
-
const disabled = formex.isSubmitting || (!modified && status === "existing");
|
|
916
|
-
const formRef = React.useRef<HTMLDivElement>(null);
|
|
917
|
-
|
|
918
|
-
const entityActions = getActionsForEntity({
|
|
919
|
-
entity,
|
|
920
|
-
customEntityActions: inputCollection.entityActions
|
|
921
|
-
});
|
|
922
|
-
const formActions = entityActions.filter(a => a.includeInForm === undefined || a.includeInForm);
|
|
923
|
-
|
|
924
|
-
const dialogActions = actionsAtTheBottom
|
|
925
|
-
? buildBottomActions({
|
|
926
|
-
savingError: savingError,
|
|
927
|
-
entity: entity,
|
|
928
|
-
formActions: formActions,
|
|
929
|
-
resolvedCollection: resolvedCollection,
|
|
930
|
-
context: context,
|
|
931
|
-
sideEntityController: sideEntityController,
|
|
932
|
-
isSubmitting: formex.isSubmitting,
|
|
933
|
-
disabled: disabled,
|
|
934
|
-
status: status,
|
|
935
|
-
setPendingClose: (value: boolean) => {
|
|
936
|
-
closeAfterSaveRef.current = value;
|
|
937
|
-
},
|
|
938
|
-
pluginActions,
|
|
939
|
-
layout
|
|
940
|
-
})
|
|
941
|
-
: buildSideActions({
|
|
942
|
-
savingError: savingError,
|
|
943
|
-
entity: entity,
|
|
944
|
-
formActions: formActions,
|
|
945
|
-
resolvedCollection: resolvedCollection,
|
|
946
|
-
context: context,
|
|
947
|
-
sideEntityController: sideEntityController,
|
|
948
|
-
isSubmitting: formex.isSubmitting,
|
|
949
|
-
disabled: disabled,
|
|
950
|
-
status: status,
|
|
951
|
-
pluginActions,
|
|
952
|
-
layout
|
|
953
|
-
});
|
|
954
|
-
|
|
955
|
-
const entityView = (readOnly === undefined)
|
|
956
|
-
? <></>
|
|
957
|
-
: (!readOnly
|
|
958
|
-
? (
|
|
959
|
-
<ErrorBoundary>
|
|
960
|
-
<>
|
|
961
|
-
<div
|
|
962
|
-
className={"w-full py-2 flex flex-col items-start mt-4 lg:mt-8 mb-8"}>
|
|
963
|
-
|
|
964
|
-
<Typography
|
|
965
|
-
className={"mt-4 flex-grow line-clamp-1 " + inputCollection.hideIdFromForm ? "mb-2" : "mb-0"}
|
|
966
|
-
variant={"h4"}>{title ?? inputCollection.singularName ?? inputCollection.name}
|
|
967
|
-
</Typography>
|
|
968
|
-
<Alert color={"base"} className={"w-full"} size={"small"}>
|
|
969
|
-
<code
|
|
970
|
-
className={"text-xs select-all text-text-secondary dark:text-text-secondary-dark"}>{entity?.path ?? path}/{entityId}</code>
|
|
971
|
-
</Alert>
|
|
972
|
-
</div>
|
|
973
|
-
|
|
974
|
-
{!collection.hideIdFromForm &&
|
|
975
|
-
<CustomIdField customId={inputCollection.customId}
|
|
976
|
-
entityId={entityId}
|
|
977
|
-
status={status}
|
|
978
|
-
onChange={setEntityId}
|
|
979
|
-
error={entityIdError}
|
|
980
|
-
loading={customIdLoading}
|
|
981
|
-
entity={entity}/>}
|
|
982
|
-
|
|
983
|
-
{entityId && formContext && <>
|
|
984
|
-
<div className="mt-12 flex flex-col gap-8"
|
|
985
|
-
ref={formRef}>
|
|
986
|
-
|
|
987
|
-
{formFields}
|
|
988
|
-
|
|
989
|
-
<ErrorFocus containerRef={formRef}/>
|
|
990
|
-
|
|
991
|
-
</div>
|
|
992
|
-
</>}
|
|
993
|
-
|
|
994
|
-
{actionsAtTheBottom && <div className="h-16"/>}
|
|
399
|
+
// Render the main entity form view (or a read-only view if the user cannot edit)
|
|
400
|
+
const entityView = <EntityForm<M>
|
|
401
|
+
collection={collection}
|
|
402
|
+
path={path}
|
|
403
|
+
entityId={entityId ?? usedEntity?.id}
|
|
404
|
+
onValuesModified={onValuesModified}
|
|
405
|
+
onSaved={onSaved ? (params) => onSaved({
|
|
406
|
+
...params,
|
|
407
|
+
selectedTab: MAIN_TAB_VALUE === selectedTab ? undefined : selectedTab,
|
|
408
|
+
}) : undefined}
|
|
409
|
+
onClose={onClose}
|
|
410
|
+
entity={entity}
|
|
411
|
+
cachedDirtyValues={cachedDirtyValues}
|
|
412
|
+
openEntityMode={layout}
|
|
413
|
+
onFormContextReady={setFormContext}
|
|
414
|
+
forceActionsAtTheBottom={actionsAtTheBottom}
|
|
415
|
+
initialStatus={status}
|
|
416
|
+
onStatusChange={setStatus}
|
|
417
|
+
className={!mainViewVisible ? "hidden" : ""}
|
|
418
|
+
onEntityChange={setUsedEntity}
|
|
419
|
+
/>;
|
|
995
420
|
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
variant={"h4"}>{collection.singularName ?? collection.name}
|
|
1004
|
-
</Typography>
|
|
1005
|
-
<EntityView
|
|
1006
|
-
className={"px-8"}
|
|
1007
|
-
entity={usedEntity as Entity<M>}
|
|
1008
|
-
path={path}
|
|
1009
|
-
collection={collection}/>
|
|
1010
|
-
|
|
1011
|
-
</div>
|
|
1012
|
-
));
|
|
1013
|
-
|
|
1014
|
-
const subcollectionTabs = subcollections && subcollections.map(
|
|
1015
|
-
(subcollection) =>
|
|
1016
|
-
<Tab
|
|
1017
|
-
className="text-sm min-w-[120px]"
|
|
1018
|
-
value={subcollection.id}
|
|
1019
|
-
key={`entity_detail_collection_tab_${subcollection.name}`}>
|
|
1020
|
-
{subcollection.name}
|
|
1021
|
-
</Tab>
|
|
421
|
+
const subcollectionTabs = subcollections && subcollections.map((subcollection) =>
|
|
422
|
+
<Tab
|
|
423
|
+
className="text-sm min-w-[120px]"
|
|
424
|
+
value={subcollection.id}
|
|
425
|
+
key={`entity_detail_collection_tab_${subcollection.name}`}>
|
|
426
|
+
{subcollection.name}
|
|
427
|
+
</Tab>
|
|
1022
428
|
);
|
|
1023
429
|
|
|
1024
|
-
const customViewTabs = resolvedEntityViews.map(
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
{view.name}
|
|
1032
|
-
</Tab>
|
|
430
|
+
const customViewTabs = resolvedEntityViews.map((view) =>
|
|
431
|
+
<Tab
|
|
432
|
+
className="text-sm min-w-[120px]"
|
|
433
|
+
value={view.key}
|
|
434
|
+
key={`entity_detail_collection_tab_${view.name}`}>
|
|
435
|
+
{view.name}
|
|
436
|
+
</Tab>
|
|
1033
437
|
);
|
|
1034
438
|
|
|
1035
|
-
useEffect(() => {
|
|
1036
|
-
if (entityId && onIdChange)
|
|
1037
|
-
onIdChange(entityId);
|
|
1038
|
-
}, [entityId, onIdChange]);
|
|
1039
|
-
|
|
1040
439
|
const shouldShowTopBar = Boolean(barActions) || hasAdditionalViews;
|
|
1041
|
-
const shouldIncludeForm = selectedTab === MAIN_TAB_VALUE || selectedEntityView?.includeActions;
|
|
1042
440
|
|
|
1043
441
|
let result = <div className="relative flex flex-col h-full w-full bg-white dark:bg-surface-900">
|
|
1044
442
|
|
|
@@ -1049,8 +447,7 @@ export function EntityEditViewInner<M extends Record<string, any>>({
|
|
|
1049
447
|
|
|
1050
448
|
<div className={"flex-grow"}/>
|
|
1051
449
|
|
|
1052
|
-
{globalLoading && <div
|
|
1053
|
-
className="self-center">
|
|
450
|
+
{globalLoading && <div className="self-center">
|
|
1054
451
|
<CircularProgress size={"small"}/>
|
|
1055
452
|
</div>}
|
|
1056
453
|
|
|
@@ -1063,48 +460,23 @@ export function EntityEditViewInner<M extends Record<string, any>>({
|
|
|
1063
460
|
<Tab
|
|
1064
461
|
disabled={!hasAdditionalViews}
|
|
1065
462
|
value={MAIN_TAB_VALUE}
|
|
1066
|
-
className={"text-sm min-w-[120px]"}
|
|
1067
|
-
|
|
463
|
+
className={"text-sm min-w-[120px]"}>
|
|
464
|
+
{collection.singularName ?? collection.name}
|
|
465
|
+
</Tab>
|
|
1068
466
|
|
|
1069
467
|
{customViewTabs}
|
|
1070
468
|
|
|
1071
469
|
{subcollectionTabs}
|
|
1072
470
|
</Tabs>}
|
|
1073
|
-
|
|
1074
471
|
</div>}
|
|
1075
472
|
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
473
|
+
{globalLoading
|
|
474
|
+
? <div className="w-full pt-12 pb-16 px-4 sm:px-8 md:px-10">
|
|
475
|
+
<CircularProgressCenter/>
|
|
476
|
+
</div>
|
|
477
|
+
: entityView}
|
|
1079
478
|
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
formex.resetForm({
|
|
1083
|
-
values: getDataSourceEntityValues(initialResolvedCollection, status, entity) as M,
|
|
1084
|
-
});
|
|
1085
|
-
|
|
1086
|
-
return onDiscard();
|
|
1087
|
-
}}
|
|
1088
|
-
noValidate
|
|
1089
|
-
className={cls("flex-1 flex flex-row w-full overflow-y-auto justify-center",
|
|
1090
|
-
shouldIncludeForm ? "" : "hidden")}>
|
|
1091
|
-
|
|
1092
|
-
<FormLayout
|
|
1093
|
-
className={!mainViewVisible ? "hidden" : ""}
|
|
1094
|
-
id={`form_${path}`}
|
|
1095
|
-
formex={formex}>
|
|
1096
|
-
{globalLoading
|
|
1097
|
-
? <div className="w-full pt-12 pb-16 px-4 sm:px-8 md:px-10">
|
|
1098
|
-
<CircularProgressCenter/>
|
|
1099
|
-
</div>
|
|
1100
|
-
: entityView}
|
|
1101
|
-
</FormLayout>
|
|
1102
|
-
|
|
1103
|
-
{secondaryForms}
|
|
1104
|
-
|
|
1105
|
-
{shouldShowEntityActions && dialogActions}
|
|
1106
|
-
|
|
1107
|
-
</form>
|
|
479
|
+
{secondaryForms}
|
|
1108
480
|
|
|
1109
481
|
{customViewsView}
|
|
1110
482
|
|
|
@@ -1112,6 +484,8 @@ export function EntityEditViewInner<M extends Record<string, any>>({
|
|
|
1112
484
|
|
|
1113
485
|
</div>;
|
|
1114
486
|
|
|
487
|
+
const plugins = customizationController.plugins;
|
|
488
|
+
|
|
1115
489
|
if (plugins) {
|
|
1116
490
|
plugins.forEach((plugin: FireCMSPlugin) => {
|
|
1117
491
|
if (plugin.form?.provider) {
|
|
@@ -1120,7 +494,6 @@ export function EntityEditViewInner<M extends Record<string, any>>({
|
|
|
1120
494
|
status={status}
|
|
1121
495
|
path={path}
|
|
1122
496
|
collection={collection}
|
|
1123
|
-
onDiscard={onDiscard}
|
|
1124
497
|
entity={usedEntity}
|
|
1125
498
|
context={context}
|
|
1126
499
|
formContext={formContext}
|
|
@@ -1132,218 +505,6 @@ export function EntityEditViewInner<M extends Record<string, any>>({
|
|
|
1132
505
|
});
|
|
1133
506
|
}
|
|
1134
507
|
|
|
1135
|
-
return
|
|
1136
|
-
<Formex value={formex}>
|
|
1137
|
-
|
|
1138
|
-
{result}
|
|
1139
|
-
</Formex>
|
|
1140
|
-
);
|
|
1141
|
-
}
|
|
1142
|
-
|
|
1143
|
-
function getDataSourceEntityValues<M extends object>(collection: ResolvedEntityCollection,
|
|
1144
|
-
status: "new" | "existing" | "copy",
|
|
1145
|
-
entity: Entity<M> | undefined): Partial<EntityValues<M>> {
|
|
1146
|
-
|
|
1147
|
-
const properties = collection.properties;
|
|
1148
|
-
if ((status === "existing" || status === "copy") && entity) {
|
|
1149
|
-
return entity.values ?? getDefaultValuesFor(properties);
|
|
1150
|
-
} else if (status === "new") {
|
|
1151
|
-
return getDefaultValuesFor(properties);
|
|
1152
|
-
} else {
|
|
1153
|
-
console.error({
|
|
1154
|
-
status,
|
|
1155
|
-
entity
|
|
1156
|
-
});
|
|
1157
|
-
throw new Error("Form has not been initialised with the correct parameters");
|
|
1158
|
-
}
|
|
1159
|
-
}
|
|
1160
|
-
|
|
1161
|
-
export type EntityFormSaveParams<M extends Record<string, any>> = {
|
|
1162
|
-
collection: ResolvedEntityCollection<M>,
|
|
1163
|
-
path: string,
|
|
1164
|
-
entityId: string | undefined,
|
|
1165
|
-
values: EntityValues<M>,
|
|
1166
|
-
previousValues?: EntityValues<M>,
|
|
1167
|
-
closeAfterSave: boolean,
|
|
1168
|
-
autoSave: boolean
|
|
1169
|
-
};
|
|
1170
|
-
|
|
1171
|
-
export function yupToFormErrors(yupError: ValidationError): Record<string, any> {
|
|
1172
|
-
let errors: Record<string, any> = {};
|
|
1173
|
-
if (yupError.inner) {
|
|
1174
|
-
if (yupError.inner.length === 0) {
|
|
1175
|
-
return setIn(errors, yupError.path!, yupError.message);
|
|
1176
|
-
}
|
|
1177
|
-
for (const err of yupError.inner) {
|
|
1178
|
-
if (!getIn(errors, err.path!)) {
|
|
1179
|
-
errors = setIn(errors, err.path!, err.message);
|
|
1180
|
-
}
|
|
1181
|
-
}
|
|
1182
|
-
}
|
|
1183
|
-
return errors;
|
|
1184
|
-
}
|
|
1185
|
-
|
|
1186
|
-
type ActionsViewProps<M extends object> = {
|
|
1187
|
-
savingError: Error | undefined,
|
|
1188
|
-
entity: Entity<M> | undefined,
|
|
1189
|
-
formActions: EntityAction[],
|
|
1190
|
-
resolvedCollection: ResolvedEntityCollection,
|
|
1191
|
-
context: FireCMSContext,
|
|
1192
|
-
sideEntityController: SideEntityController,
|
|
1193
|
-
isSubmitting: boolean,
|
|
1194
|
-
disabled: boolean,
|
|
1195
|
-
status: "new" | "existing" | "copy",
|
|
1196
|
-
setPendingClose?: (value: boolean) => void,
|
|
1197
|
-
pluginActions?: React.ReactNode[],
|
|
1198
|
-
layout: "side_panel" | "full_screen";
|
|
1199
|
-
};
|
|
1200
|
-
|
|
1201
|
-
function buildBottomActions<M extends object>({
|
|
1202
|
-
savingError,
|
|
1203
|
-
entity,
|
|
1204
|
-
formActions,
|
|
1205
|
-
resolvedCollection,
|
|
1206
|
-
context,
|
|
1207
|
-
sideEntityController,
|
|
1208
|
-
isSubmitting,
|
|
1209
|
-
disabled,
|
|
1210
|
-
status,
|
|
1211
|
-
setPendingClose,
|
|
1212
|
-
pluginActions,
|
|
1213
|
-
layout
|
|
1214
|
-
}: ActionsViewProps<M>) {
|
|
1215
|
-
|
|
1216
|
-
const canClose = layout === "side_panel";
|
|
1217
|
-
return <DialogActions position={"absolute"}>
|
|
1218
|
-
|
|
1219
|
-
{savingError &&
|
|
1220
|
-
<div className="text-right">
|
|
1221
|
-
<Typography color={"error"}>
|
|
1222
|
-
{savingError.message}
|
|
1223
|
-
</Typography>
|
|
1224
|
-
</div>}
|
|
1225
|
-
|
|
1226
|
-
{entity && formActions.length > 0 && <div className="flex-grow flex overflow-auto no-scrollbar">
|
|
1227
|
-
{formActions.map(action => (
|
|
1228
|
-
<IconButton
|
|
1229
|
-
key={action.name}
|
|
1230
|
-
color="primary"
|
|
1231
|
-
onClick={(event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
|
1232
|
-
event.stopPropagation();
|
|
1233
|
-
if (entity)
|
|
1234
|
-
action.onClick({
|
|
1235
|
-
entity,
|
|
1236
|
-
fullPath: resolvedCollection.path,
|
|
1237
|
-
collection: resolvedCollection,
|
|
1238
|
-
context,
|
|
1239
|
-
sideEntityController,
|
|
1240
|
-
openEntityMode: layout
|
|
1241
|
-
});
|
|
1242
|
-
}}>
|
|
1243
|
-
{action.icon}
|
|
1244
|
-
</IconButton>
|
|
1245
|
-
))}
|
|
1246
|
-
</div>}
|
|
1247
|
-
|
|
1248
|
-
{pluginActions}
|
|
1249
|
-
|
|
1250
|
-
<Button
|
|
1251
|
-
variant="text"
|
|
1252
|
-
disabled={disabled || isSubmitting}
|
|
1253
|
-
type="reset">
|
|
1254
|
-
{status === "existing" ? "Discard" : "Clear"}
|
|
1255
|
-
</Button>
|
|
1256
|
-
|
|
1257
|
-
<Button
|
|
1258
|
-
variant={canClose ? "text" : "filled"}
|
|
1259
|
-
color="primary"
|
|
1260
|
-
type="submit"
|
|
1261
|
-
disabled={disabled || isSubmitting}
|
|
1262
|
-
onClick={() => {
|
|
1263
|
-
setPendingClose?.(false);
|
|
1264
|
-
}}>
|
|
1265
|
-
{status === "existing" && "Save"}
|
|
1266
|
-
{status === "copy" && "Create copy"}
|
|
1267
|
-
{status === "new" && "Create"}
|
|
1268
|
-
</Button>
|
|
1269
|
-
|
|
1270
|
-
{canClose && <LoadingButton
|
|
1271
|
-
variant="filled"
|
|
1272
|
-
color="primary"
|
|
1273
|
-
type="submit"
|
|
1274
|
-
loading={isSubmitting}
|
|
1275
|
-
disabled={disabled}
|
|
1276
|
-
onClick={() => {
|
|
1277
|
-
setPendingClose?.(true);
|
|
1278
|
-
}}>
|
|
1279
|
-
{status === "existing" && "Save and close"}
|
|
1280
|
-
{status === "copy" && "Create copy and close"}
|
|
1281
|
-
{status === "new" && "Create and close"}
|
|
1282
|
-
</LoadingButton>}
|
|
1283
|
-
|
|
1284
|
-
</DialogActions>;
|
|
1285
|
-
}
|
|
1286
|
-
|
|
1287
|
-
function buildSideActions<M extends object>({
|
|
1288
|
-
savingError,
|
|
1289
|
-
entity,
|
|
1290
|
-
formActions,
|
|
1291
|
-
resolvedCollection,
|
|
1292
|
-
context,
|
|
1293
|
-
sideEntityController,
|
|
1294
|
-
isSubmitting,
|
|
1295
|
-
disabled,
|
|
1296
|
-
status,
|
|
1297
|
-
setPendingClose,
|
|
1298
|
-
pluginActions
|
|
1299
|
-
}: ActionsViewProps<M>) {
|
|
1300
|
-
|
|
1301
|
-
return <div
|
|
1302
|
-
className={cls("overflow-auto h-full flex flex-col gap-2 w-80 2xl:w-96 px-4 py-16 sticky top-0 border-l", defaultBorderMixin)}>
|
|
1303
|
-
|
|
1304
|
-
<LoadingButton
|
|
1305
|
-
fullWidth={true}
|
|
1306
|
-
variant="filled"
|
|
1307
|
-
color="primary"
|
|
1308
|
-
type="submit"
|
|
1309
|
-
size={"large"}
|
|
1310
|
-
disabled={disabled || isSubmitting}
|
|
1311
|
-
onClick={() => {
|
|
1312
|
-
setPendingClose?.(false);
|
|
1313
|
-
}}>
|
|
1314
|
-
{status === "existing" && "Save"}
|
|
1315
|
-
{status === "copy" && "Create copy"}
|
|
1316
|
-
{status === "new" && "Create"}
|
|
1317
|
-
</LoadingButton>
|
|
1318
|
-
|
|
1319
|
-
<Button
|
|
1320
|
-
fullWidth={true}
|
|
1321
|
-
variant="text"
|
|
1322
|
-
disabled={disabled || isSubmitting}
|
|
1323
|
-
type="reset">
|
|
1324
|
-
{status === "existing" ? "Discard" : "Clear"}
|
|
1325
|
-
</Button>
|
|
1326
|
-
|
|
1327
|
-
{pluginActions}
|
|
1328
|
-
|
|
1329
|
-
{savingError &&
|
|
1330
|
-
<div className="text-right">
|
|
1331
|
-
<Typography color={"error"}>
|
|
1332
|
-
{savingError.message}
|
|
1333
|
-
</Typography>
|
|
1334
|
-
</div>}
|
|
1335
|
-
|
|
1336
|
-
</div>;
|
|
1337
|
-
}
|
|
1338
|
-
|
|
1339
|
-
function useOnAutoSave(autoSave: undefined | boolean, formex: FormexController<any>, lastSavedValues: any, save: (values: EntityValues<any>) => Promise<void>) {
|
|
1340
|
-
if (!autoSave) return;
|
|
1341
|
-
useEffect(() => {
|
|
1342
|
-
if (autoSave) {
|
|
1343
|
-
if (formex.values && !equal(formex.values, lastSavedValues.current)) {
|
|
1344
|
-
save(formex.values);
|
|
1345
|
-
}
|
|
1346
|
-
}
|
|
1347
|
-
}, [formex.values]);
|
|
508
|
+
return result;
|
|
1348
509
|
}
|
|
1349
510
|
|