@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.
@@ -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 { CustomFieldValidator, getYupEntitySchema } from "../form/validation";
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 ? getEntityFromCache(props.path + "/" + entityId) : getEntityFromCache(props.path + "#new");
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
- if (dataLoading && !cachedValues) {
147
- return <CircularProgressCenter/>
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
- return <div
175
- role="tabpanel"
176
- id={id}
177
- className={cls("relative flex flex-row max-w-4xl lg:max-w-3xl xl:max-w-4xl 2xl:max-w-6xl w-full h-fit", className)}>
178
-
179
- <div className={cls("flex flex-col w-full pt-12 pb-16 px-4 sm:px-8 md:px-10")}>
180
-
181
- {formex.dirty
182
- ? <Tooltip title={"Unsaved changes"}
183
- className={"self-end sticky top-4 z-10"}>
184
- <Chip size={"small"} colorScheme={"orangeDarker"}>
185
- <EditIcon size={"smallest"}/>
186
- </Chip>
187
- </Tooltip>
188
- : <Tooltip title={"In sync with the database"}
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
- </div>
208
+ if (!customView.Builder) {
209
+ console.error("customView.Builder is not defined");
210
+ return null;
211
+ }
198
212
 
199
- </div>
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: entityIdProp,
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 closeAfterSaveRef = useRef(false);
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
- const initialEntityId: string | undefined = useMemo((): string | undefined => {
273
- if (status === "new" || status === "copy") {
274
- if (mustSetCustomId) {
275
- return undefined;
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
- const [entityId, setEntityId] = React.useState<string | undefined>(initialEntityId);
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 [entityIdError, setEntityIdError] = React.useState<boolean>(false);
287
- const [savingError, setSavingError] = React.useState<Error | undefined>();
266
+ const largeLayout = useLargeLayout();
288
267
 
289
- const [customIdLoading, setCustomIdLoading] = React.useState<boolean>(false);
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 shouldShowEntityActions = !readOnly && (selectedTab === MAIN_TAB_VALUE || selectedEntityView?.includeActions);
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, colIndex) => {
581
- if (!customView)
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
- ).filter(Boolean);
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, colIndex) => {
610
- if (!customView)
611
- return null;
612
- const Builder = customView.Builder;
613
- if (!Builder) {
614
- console.error("customView.Builder is not defined");
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
- const onDiscard = useCallback(() => {
678
- onValuesModified?.(false);
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
- const onIdUpdateError = useCallback((error: any) => {
694
- snackbarController.open({
695
- type: "error",
696
- message: "Error updating id, check the console"
697
- });
698
- }, []);
699
-
700
- const onIdChange = useCallback((id: string) => {
701
- setUsedEntity((prevEntity) => prevEntity
702
- ? {
703
- ...prevEntity,
704
- id
705
- }
706
- : undefined);
707
- }, []);
708
-
709
- const pluginActions: React.ReactNode[] = [];
710
-
711
- const plugins = customizationController.plugins;
712
-
713
- if (plugins && inputCollection) {
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
- </ErrorBoundary>
998
- )
999
- : (
1000
- <div className={"flex flex-col"}>
1001
- <Typography
1002
- className={"mt-16 mb-8 mx-8"}
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
- (view) =>
1026
-
1027
- <Tab
1028
- className="text-sm min-w-[120px]"
1029
- value={view.key}
1030
- key={`entity_detail_collection_tab_${view.name}`}>
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
- >{collection.singularName ?? collection.name}</Tab>
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
- <form
1077
- onSubmit={formex.handleSubmit}
1078
- onReset={() => {
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
- clearDirtyCache();
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