@firecms/core 3.0.0-rc.1 → 3.0.0-rc.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/HomePage/HomePageDnD.d.ts +2 -1
- package/dist/components/PropertyCollectionView.d.ts +23 -0
- package/dist/components/UserDisplay.d.ts +7 -0
- package/dist/components/VirtualTable/fields/VirtualTableUserSelect.d.ts +12 -0
- package/dist/contexts/InternalUserManagementContext.d.ts +3 -0
- package/dist/core/EntityEditView.d.ts +10 -4
- package/dist/core/FireCMS.d.ts +0 -1
- package/dist/core/field_configs.d.ts +1 -1
- package/dist/form/EntityForm.d.ts +5 -2
- package/dist/form/components/LocalChangesMenu.d.ts +11 -0
- package/dist/form/field_bindings/UserSelectFieldBinding.d.ts +12 -0
- package/dist/form/index.d.ts +2 -1
- package/dist/hooks/index.d.ts +2 -0
- package/dist/hooks/useCollapsedGroups.d.ts +9 -0
- package/dist/hooks/useInternalUserManagementController.d.ts +12 -0
- package/dist/index.es.js +1983 -650
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +1981 -648
- package/dist/index.umd.js.map +1 -1
- package/dist/preview/components/UserPreview.d.ts +8 -0
- package/dist/preview/index.d.ts +1 -0
- package/dist/types/collections.d.ts +13 -0
- package/dist/types/entities.d.ts +5 -1
- package/dist/types/firecms.d.ts +15 -0
- package/dist/types/firecms_context.d.ts +16 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/internal_user_management.d.ts +20 -0
- package/dist/types/plugins.d.ts +2 -0
- package/dist/types/properties.d.ts +41 -6
- package/dist/types/property_config.d.ts +1 -1
- package/dist/types/user.d.ts +1 -1
- package/dist/util/collections.d.ts +1 -0
- package/dist/util/entity_cache.d.ts +6 -1
- package/dist/util/make_properties_editable.d.ts +1 -2
- package/dist/util/objects.d.ts +1 -0
- package/dist/util/useStorageUploadController.d.ts +1 -0
- package/package.json +6 -6
- package/src/components/EntityCollectionTable/EntityCollectionRowActions.tsx +47 -47
- package/src/components/EntityCollectionTable/PropertyTableCell.tsx +12 -0
- package/src/components/EntityCollectionView/EntityCollectionView.tsx +6 -1
- package/src/components/EntityView.tsx +29 -40
- package/src/components/ErrorView.tsx +1 -1
- package/src/components/HomePage/DefaultHomePage.tsx +21 -34
- package/src/components/HomePage/HomePageDnD.tsx +143 -83
- package/src/components/HomePage/RenameGroupDialog.tsx +9 -3
- package/src/components/PropertyCollectionView.tsx +329 -0
- package/src/components/PropertyConfigBadge.tsx +2 -2
- package/src/components/SelectableTable/filters/DateTimeFilterField.tsx +2 -1
- package/src/components/SelectableTable/filters/StringNumberFilterField.tsx +1 -2
- package/src/components/UserDisplay.tsx +55 -0
- package/src/components/VirtualTable/fields/VirtualTableUserSelect.tsx +99 -0
- package/src/components/common/useColumnsIds.tsx +1 -8
- package/src/contexts/InternalUserManagementContext.tsx +4 -0
- package/src/core/EntityEditView.tsx +27 -14
- package/src/core/EntityEditViewFormActions.tsx +33 -18
- package/src/core/EntitySidePanel.tsx +9 -3
- package/src/core/FireCMS.tsx +22 -13
- package/src/core/field_configs.tsx +15 -1
- package/src/form/EntityForm.tsx +173 -42
- package/src/form/EntityFormActions.tsx +30 -15
- package/src/form/PropertyFieldBinding.tsx +4 -0
- package/src/form/components/ErrorFocus.tsx +22 -29
- package/src/form/components/LocalChangesMenu.tsx +144 -0
- package/src/form/field_bindings/UserSelectFieldBinding.tsx +94 -0
- package/src/form/index.tsx +5 -1
- package/src/hooks/index.tsx +3 -0
- package/src/hooks/useBrowserTitleAndIcon.tsx +1 -1
- package/src/hooks/useBuildNavigationController.tsx +104 -31
- package/src/hooks/useCollapsedGroups.ts +64 -0
- package/src/hooks/useFireCMSContext.tsx +6 -2
- package/src/hooks/useInternalUserManagementController.tsx +16 -0
- package/src/preview/PropertyPreview.tsx +8 -0
- package/src/preview/components/ReferencePreview.tsx +4 -2
- package/src/preview/components/UserPreview.tsx +27 -0
- package/src/preview/index.ts +1 -0
- package/src/preview/property_previews/ArrayPropertyPreview.tsx +1 -1
- package/src/preview/property_previews/MapPropertyPreview.tsx +2 -2
- package/src/preview/property_previews/NumberPropertyPreview.tsx +2 -2
- package/src/types/collections.ts +14 -0
- package/src/types/entities.ts +7 -1
- package/src/types/firecms.tsx +16 -0
- package/src/types/firecms_context.tsx +17 -0
- package/src/types/index.ts +1 -0
- package/src/types/internal_user_management.ts +24 -0
- package/src/types/plugins.tsx +3 -0
- package/src/types/properties.ts +45 -6
- package/src/types/property_config.tsx +1 -0
- package/src/types/user.ts +1 -1
- package/src/util/collections.ts +8 -0
- package/src/util/createFormexStub.tsx +4 -0
- package/src/util/entities.ts +1 -1
- package/src/util/entity_cache.ts +72 -53
- package/src/util/join_collections.ts +3 -3
- package/src/util/make_properties_editable.ts +0 -22
- package/src/util/objects.ts +40 -2
- package/src/util/useStorageUploadController.tsx +71 -34
package/src/form/EntityForm.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useCallback,
|
|
1
|
+
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
2
|
import {
|
|
3
3
|
AuthController,
|
|
4
4
|
CMSAnalyticsEvent,
|
|
@@ -19,8 +19,10 @@ import { ErrorBoundary, getFormFieldKeys } from "../components";
|
|
|
19
19
|
import {
|
|
20
20
|
getDefaultValuesFor,
|
|
21
21
|
getEntityTitlePropertyKey,
|
|
22
|
+
getLocalChangesBackup,
|
|
22
23
|
getValueInPath,
|
|
23
24
|
isHidden,
|
|
25
|
+
isObject,
|
|
24
26
|
isReadOnly,
|
|
25
27
|
mergeDeep,
|
|
26
28
|
resolveCollection,
|
|
@@ -32,7 +34,8 @@ import {
|
|
|
32
34
|
useAuthController,
|
|
33
35
|
useCustomizationController,
|
|
34
36
|
useDataSource,
|
|
35
|
-
useFireCMSContext,
|
|
37
|
+
useFireCMSContext,
|
|
38
|
+
useNavigationController,
|
|
36
39
|
useSideEntityController,
|
|
37
40
|
useSnackbarController
|
|
38
41
|
} from "../hooks";
|
|
@@ -41,11 +44,18 @@ import { Formex, FormexController, getIn, setIn, useCreateFormex } from "@firecm
|
|
|
41
44
|
import { useAnalyticsController } from "../hooks/useAnalyticsController";
|
|
42
45
|
import { FormEntry, FormLayout, LabelWithIconAndTooltip, PropertyFieldBinding } from "../form";
|
|
43
46
|
import { ValidationError } from "yup";
|
|
44
|
-
import {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
47
|
+
import {
|
|
48
|
+
flattenKeys,
|
|
49
|
+
getEntityFromCache,
|
|
50
|
+
removeEntityFromCache,
|
|
51
|
+
removeEntityFromMemoryCache,
|
|
52
|
+
saveEntityToCache
|
|
53
|
+
} from "../util/entity_cache";
|
|
54
|
+
import { CustomIdField } from "./components/CustomIdField";
|
|
55
|
+
import { ErrorFocus } from "./components/ErrorFocus";
|
|
56
|
+
import { CustomFieldValidator, getYupEntitySchema } from "./validation";
|
|
48
57
|
import { EntityFormActions, EntityFormActionsProps } from "./EntityFormActions";
|
|
58
|
+
import { LocalChangesMenu } from "./components/LocalChangesMenu";
|
|
49
59
|
|
|
50
60
|
export type OnUpdateParams = {
|
|
51
61
|
entity: Entity<any>,
|
|
@@ -64,7 +74,7 @@ export type EntityFormProps<M extends Record<string, any>> = {
|
|
|
64
74
|
entity?: Entity<M>;
|
|
65
75
|
databaseId?: string;
|
|
66
76
|
onIdChange?: (id: string) => void;
|
|
67
|
-
onValuesModified?: (modified: boolean) => void;
|
|
77
|
+
onValuesModified?: (modified: boolean, values: M) => void;
|
|
68
78
|
onSaved?: (params: OnUpdateParams) => void;
|
|
69
79
|
initialDirtyValues?: Partial<M>; // dirty cached entity in memory
|
|
70
80
|
onFormContextReady?: (formContext: FormContext) => void;
|
|
@@ -96,6 +106,80 @@ export type EntityFormProps<M extends Record<string, any>> = {
|
|
|
96
106
|
children?: React.ReactNode;
|
|
97
107
|
};
|
|
98
108
|
|
|
109
|
+
// extract touched values for nested touched trees and map to current values
|
|
110
|
+
export function extractTouchedValues(values: any, touched: Record<string, boolean>): Record<string, any> {
|
|
111
|
+
let acc: Record<string, any> = {};
|
|
112
|
+
if (!touched || typeof touched !== "object") {
|
|
113
|
+
return acc;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
Object.entries(touched).forEach(([key, value]) => {
|
|
117
|
+
if (value) {
|
|
118
|
+
acc = setIn(acc, key, getIn(values, key));
|
|
119
|
+
}
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
return acc;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function getChanges<T extends object>(source: Partial<T>, comparison: Partial<T>): Partial<T> {
|
|
126
|
+
const changes: Partial<T> = {};
|
|
127
|
+
|
|
128
|
+
if (!source) {
|
|
129
|
+
return {};
|
|
130
|
+
}
|
|
131
|
+
if (!comparison) {
|
|
132
|
+
return source;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const allKeys = Array.from(new Set([...Object.keys(source), ...Object.keys(comparison)]));
|
|
136
|
+
|
|
137
|
+
for (const key of allKeys) {
|
|
138
|
+
const sourceValue = (source as any)[key];
|
|
139
|
+
const comparisonValue = (comparison as any)[key];
|
|
140
|
+
|
|
141
|
+
if (equal(sourceValue, comparisonValue)) {
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const sourceHasKey = source && typeof source === "object" && Object.prototype.hasOwnProperty.call(source, key);
|
|
146
|
+
const comparisonHasKey = comparison && typeof comparison === "object" && Object.prototype.hasOwnProperty.call(comparison, key);
|
|
147
|
+
|
|
148
|
+
if (comparisonHasKey && !sourceHasKey) {
|
|
149
|
+
(changes as any)[key] = undefined;
|
|
150
|
+
} else if (Array.isArray(sourceValue)) {
|
|
151
|
+
const comparisonArray = Array.isArray(comparisonValue) ? comparisonValue : [];
|
|
152
|
+
if (sourceValue.length < comparisonArray.length) {
|
|
153
|
+
(changes as any)[key] = sourceValue;
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
const changedArray = sourceValue.map((item, index) => {
|
|
157
|
+
const comparisonItem = comparisonArray[index];
|
|
158
|
+
if (equal(item, comparisonItem)) {
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
if (isObject(item) && item && isObject(comparisonItem) && comparisonItem) {
|
|
162
|
+
const nestedChanges = getChanges(item, comparisonItem);
|
|
163
|
+
return Object.keys(nestedChanges).length > 0 ? nestedChanges : item;
|
|
164
|
+
}
|
|
165
|
+
return item;
|
|
166
|
+
});
|
|
167
|
+
if (changedArray.some(item => item !== null) || sourceValue.length > comparisonArray.length) {
|
|
168
|
+
(changes as any)[key] = changedArray;
|
|
169
|
+
}
|
|
170
|
+
} else if (isObject(sourceValue) && sourceValue && isObject(comparisonValue) && comparisonValue) {
|
|
171
|
+
const nestedChanges = getChanges(sourceValue, comparisonValue);
|
|
172
|
+
if (Object.keys(nestedChanges).length > 0) {
|
|
173
|
+
(changes as any)[key] = nestedChanges;
|
|
174
|
+
}
|
|
175
|
+
} else {
|
|
176
|
+
(changes as any)[key] = sourceValue;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return changes;
|
|
181
|
+
}
|
|
182
|
+
|
|
99
183
|
export function EntityForm<M extends Record<string, any>>({
|
|
100
184
|
path,
|
|
101
185
|
fullIdPath,
|
|
@@ -126,7 +210,6 @@ export function EntityForm<M extends Record<string, any>>({
|
|
|
126
210
|
console.warn(`The collection ${collection.path} has customId and formAutoSave enabled. This is not supported and formAutoSave will be ignored`);
|
|
127
211
|
}
|
|
128
212
|
|
|
129
|
-
|
|
130
213
|
const sideEntityController = useSideEntityController();
|
|
131
214
|
const navigationController = useNavigationController();
|
|
132
215
|
|
|
@@ -164,7 +247,7 @@ export function EntityForm<M extends Record<string, any>>({
|
|
|
164
247
|
const context = useFireCMSContext();
|
|
165
248
|
const analyticsController = useAnalyticsController();
|
|
166
249
|
|
|
167
|
-
const [underlyingChanges
|
|
250
|
+
const [underlyingChanges] = useState<Partial<EntityValues<M>>>({});
|
|
168
251
|
|
|
169
252
|
const [customIdLoading, setCustomIdLoading] = useState<boolean>(false);
|
|
170
253
|
|
|
@@ -189,6 +272,18 @@ export function EntityForm<M extends Record<string, any>>({
|
|
|
189
272
|
|
|
190
273
|
const autoSave = collection.formAutoSave && !collection.customId;
|
|
191
274
|
|
|
275
|
+
const baseInitialValues = useMemo(() => getInitialEntityValues(authController, collection, path, status, entity, customizationController.propertyConfigs), [authController, collection, path, status, entity, customizationController.propertyConfigs]);
|
|
276
|
+
|
|
277
|
+
const localChangesDataRaw = useMemo(() => entityId
|
|
278
|
+
? getEntityFromCache(path + "/" + entityId)
|
|
279
|
+
: getEntityFromCache(path + "#new"), [entityId, path]);
|
|
280
|
+
|
|
281
|
+
const [localChangesCleared, setLocalChangesCleared] = useState<boolean>(false);
|
|
282
|
+
|
|
283
|
+
const localChangesBackup = getLocalChangesBackup(collection);
|
|
284
|
+
const autoApplyLocalChanges = localChangesBackup === "auto_apply";
|
|
285
|
+
const manualApplyLocalChanges = localChangesBackup === "manual_apply";
|
|
286
|
+
|
|
192
287
|
const onSubmit = (values: EntityValues<M>, formexController: FormexController<EntityValues<M>>) => {
|
|
193
288
|
|
|
194
289
|
if (mustSetCustomId && !entityId) {
|
|
@@ -226,13 +321,43 @@ export function EntityForm<M extends Record<string, any>>({
|
|
|
226
321
|
});
|
|
227
322
|
};
|
|
228
323
|
|
|
324
|
+
const [initialValues, initialDirty] = useMemo(() => {
|
|
325
|
+
const initialValuesWithLocalChanges: Partial<M> = autoApplyLocalChanges && localChangesDataRaw ? mergeDeep(baseInitialValues, localChangesDataRaw as Partial<M>) : baseInitialValues;
|
|
326
|
+
const initialValues = initialDirtyValues ? mergeDeep(initialValuesWithLocalChanges, initialDirtyValues) : initialValuesWithLocalChanges;
|
|
327
|
+
const initialDirty = Boolean(initialDirtyValues) && initialDirtyValues && Object.keys(initialDirtyValues).length > 0;
|
|
328
|
+
return [initialValues, initialDirty];
|
|
329
|
+
}, [autoApplyLocalChanges, localChangesDataRaw, baseInitialValues, initialDirtyValues]);
|
|
330
|
+
|
|
331
|
+
const localChangesData = useMemo(() => {
|
|
332
|
+
if (!localChangesDataRaw) {
|
|
333
|
+
return undefined;
|
|
334
|
+
}
|
|
335
|
+
return getChanges(localChangesDataRaw, initialValues);
|
|
336
|
+
}, [localChangesDataRaw, initialValues]);
|
|
337
|
+
|
|
338
|
+
const hasLocalChanges = !localChangesCleared && localChangesData && Object.keys(localChangesData).length > 0;
|
|
339
|
+
|
|
229
340
|
const formex: FormexController<M> = formexProp ?? useCreateFormex<M>({
|
|
230
|
-
initialValues:
|
|
231
|
-
initialDirty
|
|
341
|
+
initialValues: initialValues as M,
|
|
342
|
+
initialDirty,
|
|
343
|
+
initialTouched: initialDirtyValues ?
|
|
344
|
+
flattenKeys(initialDirtyValues!)
|
|
345
|
+
.reduce((previousValue, currentValue) => ({
|
|
346
|
+
...previousValue,
|
|
347
|
+
[currentValue]: true
|
|
348
|
+
}), {})
|
|
349
|
+
: {},
|
|
232
350
|
onSubmit,
|
|
233
351
|
onReset: () => {
|
|
234
352
|
clearDirtyCache();
|
|
235
|
-
onValuesModified?.(false);
|
|
353
|
+
onValuesModified?.(false, initialValues as M);
|
|
354
|
+
},
|
|
355
|
+
onValuesChangeDeferred: (values: M, controller: FormexController<M>) => {
|
|
356
|
+
const key = (status === "new" || status === "copy") ? path + "#new" : path + "/" + entityId;
|
|
357
|
+
if (controller.dirty) {
|
|
358
|
+
const touchedValues = extractTouchedValues(values, controller.touched);
|
|
359
|
+
saveEntityToCache(key, touchedValues);
|
|
360
|
+
}
|
|
236
361
|
},
|
|
237
362
|
validation: (values) => {
|
|
238
363
|
return validationSchema?.validate(values, { abortEarly: false })
|
|
@@ -295,8 +420,10 @@ export function EntityForm<M extends Record<string, any>>({
|
|
|
295
420
|
|
|
296
421
|
function clearDirtyCache() {
|
|
297
422
|
if (status === "new" || status === "copy") {
|
|
423
|
+
removeEntityFromMemoryCache(path + "#new");
|
|
298
424
|
removeEntityFromCache(path + "#new");
|
|
299
425
|
} else {
|
|
426
|
+
removeEntityFromMemoryCache(path + "/" + entityId);
|
|
300
427
|
removeEntityFromCache(path + "/" + entityId);
|
|
301
428
|
}
|
|
302
429
|
}
|
|
@@ -304,7 +431,7 @@ export function EntityForm<M extends Record<string, any>>({
|
|
|
304
431
|
const onSaveSuccess = (updatedEntity: Entity<M>) => {
|
|
305
432
|
|
|
306
433
|
clearDirtyCache();
|
|
307
|
-
onValuesModified?.(false);
|
|
434
|
+
onValuesModified?.(false, updatedEntity.values);
|
|
308
435
|
if (!autoSave)
|
|
309
436
|
snackbarController.open({
|
|
310
437
|
type: "success",
|
|
@@ -405,7 +532,7 @@ export function EntityForm<M extends Record<string, any>>({
|
|
|
405
532
|
values,
|
|
406
533
|
previousValues: entity?.values,
|
|
407
534
|
autoSave: autoSave ?? false
|
|
408
|
-
}).then((
|
|
535
|
+
}).then(() => {
|
|
409
536
|
const eventName: CMSAnalyticsEvent = status === "new"
|
|
410
537
|
? "new_entity_saved"
|
|
411
538
|
: (status === "copy" ? "entity_copied" : (status === "existing" ? "entity_edited" : "unmapped_event"));
|
|
@@ -443,7 +570,8 @@ export function EntityForm<M extends Record<string, any>>({
|
|
|
443
570
|
type: "error",
|
|
444
571
|
message: "Error updating id, check the console"
|
|
445
572
|
});
|
|
446
|
-
|
|
573
|
+
console.error(error);
|
|
574
|
+
}, [snackbarController]);
|
|
447
575
|
|
|
448
576
|
const pluginActions: React.ReactNode[] = [];
|
|
449
577
|
const plugins = customizationController.plugins;
|
|
@@ -503,17 +631,15 @@ export function EntityForm<M extends Record<string, any>>({
|
|
|
503
631
|
|
|
504
632
|
useEffect(() => {
|
|
505
633
|
if (!autoSave) {
|
|
506
|
-
onValuesModified?.(modified);
|
|
634
|
+
onValuesModified?.(modified, formex.values);
|
|
507
635
|
}
|
|
508
636
|
}, [formex.dirty]);
|
|
509
637
|
|
|
510
|
-
const deferredValues = useDeferredValue(formex.values);
|
|
511
638
|
const modified = formex.dirty;
|
|
512
639
|
|
|
513
640
|
const uniqueFieldValidator: CustomFieldValidator = useCallback(({
|
|
514
641
|
name,
|
|
515
|
-
value
|
|
516
|
-
property
|
|
642
|
+
value
|
|
517
643
|
}) => dataSource.checkUniqueField(path, name, value, entityId, collection),
|
|
518
644
|
[dataSource, path, entityId]);
|
|
519
645
|
|
|
@@ -525,13 +651,6 @@ export function EntityForm<M extends Record<string, any>>({
|
|
|
525
651
|
: undefined,
|
|
526
652
|
[entityId, resolvedCollection.properties, uniqueFieldValidator]);
|
|
527
653
|
|
|
528
|
-
useEffect(() => {
|
|
529
|
-
const key = (status === "new" || status === "copy") ? path + "#new" : path + "/" + entityId;
|
|
530
|
-
if (modified) {
|
|
531
|
-
saveEntityToCache(key, deferredValues);
|
|
532
|
-
}
|
|
533
|
-
}, [deferredValues, modified, path, entityId, status]);
|
|
534
|
-
|
|
535
654
|
useOnAutoSave(autoSave, formex, lastSavedValues, save);
|
|
536
655
|
|
|
537
656
|
useEffect(() => {
|
|
@@ -716,7 +835,7 @@ export function EntityForm<M extends Record<string, any>>({
|
|
|
716
835
|
<form
|
|
717
836
|
onSubmit={formex.handleSubmit}
|
|
718
837
|
onReset={() => formex.resetForm({
|
|
719
|
-
values:
|
|
838
|
+
values: baseInitialValues as M
|
|
720
839
|
})}
|
|
721
840
|
noValidate
|
|
722
841
|
className={cls("flex-1 flex flex-row w-full overflow-y-auto justify-center", className)}>
|
|
@@ -725,34 +844,46 @@ export function EntityForm<M extends Record<string, any>>({
|
|
|
725
844
|
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")}>
|
|
726
845
|
|
|
727
846
|
<div className={cls("flex flex-col w-full pt-12 pb-16 px-4 sm:px-8 md:px-10")}>
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
<
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
847
|
+
<div
|
|
848
|
+
className={"flex flex-row gap-4 self-end sticky top-4 z-10"}>
|
|
849
|
+
|
|
850
|
+
{manualApplyLocalChanges && hasLocalChanges &&
|
|
851
|
+
<LocalChangesMenu
|
|
852
|
+
cacheKey={status === "new" || status === "copy" ? path + "#new" : path + "/" + entityId}
|
|
853
|
+
properties={resolvedCollection.properties}
|
|
854
|
+
localChangesData={localChangesData as Partial<M>}
|
|
855
|
+
formex={formex}
|
|
856
|
+
onClearLocalChanges={() => setLocalChangesCleared(true)}
|
|
857
|
+
/>}
|
|
858
|
+
|
|
859
|
+
{formex.dirty
|
|
860
|
+
? <Tooltip title={"There are local unsaved changes"}>
|
|
861
|
+
<Chip size={"small"} className={"py-1"} colorScheme={"orangeDarker"}>
|
|
862
|
+
<EditIcon size={"smallest"}/>
|
|
863
|
+
</Chip>
|
|
864
|
+
</Tooltip>
|
|
865
|
+
: <Tooltip title={"The current form is in sync with the database"}>
|
|
866
|
+
<Chip size={"small"} className={"py-1"} >
|
|
867
|
+
<CheckIcon size={"smallest"}/>
|
|
868
|
+
</Chip>
|
|
869
|
+
</Tooltip>}
|
|
870
|
+
</div>
|
|
742
871
|
|
|
743
872
|
{formView}
|
|
744
873
|
|
|
745
874
|
</div>
|
|
746
875
|
|
|
747
876
|
</div>
|
|
877
|
+
|
|
748
878
|
{dialogActions}
|
|
879
|
+
|
|
749
880
|
</form>
|
|
750
881
|
|
|
751
882
|
</Formex>
|
|
752
883
|
);
|
|
753
884
|
}
|
|
754
885
|
|
|
755
|
-
function getInitialEntityValues<M extends object>(
|
|
886
|
+
export function getInitialEntityValues<M extends object>(
|
|
756
887
|
authController: AuthController,
|
|
757
888
|
collection: EntityCollection,
|
|
758
889
|
path: string,
|
|
@@ -7,7 +7,16 @@ import {
|
|
|
7
7
|
ResolvedEntityCollection,
|
|
8
8
|
SideEntityController
|
|
9
9
|
} from "../types";
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
Button,
|
|
12
|
+
cls,
|
|
13
|
+
defaultBorderMixin,
|
|
14
|
+
DialogActions,
|
|
15
|
+
ErrorIcon,
|
|
16
|
+
IconButton,
|
|
17
|
+
LoadingButton,
|
|
18
|
+
Typography
|
|
19
|
+
} from "@firecms/ui";
|
|
11
20
|
import { FormexController } from "@firecms/formex";
|
|
12
21
|
import { useFireCMSContext, useSideEntityController } from "../hooks";
|
|
13
22
|
|
|
@@ -57,13 +66,13 @@ export function EntityFormActions({
|
|
|
57
66
|
collection,
|
|
58
67
|
context,
|
|
59
68
|
sideEntityController,
|
|
60
|
-
isSubmitting: formex.isSubmitting,
|
|
61
69
|
disabled,
|
|
62
70
|
status,
|
|
63
71
|
pluginActions,
|
|
64
72
|
openEntityMode,
|
|
65
73
|
navigateBack,
|
|
66
|
-
formContext
|
|
74
|
+
formContext,
|
|
75
|
+
formex
|
|
67
76
|
})
|
|
68
77
|
: buildSideActions({
|
|
69
78
|
fullPath,
|
|
@@ -73,13 +82,13 @@ export function EntityFormActions({
|
|
|
73
82
|
collection,
|
|
74
83
|
context,
|
|
75
84
|
sideEntityController,
|
|
76
|
-
isSubmitting: formex.isSubmitting,
|
|
77
85
|
disabled,
|
|
78
86
|
status,
|
|
79
87
|
pluginActions,
|
|
80
88
|
openEntityMode,
|
|
81
89
|
navigateBack,
|
|
82
|
-
formContext
|
|
90
|
+
formContext,
|
|
91
|
+
formex
|
|
83
92
|
});
|
|
84
93
|
}
|
|
85
94
|
|
|
@@ -92,13 +101,13 @@ type ActionsViewProps<M extends object> = {
|
|
|
92
101
|
collection: ResolvedEntityCollection,
|
|
93
102
|
context: FireCMSContext,
|
|
94
103
|
sideEntityController: SideEntityController,
|
|
95
|
-
isSubmitting: boolean,
|
|
96
104
|
disabled: boolean,
|
|
97
105
|
status: "new" | "existing" | "copy",
|
|
98
106
|
pluginActions?: React.ReactNode[],
|
|
99
107
|
openEntityMode: "side_panel" | "full_screen";
|
|
100
108
|
navigateBack: () => void;
|
|
101
|
-
formContext: FormContext
|
|
109
|
+
formContext: FormContext,
|
|
110
|
+
formex: FormexController<any>;
|
|
102
111
|
};
|
|
103
112
|
|
|
104
113
|
function buildBottomActions<M extends object>({
|
|
@@ -110,15 +119,17 @@ function buildBottomActions<M extends object>({
|
|
|
110
119
|
collection,
|
|
111
120
|
context,
|
|
112
121
|
sideEntityController,
|
|
113
|
-
isSubmitting,
|
|
114
122
|
disabled,
|
|
115
123
|
status,
|
|
116
124
|
pluginActions,
|
|
117
125
|
openEntityMode,
|
|
118
126
|
navigateBack,
|
|
119
|
-
formContext
|
|
127
|
+
formContext,
|
|
128
|
+
formex
|
|
120
129
|
}: ActionsViewProps<M>) {
|
|
121
130
|
|
|
131
|
+
const hasErrors = Object.keys(formex.errors).length > 0 && formex.submitCount > 0;
|
|
132
|
+
|
|
122
133
|
return <DialogActions position={"absolute"}>
|
|
123
134
|
{savingError &&
|
|
124
135
|
<div className="text-right">
|
|
@@ -151,7 +162,7 @@ function buildBottomActions<M extends object>({
|
|
|
151
162
|
))}
|
|
152
163
|
</div>}
|
|
153
164
|
{pluginActions}
|
|
154
|
-
<Button variant="text" disabled={disabled || isSubmitting}
|
|
165
|
+
<Button variant="text" disabled={disabled || formex.isSubmitting}
|
|
155
166
|
color={"primary"}
|
|
156
167
|
type="reset">
|
|
157
168
|
{status === "existing" ? "Discard" : "Clear"}
|
|
@@ -159,7 +170,8 @@ function buildBottomActions<M extends object>({
|
|
|
159
170
|
<Button variant={"filled"}
|
|
160
171
|
color="primary"
|
|
161
172
|
type="submit"
|
|
162
|
-
disabled={disabled || isSubmitting}
|
|
173
|
+
disabled={disabled || formex.isSubmitting}
|
|
174
|
+
startIcon={hasErrors ? <ErrorIcon/> : undefined}>
|
|
163
175
|
{status === "existing" && "Save"}
|
|
164
176
|
{status === "copy" && "Create copy"}
|
|
165
177
|
{status === "new" && "Create"}
|
|
@@ -178,12 +190,14 @@ function buildSideActions<M extends object>({
|
|
|
178
190
|
collection,
|
|
179
191
|
context,
|
|
180
192
|
sideEntityController,
|
|
181
|
-
isSubmitting,
|
|
182
193
|
disabled,
|
|
183
194
|
status,
|
|
184
|
-
pluginActions
|
|
195
|
+
pluginActions,
|
|
196
|
+
formex
|
|
185
197
|
}: ActionsViewProps<M>) {
|
|
186
198
|
|
|
199
|
+
const hasErrors = Object.keys(formex.errors).length > 0 && formex.submitCount > 0;
|
|
200
|
+
|
|
187
201
|
return <div
|
|
188
202
|
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)}>
|
|
189
203
|
<LoadingButton fullWidth={true}
|
|
@@ -191,12 +205,13 @@ function buildSideActions<M extends object>({
|
|
|
191
205
|
color="primary"
|
|
192
206
|
type="submit"
|
|
193
207
|
size={"large"}
|
|
194
|
-
|
|
208
|
+
startIcon={hasErrors ? <ErrorIcon/> : undefined}
|
|
209
|
+
disabled={disabled || formex.isSubmitting}>
|
|
195
210
|
{status === "existing" && "Save"}
|
|
196
211
|
{status === "copy" && "Create copy"}
|
|
197
212
|
{status === "new" && "Create"}
|
|
198
213
|
</LoadingButton>
|
|
199
|
-
<Button fullWidth={true} variant="text" disabled={disabled || isSubmitting} type="reset">
|
|
214
|
+
<Button fullWidth={true} variant="text" disabled={disabled || formex.isSubmitting} type="reset">
|
|
200
215
|
{status === "existing" ? "Discard" : "Clear"}
|
|
201
216
|
</Button>
|
|
202
217
|
|
|
@@ -93,6 +93,10 @@ function PropertyFieldBindingInternal<T extends CMSType = CMSType, M extends Rec
|
|
|
93
93
|
const authController = useAuthController();
|
|
94
94
|
const customizationController = useCustomizationController();
|
|
95
95
|
|
|
96
|
+
if(propertyKey === "created_by"){
|
|
97
|
+
console.log("Rendering field for created_by", {propertyKey, property, context});
|
|
98
|
+
}
|
|
99
|
+
|
|
96
100
|
return (
|
|
97
101
|
<Field
|
|
98
102
|
key={propertyKey}
|
|
@@ -1,51 +1,44 @@
|
|
|
1
|
-
import React, { useEffect } from "react";
|
|
1
|
+
import React, { useEffect, useRef } from "react";
|
|
2
2
|
import { useFormex } from "@firecms/formex";
|
|
3
3
|
|
|
4
4
|
export const ErrorFocus = ({ containerRef }:
|
|
5
5
|
{
|
|
6
6
|
containerRef?: React.RefObject<HTMLDivElement>
|
|
7
7
|
}) => {
|
|
8
|
-
const {
|
|
8
|
+
const {
|
|
9
|
+
isValidating,
|
|
10
|
+
errors,
|
|
11
|
+
version
|
|
12
|
+
} = useFormex();
|
|
13
|
+
|
|
14
|
+
const prevVersion = useRef(version);
|
|
9
15
|
|
|
10
16
|
useEffect(() => {
|
|
17
|
+
|
|
18
|
+
if (version === prevVersion.current) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
11
22
|
const keys = Object.keys(errors);
|
|
12
23
|
|
|
13
|
-
// Whenever there are errors and the form
|
|
14
|
-
if (keys.length > 0
|
|
24
|
+
// Whenever there are errors and the form has been submitted and is not validating
|
|
25
|
+
if (!isValidating && keys.length > 0) {
|
|
15
26
|
const errorElement = containerRef?.current?.querySelector<HTMLDivElement>(
|
|
16
27
|
`#form_field_${keys[0]}`
|
|
17
28
|
);
|
|
18
29
|
|
|
19
|
-
if (errorElement
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
top: scrollableParent.scrollTop + top - 196,
|
|
25
|
-
behavior: "smooth"
|
|
26
|
-
});
|
|
27
|
-
}
|
|
30
|
+
if (errorElement) {
|
|
31
|
+
errorElement.scrollIntoView({
|
|
32
|
+
behavior: "smooth",
|
|
33
|
+
block: "center"
|
|
34
|
+
});
|
|
28
35
|
const input = errorElement.querySelector("input");
|
|
29
36
|
if (input) input.focus();
|
|
30
37
|
}
|
|
38
|
+
prevVersion.current = version;
|
|
31
39
|
}
|
|
32
|
-
}, [
|
|
40
|
+
}, [isValidating, errors, containerRef, version]);
|
|
33
41
|
|
|
34
42
|
// This component does not render anything by itself.
|
|
35
43
|
return null;
|
|
36
44
|
};
|
|
37
|
-
|
|
38
|
-
const isScrollable = (ele: HTMLElement | null) => {
|
|
39
|
-
const hasScrollableContent = ele && ele.scrollHeight > ele.clientHeight;
|
|
40
|
-
|
|
41
|
-
const overflowYStyle = ele ? window.getComputedStyle(ele).overflowY : null;
|
|
42
|
-
const isOverflowHidden = overflowYStyle && overflowYStyle.indexOf("hidden") !== -1;
|
|
43
|
-
|
|
44
|
-
return hasScrollableContent && !isOverflowHidden;
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
const getScrollableParent = (ele: HTMLElement | null): HTMLElement | null => {
|
|
48
|
-
return (!ele || ele === document.body)
|
|
49
|
-
? document.body
|
|
50
|
-
: (isScrollable(ele) ? ele : getScrollableParent(ele.parentNode as HTMLElement));
|
|
51
|
-
};
|