@rebasepro/admin 0.6.1 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{CollectionEditorDialog-HNXGxXdO.js → CollectionEditorDialog-BO_aQ-Z4.js} +236 -155
- package/dist/CollectionEditorDialog-BO_aQ-Z4.js.map +1 -0
- package/dist/{PropertyEditView-C2wp8kQ9.js → PropertyEditView-DKzOpMjR.js} +190 -50
- package/dist/PropertyEditView-DKzOpMjR.js.map +1 -0
- package/dist/{CollectionsStudioView-B1mN33GB.js → RouterCollectionsStudioView-BQTIExjA.js} +61 -39
- package/dist/RouterCollectionsStudioView-BQTIExjA.js.map +1 -0
- package/dist/collection_editor/_cms_internals.d.ts +1 -1
- package/dist/collection_editor/api/generateCollectionApi.d.ts +3 -2
- package/dist/collection_editor/extensibility_types.d.ts +135 -0
- package/dist/collection_editor/index.d.ts +6 -0
- package/dist/collection_editor/serializable_types.d.ts +370 -0
- package/dist/collection_editor/serializable_utils.d.ts +50 -0
- package/dist/collection_editor/ui/collection_editor/CollectionEditorDialog.d.ts +3 -3
- package/dist/collection_editor/ui/collection_editor/CollectionPropertiesEditorForm.d.ts +12 -1
- package/dist/collection_editor/ui/collection_editor/CollectionStudioView.d.ts +13 -1
- package/dist/collection_editor/ui/collection_editor/CollectionsStudioView.d.ts +24 -2
- package/dist/collection_editor/ui/collection_editor/DisplaySettingsForm.d.ts +2 -1
- package/dist/collection_editor/ui/collection_editor/GeneralSettingsForm.d.ts +4 -1
- package/dist/collection_editor/ui/collection_editor/PropertyEditView.d.ts +12 -1
- package/dist/collection_editor/ui/collection_editor/RouterCollectionStudioView.d.ts +15 -0
- package/dist/collection_editor/ui/collection_editor/RouterCollectionsStudioView.d.ts +14 -0
- package/dist/collection_editor/useJsonCollectionsConfigController.d.ts +37 -0
- package/dist/collection_editor/useSafeSnackbarController.d.ts +2 -0
- package/dist/collection_editor_ui.d.ts +4 -0
- package/dist/collection_editor_ui.js +33 -4
- package/dist/collection_editor_ui.js.map +1 -0
- package/dist/components/EntityCollectionTable/EntityCollectionTable.d.ts +1 -1
- package/dist/components/EntityCollectionTable/index.d.ts +1 -1
- package/dist/components/EntityCollectionView/EntityBoardCard.d.ts +3 -3
- package/dist/components/EntityCollectionView/hooks/useKanbanDragAndDrop.d.ts +3 -3
- package/dist/components/EntityEditView.d.ts +3 -2
- package/dist/components/RebaseCMS.d.ts +1 -1
- package/dist/components/SelectableTable/SelectionStore.d.ts +12 -21
- package/dist/components/field_configs.d.ts +1 -1
- package/dist/editor/editor.d.ts +0 -6
- package/dist/editor.js +1 -3
- package/dist/editor.js.map +1 -1
- package/dist/form/EntityForm.d.ts +10 -9
- package/dist/form/EntityFormBinding.d.ts +27 -0
- package/dist/form/components/StorageUploadProgress.d.ts +6 -1
- package/dist/form/form_utils.d.ts +8 -0
- package/dist/form/index.d.ts +5 -2
- package/dist/{history-BO6VT85a.js → history-B3cqBzEV.js} +2 -2
- package/dist/{history-BO6VT85a.js.map → history-B3cqBzEV.js.map} +1 -1
- package/dist/hooks/navigation/contexts/CollectionRegistryContext.d.ts +1 -1
- package/dist/hooks/navigation/useAsyncResolver.d.ts +26 -0
- package/dist/hooks/navigation/useBuildCollectionRegistryController.d.ts +2 -0
- package/dist/hooks/navigation/useTopLevelNavigation.d.ts +1 -1
- package/dist/hooks/navigation/utils.d.ts +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +2 -2
- package/dist/preview/components/StorageThumbnail.d.ts +3 -1
- package/dist/routes/CustomViewRoute.d.ts +2 -1
- package/dist/{src-BYniefVu.js → src-cxV1ODRG.js} +1731 -1710
- package/dist/src-cxV1ODRG.js.map +1 -0
- package/dist/types/components/EntityFormActionsProps.d.ts +1 -1
- package/dist/types/components/EntityFormProps.d.ts +59 -0
- package/dist/types/components/PropertyPreviewProps.d.ts +2 -1
- package/dist/types/fields.d.ts +10 -7
- package/dist/util/dataControllerAdapter.d.ts +30 -0
- package/dist/util/navigation_utils.d.ts +1 -1
- package/dist/util/propertyConfigMapper.d.ts +17 -0
- package/package.json +8 -8
- package/src/collection_editor/_cms_internals.ts +1 -1
- package/src/collection_editor/api/generateCollectionApi.ts +3 -2
- package/src/collection_editor/extensibility_types.ts +168 -0
- package/src/collection_editor/index.ts +46 -0
- package/src/collection_editor/pgColumnToProperty.ts +2 -2
- package/src/collection_editor/serializable_types.ts +474 -0
- package/src/collection_editor/serializable_utils.ts +576 -0
- package/src/collection_editor/ui/collection_editor/AICollectionGeneratorPopover.tsx +6 -6
- package/src/collection_editor/ui/collection_editor/CollectionEditorDialog.tsx +170 -89
- package/src/collection_editor/ui/collection_editor/CollectionPropertiesEditorForm.tsx +95 -78
- package/src/collection_editor/ui/collection_editor/CollectionStudioView.tsx +23 -20
- package/src/collection_editor/ui/collection_editor/CollectionsStudioView.tsx +79 -27
- package/src/collection_editor/ui/collection_editor/DisplaySettingsForm.tsx +9 -7
- package/src/collection_editor/ui/collection_editor/GeneralSettingsForm.tsx +22 -5
- package/src/collection_editor/ui/collection_editor/GetCodeDialog.tsx +3 -3
- package/src/collection_editor/ui/collection_editor/PropertyEditView.tsx +154 -18
- package/src/collection_editor/ui/collection_editor/PropertyFieldPreview.tsx +6 -6
- package/src/collection_editor/ui/collection_editor/PropertyTree.tsx +1 -0
- package/src/collection_editor/ui/collection_editor/RouterCollectionStudioView.tsx +44 -0
- package/src/collection_editor/ui/collection_editor/RouterCollectionsStudioView.tsx +41 -0
- package/src/collection_editor/ui/collection_editor/properties/EnumPropertyField.tsx +4 -3
- package/src/collection_editor/ui/collection_editor/properties/StoragePropertyField.tsx +35 -1
- package/src/collection_editor/ui/collection_editor/templates/blog_template.ts +1 -1
- package/src/collection_editor/ui/collection_editor/utils/supported_fields.tsx +0 -1
- package/src/collection_editor/ui/collection_editor/utils/update_property_for_widget.ts +7 -29
- package/src/collection_editor/useJsonCollectionsConfigController.tsx +286 -0
- package/src/collection_editor/useSafeSnackbarController.ts +9 -0
- package/src/collection_editor_ui.ts +6 -0
- package/src/components/DefaultDrawer.tsx +14 -4
- package/src/components/DrawerNavigationGroup.tsx +2 -2
- package/src/components/EntityCollectionTable/EntityCollectionTable.tsx +11 -9
- package/src/components/EntityCollectionTable/fields/TableMultipleRelationField.tsx +2 -1
- package/src/components/EntityCollectionTable/fields/TableRelationField.tsx +2 -1
- package/src/components/EntityCollectionTable/fields/TableStorageUpload.tsx +23 -20
- package/src/components/EntityCollectionTable/index.tsx +1 -1
- package/src/components/EntityCollectionTable/table_bindings.tsx +24 -44
- package/src/components/EntityCollectionView/EntityBoardCard.tsx +6 -6
- package/src/components/EntityCollectionView/EntityCollectionBoardView.tsx +13 -12
- package/src/components/EntityCollectionView/EntityCollectionCardView.tsx +40 -190
- package/src/components/EntityCollectionView/EntityCollectionListView.tsx +66 -184
- package/src/components/EntityCollectionView/EntityCollectionView.tsx +8 -25
- package/src/components/EntityCollectionView/hooks/useCollectionInlineEditor.ts +1 -1
- package/src/components/EntityCollectionView/hooks/useKanbanDragAndDrop.ts +6 -6
- package/src/components/EntityCollectionView/useBoardDataController.tsx +12 -40
- package/src/components/EntityCollectionView/useEntityPreviewSlots.ts +3 -3
- package/src/components/EntityDetailView.tsx +2 -2
- package/src/components/EntityEditView.tsx +7 -6
- package/src/components/EntityEditViewFormActions.tsx +35 -29
- package/src/components/HomePage/ContentHomePage.tsx +14 -5
- package/src/components/RebaseCMS.tsx +3 -3
- package/src/components/RebaseNavigation.tsx +74 -9
- package/src/components/RelationSelector.tsx +2 -2
- package/src/components/SelectableTable/SelectableTable.tsx +13 -5
- package/src/components/SelectableTable/SelectionStore.ts +18 -48
- package/src/components/SideDialogs.tsx +0 -7
- package/src/components/common/default_entity_actions.tsx +22 -11
- package/src/components/field_configs.tsx +0 -14
- package/src/contexts/BreacrumbsContext.tsx +15 -6
- package/src/editor/editor.tsx +4 -7
- package/src/editor/selectors/ai-selector.tsx +0 -1
- package/src/form/EntityForm.tsx +191 -428
- package/src/form/EntityFormActions.tsx +35 -49
- package/src/form/EntityFormBinding.tsx +323 -0
- package/src/form/components/FormLayout.tsx +1 -1
- package/src/form/components/StorageUploadProgress.tsx +9 -2
- package/src/form/field_bindings/MarkdownEditorFieldBinding.tsx +12 -1
- package/src/form/field_bindings/MultipleRelationFieldBinding.tsx +4 -4
- package/src/form/field_bindings/ReadOnlyFieldBinding.tsx +7 -2
- package/src/form/field_bindings/RelationFieldBinding.tsx +5 -7
- package/src/form/field_bindings/StorageUploadFieldBinding.tsx +19 -15
- package/src/form/form_utils.ts +163 -0
- package/src/form/index.tsx +14 -6
- package/src/hooks/navigation/contexts/CollectionRegistryContext.tsx +2 -2
- package/src/hooks/navigation/contexts/NavigationStateContext.tsx +2 -2
- package/src/hooks/navigation/contexts/UrlContext.tsx +1 -2
- package/src/hooks/navigation/useAsyncResolver.ts +100 -0
- package/src/hooks/navigation/useBuildCollectionRegistryController.tsx +15 -8
- package/src/hooks/navigation/useBuildNavigationStateController.tsx +12 -8
- package/src/hooks/navigation/useNavigationResolution.ts +33 -0
- package/src/hooks/navigation/useResolvedCollections.ts +29 -71
- package/src/hooks/navigation/useResolvedViews.tsx +40 -70
- package/src/hooks/navigation/useTopLevelNavigation.ts +20 -16
- package/src/hooks/navigation/utils.ts +7 -4
- package/src/index.ts +6 -1
- package/src/preview/PropertyPreview.tsx +1 -16
- package/src/preview/components/StorageThumbnail.tsx +24 -7
- package/src/routes/CustomViewRoute.tsx +14 -6
- package/src/types/components/EntityFormActionsProps.tsx +1 -1
- package/src/types/components/EntityFormProps.tsx +68 -0
- package/src/types/components/PropertyPreviewProps.tsx +1 -1
- package/src/types/fields.tsx +9 -8
- package/src/util/dataControllerAdapter.ts +93 -0
- package/src/util/navigation_utils.ts +1 -1
- package/src/util/previews.ts +20 -0
- package/src/util/propertyConfigMapper.ts +134 -0
- package/src/util/property_utils.tsx +1 -2
- package/dist/CollectionEditorDialog-HNXGxXdO.js.map +0 -1
- package/dist/CollectionsStudioView-B1mN33GB.js.map +0 -1
- package/dist/PropertyEditView-C2wp8kQ9.js.map +0 -1
- package/dist/components/EntityCollectionTable/fields/VirtualTableDateField.d.ts +0 -12
- package/dist/components/EntityCollectionTable/fields/VirtualTableInput.d.ts +0 -10
- package/dist/components/EntityCollectionTable/fields/VirtualTableNumberInput.d.ts +0 -9
- package/dist/components/EntityCollectionTable/fields/VirtualTableSwitch.d.ts +0 -8
- package/dist/components/EntityCollectionView/Board.d.ts +0 -3
- package/dist/components/EntityCollectionView/BoardColumn.d.ts +0 -42
- package/dist/components/EntityCollectionView/BoardColumnTitle.d.ts +0 -9
- package/dist/components/EntityCollectionView/BoardSortableList.d.ts +0 -14
- package/dist/components/EntityCollectionView/board_types.d.ts +0 -105
- package/dist/form/field_bindings/ReferenceAsStringFieldBinding.d.ts +0 -11
- package/dist/hooks/navigation/useNavigationRegistry.d.ts +0 -12
- package/dist/hooks/navigation/useNavigationURLs.d.ts +0 -11
- package/dist/src-BYniefVu.js.map +0 -1
- package/src/components/EntityCollectionTable/fields/VirtualTableDateField.tsx +0 -39
- package/src/components/EntityCollectionTable/fields/VirtualTableInput.tsx +0 -90
- package/src/components/EntityCollectionTable/fields/VirtualTableNumberInput.tsx +0 -83
- package/src/components/EntityCollectionTable/fields/VirtualTableSwitch.tsx +0 -32
- package/src/components/EntityCollectionView/Board.tsx +0 -475
- package/src/components/EntityCollectionView/BoardColumn.tsx +0 -165
- package/src/components/EntityCollectionView/BoardColumnTitle.tsx +0 -46
- package/src/components/EntityCollectionView/BoardSortableList.tsx +0 -177
- package/src/components/EntityCollectionView/board_types.ts +0 -113
- package/src/form/field_bindings/ReferenceAsStringFieldBinding.tsx +0 -141
- package/src/hooks/navigation/useNavigationRegistry.ts +0 -164
- package/src/hooks/navigation/useNavigationURLs.ts +0 -56
package/src/form/EntityForm.tsx
CHANGED
|
@@ -1,30 +1,22 @@
|
|
|
1
|
-
import type { EntityCollection, EntityCustomViewParams } from "@rebasepro/types";
|
|
1
|
+
import type { EntityCollection, EntityCustomViewParams, AdditionalFieldDelegateProps } from "@rebasepro/types";
|
|
2
2
|
import type { FormContext, PropertyFieldBindingProps } from "../types/fields";
|
|
3
|
-
import type {
|
|
3
|
+
import type { PropertyConfig } from "@rebasepro/types";
|
|
4
4
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
5
|
-
import {
|
|
5
|
+
import { Entity, EntityStatus, EntityValues } from "@rebasepro/types";
|
|
6
6
|
import type { EntityFormProps, OnUpdateParams } from "../types/components/EntityFormProps";
|
|
7
7
|
import { deepEqual as equal } from "fast-equals";
|
|
8
8
|
|
|
9
9
|
import { ErrorBoundary } from "@rebasepro/ui";
|
|
10
|
-
import { AlignLeftIcon, CheckIcon, PencilIcon, useDebouncedCallback } from "@rebasepro/ui";
|
|
11
|
-
import { getDefaultValuesFor,
|
|
10
|
+
import { AlignLeftIcon, CheckIcon, LoaderIcon, PencilIcon, useDebouncedCallback } from "@rebasepro/ui";
|
|
11
|
+
import { getDefaultValuesFor, isHidden, isReadOnly } from "@rebasepro/common";
|
|
12
12
|
|
|
13
|
-
import {
|
|
13
|
+
import { useCustomizationController } from "@rebasepro/core";
|
|
14
14
|
import { getFormFieldKeys } from "@rebasepro/core";
|
|
15
|
-
import { Alert, Chip, cls, iconSize, paperMixin, Tooltip, Typography } from "@rebasepro/ui";
|
|
16
|
-
import { Formex, FormexController,
|
|
17
|
-
import { useAnalyticsController } from "@rebasepro/core";
|
|
15
|
+
import { Alert, Button, Chip, cls, Dialog, DialogActions, DialogContent, DialogTitle, iconSize, paperMixin, Tooltip, Typography } from "@rebasepro/ui";
|
|
16
|
+
import { Formex, FormexController, useCreateFormex } from "@rebasepro/formex";
|
|
18
17
|
|
|
19
18
|
import { FormEntry, FormLayout, LabelWithIconAndTooltip, PropertyFieldBinding } from "../form";
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
flattenKeys,
|
|
23
|
-
getEntityFromCache,
|
|
24
|
-
removeEntityFromCache,
|
|
25
|
-
removeEntityFromMemoryCache,
|
|
26
|
-
saveEntityToCache
|
|
27
|
-
} from "@rebasepro/core";
|
|
19
|
+
import { flattenKeys } from "@rebasepro/core";
|
|
28
20
|
import { ErrorFocus } from "./components/ErrorFocus";
|
|
29
21
|
import { CustomFieldValidator, getEntitySchema } from "./validation";
|
|
30
22
|
import { EntityFormActions } from "./EntityFormActions";
|
|
@@ -32,115 +24,21 @@ import type { EntityFormActionsProps } from "../types/components/EntityFormActio
|
|
|
32
24
|
import { LocalChangesMenu } from "./components/LocalChangesMenu";
|
|
33
25
|
|
|
34
26
|
import { getEntityTitlePropertyKey, resolveTitleToString } from "../util/previews";
|
|
35
|
-
import { getValueInPath,
|
|
36
|
-
import {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
let acc: Record<string, unknown> = {};
|
|
41
|
-
if (!touched || typeof touched !== "object") {
|
|
42
|
-
return acc;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
Object.entries(touched).forEach(([key, value]) => {
|
|
46
|
-
if (value) {
|
|
47
|
-
acc = setIn(acc, key, getIn(values, key)) as Record<string, unknown>;
|
|
48
|
-
}
|
|
49
|
-
})
|
|
50
|
-
|
|
51
|
-
return acc;
|
|
52
|
-
}
|
|
27
|
+
import { getValueInPath, mergeDeep } from "@rebasepro/utils";
|
|
28
|
+
import {
|
|
29
|
+
getChanges,
|
|
30
|
+
zodToFormErrors
|
|
31
|
+
} from "./form_utils";
|
|
53
32
|
|
|
54
33
|
/**
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
*
|
|
34
|
+
* Headless entity form component.
|
|
35
|
+
*
|
|
36
|
+
* Renders a form for an entity collection without any CMS or backend dependencies.
|
|
37
|
+
* All backend concerns (save, caching, analytics, plugin slots) are provided via
|
|
38
|
+
* callback props. For CMS-connected usage, use {@link EntityFormBinding} instead.
|
|
39
|
+
*
|
|
40
|
+
* @group Components
|
|
62
41
|
*/
|
|
63
|
-
function isSemanticEmpty(v: unknown): boolean {
|
|
64
|
-
return v === null || v === undefined || v === "";
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function removeEmptyContainers(obj: unknown): unknown {
|
|
68
|
-
if (Array.isArray(obj)) {
|
|
69
|
-
const cleaned = obj.map(removeEmptyContainers);
|
|
70
|
-
// Keep arrays even if they contain only nulls/undefined — that's intentional data
|
|
71
|
-
return cleaned;
|
|
72
|
-
}
|
|
73
|
-
if (obj && typeof obj === "object" && Object.getPrototypeOf(obj) === Object.prototype) {
|
|
74
|
-
const result: Record<string, unknown> = {};
|
|
75
|
-
for (const key of Object.keys(obj)) {
|
|
76
|
-
const cleaned = removeEmptyContainers((obj as Record<string, unknown>)[key]);
|
|
77
|
-
// Skip empty plain objects
|
|
78
|
-
if (cleaned && typeof cleaned === "object" && !Array.isArray(cleaned)
|
|
79
|
-
&& Object.getPrototypeOf(cleaned) === Object.prototype
|
|
80
|
-
&& Object.keys(cleaned).length === 0) {
|
|
81
|
-
continue;
|
|
82
|
-
}
|
|
83
|
-
result[key] = cleaned;
|
|
84
|
-
}
|
|
85
|
-
// After cleaning, check if all remaining values are semantically empty
|
|
86
|
-
// (null, undefined, or ""). This catches ghost objects like {type: "", value: null}
|
|
87
|
-
// created by oneOf block initialization that aren't meaningful changes.
|
|
88
|
-
if (Object.keys(result).length > 0 && Object.values(result).every(isSemanticEmpty)) {
|
|
89
|
-
return {};
|
|
90
|
-
}
|
|
91
|
-
return result;
|
|
92
|
-
}
|
|
93
|
-
return obj;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
export function getChanges<T extends object>(source: Partial<T>, comparison: Partial<T>): Partial<T> {
|
|
97
|
-
const changes: Partial<T> = {};
|
|
98
|
-
|
|
99
|
-
if (!source) {
|
|
100
|
-
return {};
|
|
101
|
-
}
|
|
102
|
-
if (!comparison) {
|
|
103
|
-
return source;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
const allKeys = Array.from(new Set([...Object.keys(source), ...Object.keys(comparison)]));
|
|
107
|
-
|
|
108
|
-
for (const key of allKeys) {
|
|
109
|
-
const sourceValue = (source as Record<string, unknown>)[key];
|
|
110
|
-
const comparisonValue = (comparison as Record<string, unknown>)[key];
|
|
111
|
-
|
|
112
|
-
if (equal(sourceValue, comparisonValue)) {
|
|
113
|
-
continue;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
const sourceHasKey = source && typeof source === "object" && Object.prototype.hasOwnProperty.call(source, key);
|
|
117
|
-
const comparisonHasKey = comparison && typeof comparison === "object" && Object.prototype.hasOwnProperty.call(comparison, key);
|
|
118
|
-
|
|
119
|
-
if (comparisonHasKey && !sourceHasKey) {
|
|
120
|
-
(changes as Record<string, unknown>)[key] = undefined;
|
|
121
|
-
} else if (Array.isArray(sourceValue)) {
|
|
122
|
-
const comparisonArray = Array.isArray(comparisonValue) ? comparisonValue : [];
|
|
123
|
-
if (sourceValue.length !== comparisonArray.length) {
|
|
124
|
-
(changes as Record<string, unknown>)[key] = sourceValue;
|
|
125
|
-
continue;
|
|
126
|
-
}
|
|
127
|
-
const hasChanges = sourceValue.some((item, index) => !equal(item, comparisonArray[index]));
|
|
128
|
-
if (hasChanges) {
|
|
129
|
-
(changes as Record<string, unknown>)[key] = sourceValue;
|
|
130
|
-
}
|
|
131
|
-
} else if (isObject(sourceValue) && sourceValue && isObject(comparisonValue) && comparisonValue) {
|
|
132
|
-
const nestedChanges = getChanges(sourceValue, comparisonValue);
|
|
133
|
-
if (Object.keys(nestedChanges).length > 0) {
|
|
134
|
-
(changes as Record<string, unknown>)[key] = nestedChanges;
|
|
135
|
-
}
|
|
136
|
-
} else {
|
|
137
|
-
(changes as Record<string, unknown>)[key] = sourceValue;
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
return changes;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
42
|
export function EntityForm<M extends Record<string, unknown>>({
|
|
145
43
|
path,
|
|
146
44
|
entityId: entityIdProp,
|
|
@@ -164,26 +62,33 @@ export function EntityForm<M extends Record<string, unknown>>({
|
|
|
164
62
|
showDefaultActions = true,
|
|
165
63
|
showEntityPath = true,
|
|
166
64
|
navigateBack: navigateBackProp,
|
|
167
|
-
children
|
|
65
|
+
children,
|
|
66
|
+
// Headless callback props
|
|
67
|
+
onSubmit: onSubmitProp,
|
|
68
|
+
onValuesChangeDeferred: onValuesChangeDeferredProp,
|
|
69
|
+
onReset: onResetProp,
|
|
70
|
+
uniqueFieldValidator: uniqueFieldValidatorProp,
|
|
71
|
+
// Slots
|
|
72
|
+
beforeFields,
|
|
73
|
+
afterFields,
|
|
74
|
+
pluginActions: pluginActionsProp,
|
|
75
|
+
// Local changes (managed externally)
|
|
76
|
+
computedInitialValues,
|
|
77
|
+
hasLocalChanges: hasLocalChangesProp,
|
|
78
|
+
localChangesData,
|
|
79
|
+
manualApplyLocalChanges,
|
|
80
|
+
localChangesCacheKey,
|
|
81
|
+
onClearLocalChanges
|
|
168
82
|
}: EntityFormProps<M>) {
|
|
169
|
-
|
|
170
|
-
const
|
|
171
|
-
const collectionRegistryController = useCollectionRegistryController();
|
|
83
|
+
|
|
84
|
+
const customizationController = useCustomizationController();
|
|
172
85
|
|
|
173
86
|
const navigateBack = useCallback(() => {
|
|
174
87
|
if (navigateBackProp) {
|
|
175
88
|
navigateBackProp();
|
|
176
|
-
return;
|
|
177
|
-
}
|
|
178
|
-
if (openEntityMode === "side_panel" || openEntityMode === "dialog") {
|
|
179
|
-
// If we are in side panel mode or dialog mode, we close the panel
|
|
180
|
-
sideEntityController.close();
|
|
181
|
-
} else {
|
|
182
|
-
window.history.back();
|
|
183
89
|
}
|
|
184
|
-
}, [navigateBackProp
|
|
90
|
+
}, [navigateBackProp]);
|
|
185
91
|
|
|
186
|
-
const authController = useAuthController();
|
|
187
92
|
const [status, setStatus] = useState<EntityStatus>(initialStatus);
|
|
188
93
|
|
|
189
94
|
const updateStatus = (status: EntityStatus) => {
|
|
@@ -193,21 +98,13 @@ export function EntityForm<M extends Record<string, unknown>>({
|
|
|
193
98
|
|
|
194
99
|
const [valuesToBeSaved, setValuesToBeSaved] = useState<EntityValues<M> | undefined>(undefined);
|
|
195
100
|
useDebouncedCallback(valuesToBeSaved, () => {
|
|
196
|
-
if (valuesToBeSaved)
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
values: valuesToBeSaved
|
|
202
|
-
});
|
|
101
|
+
if (valuesToBeSaved && onSubmitProp) {
|
|
102
|
+
setIsSavingAutoSave(true);
|
|
103
|
+
Promise.resolve(onSubmitProp(valuesToBeSaved, formex))
|
|
104
|
+
.finally(() => setIsSavingAutoSave(false));
|
|
105
|
+
}
|
|
203
106
|
}, false, 2000);
|
|
204
107
|
|
|
205
|
-
const dataClient = useData();
|
|
206
|
-
const snackbarController = useSnackbarController();
|
|
207
|
-
const customizationController = useCustomizationController();
|
|
208
|
-
const context = useCMSContext();
|
|
209
|
-
const analyticsController = useAnalyticsController();
|
|
210
|
-
|
|
211
108
|
const [underlyingChanges] = useState<Partial<EntityValues<M>>>({});
|
|
212
109
|
|
|
213
110
|
const initialEntityId: string | number | undefined = useMemo(() => {
|
|
@@ -221,20 +118,29 @@ export function EntityForm<M extends Record<string, unknown>>({
|
|
|
221
118
|
const [entityId, setEntityId] = useState<string | number | undefined>(initialEntityId);
|
|
222
119
|
const [entityIdError, setEntityIdError] = useState<boolean>(false);
|
|
223
120
|
const [savingError, setSavingError] = useState<Error | undefined>();
|
|
121
|
+
const [isSavingAutoSave, setIsSavingAutoSave] = useState<boolean>(false);
|
|
122
|
+
const [discardDialogOpen, setDiscardDialogOpen] = useState<boolean>(false);
|
|
224
123
|
|
|
225
124
|
const autoSave = collection.formAutoSave;
|
|
226
125
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
126
|
+
// Use externally computed initial values if provided, otherwise compute from entity + collection
|
|
127
|
+
const baseInitialValues = useMemo(() => {
|
|
128
|
+
if (computedInitialValues !== undefined) {
|
|
129
|
+
return computedInitialValues;
|
|
130
|
+
}
|
|
131
|
+
// Fallback: compute from entity/collection (requires authController from context)
|
|
132
|
+
// In headless mode without computedInitialValues, use entity values or defaults
|
|
133
|
+
if ((status === "existing" || status === "copy") && entity) {
|
|
134
|
+
return entity.values ?? getDefaultValuesFor(collection.properties);
|
|
135
|
+
}
|
|
136
|
+
return getDefaultValuesFor(collection.properties);
|
|
137
|
+
}, [computedInitialValues, collection.properties, status, entity]);
|
|
232
138
|
|
|
233
139
|
const [localChangesCleared, setLocalChangesCleared] = useState<boolean>(false);
|
|
234
140
|
|
|
235
|
-
const
|
|
236
|
-
|
|
237
|
-
|
|
141
|
+
const hasLocalChanges = hasLocalChangesProp !== undefined
|
|
142
|
+
? (hasLocalChangesProp && !localChangesCleared)
|
|
143
|
+
: false;
|
|
238
144
|
|
|
239
145
|
const onSubmit = (values: EntityValues<M>, formexController: FormexController<EntityValues<M>>) => {
|
|
240
146
|
|
|
@@ -247,7 +153,13 @@ export function EntityForm<M extends Record<string, unknown>>({
|
|
|
247
153
|
throw Error("New FormType added, check EntityForm");
|
|
248
154
|
}
|
|
249
155
|
|
|
250
|
-
|
|
156
|
+
if (!onSubmitProp) {
|
|
157
|
+
console.warn("EntityForm: no onSubmit callback provided. Form submission has no effect.");
|
|
158
|
+
formexController.setSubmitting(false);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return Promise.resolve(save(values))
|
|
251
163
|
?.then((savedEntity) => {
|
|
252
164
|
if (savedEntity) {
|
|
253
165
|
formexController.resetForm({
|
|
@@ -255,12 +167,6 @@ export function EntityForm<M extends Record<string, unknown>>({
|
|
|
255
167
|
submitCount: 0,
|
|
256
168
|
touched: {}
|
|
257
169
|
});
|
|
258
|
-
if (!autoSave) {
|
|
259
|
-
snackbarController.open({
|
|
260
|
-
type: "success",
|
|
261
|
-
message: `${collection.singularName ?? collection.name}: ${t("saved_correctly")}`
|
|
262
|
-
});
|
|
263
|
-
}
|
|
264
170
|
}
|
|
265
171
|
})
|
|
266
172
|
.finally(() => {
|
|
@@ -269,27 +175,10 @@ export function EntityForm<M extends Record<string, unknown>>({
|
|
|
269
175
|
};
|
|
270
176
|
|
|
271
177
|
const [initialValues, initialDirty] = useMemo(() => {
|
|
272
|
-
const
|
|
273
|
-
const initialValues = initialDirtyValues ? mergeDeep(initialValuesWithLocalChanges, initialDirtyValues) : initialValuesWithLocalChanges;
|
|
178
|
+
const initialValues = initialDirtyValues ? mergeDeep(baseInitialValues, initialDirtyValues) : baseInitialValues;
|
|
274
179
|
const initialDirty = Boolean(initialDirtyValues) && initialDirtyValues && Object.keys(initialDirtyValues).length > 0;
|
|
275
180
|
return [initialValues, initialDirty];
|
|
276
|
-
}, [
|
|
277
|
-
|
|
278
|
-
const localChangesData = useMemo(() => {
|
|
279
|
-
if (!localChangesDataRaw) {
|
|
280
|
-
return undefined;
|
|
281
|
-
}
|
|
282
|
-
const changes = getChanges(localChangesDataRaw, initialValues);
|
|
283
|
-
// Strip ghost empty containers (e.g. {content: {}}) left by intermediate
|
|
284
|
-
// path construction so they don't falsely trigger the unsaved-changes banner.
|
|
285
|
-
const cleaned = removeEmptyContainers(changes);
|
|
286
|
-
if (cleaned && typeof cleaned === "object" && Object.keys(cleaned).length === 0) {
|
|
287
|
-
return undefined;
|
|
288
|
-
}
|
|
289
|
-
return cleaned;
|
|
290
|
-
}, [localChangesDataRaw, initialValues]);
|
|
291
|
-
|
|
292
|
-
const hasLocalChanges = !localChangesCleared && !!localChangesData && Object.keys(localChangesData as object).length > 0;
|
|
181
|
+
}, [baseInitialValues, initialDirtyValues]);
|
|
293
182
|
|
|
294
183
|
const internalFormex = useCreateFormex<M>({
|
|
295
184
|
initialValues: initialValues as M,
|
|
@@ -304,21 +193,10 @@ export function EntityForm<M extends Record<string, unknown>>({
|
|
|
304
193
|
: {},
|
|
305
194
|
onSubmit,
|
|
306
195
|
onReset: () => {
|
|
307
|
-
|
|
196
|
+
onResetProp?.();
|
|
308
197
|
onValuesModified?.(false, initialValues as M);
|
|
309
198
|
},
|
|
310
|
-
onValuesChangeDeferred:
|
|
311
|
-
const key = (status === "new" || status === "copy") ? path + "#new" : path + "/" + entityId;
|
|
312
|
-
if (controller.dirty) {
|
|
313
|
-
const touchedValues = removeEmptyContainers(extractTouchedValues(values, controller.touched));
|
|
314
|
-
if (touchedValues && Object.keys(touchedValues).length > 0) {
|
|
315
|
-
saveEntityToCache(key, touchedValues);
|
|
316
|
-
} else {
|
|
317
|
-
// Clean up ghost cache entries that contain only empty containers
|
|
318
|
-
removeEntityFromCache(key);
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
},
|
|
199
|
+
onValuesChangeDeferred: onValuesChangeDeferredProp,
|
|
322
200
|
validation: async (values): Promise<Record<string, string>> => {
|
|
323
201
|
if (!validationSchema) return {};
|
|
324
202
|
const result = await validationSchema.safeParseAsync(values);
|
|
@@ -335,8 +213,12 @@ export function EntityForm<M extends Record<string, unknown>>({
|
|
|
335
213
|
const isRedo =
|
|
336
214
|
((e.metaKey || e.ctrlKey) && e.shiftKey && e.key.toLowerCase() === "z") ||
|
|
337
215
|
((e.metaKey || e.ctrlKey) && !e.shiftKey && e.key.toLowerCase() === "y");
|
|
216
|
+
const isSave = (e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "s";
|
|
338
217
|
|
|
339
|
-
if (
|
|
218
|
+
if (isSave && !disabled && formex.dirty) {
|
|
219
|
+
e.preventDefault();
|
|
220
|
+
formex.handleSubmit();
|
|
221
|
+
} else if (isUndo && formex.canUndo) {
|
|
340
222
|
e.preventDefault();
|
|
341
223
|
formex.undo();
|
|
342
224
|
} else if (isRedo && formex.canRedo) {
|
|
@@ -350,35 +232,8 @@ export function EntityForm<M extends Record<string, unknown>>({
|
|
|
350
232
|
|
|
351
233
|
}, [formex]);
|
|
352
234
|
|
|
353
|
-
const beforeSaveHookError = useCallback((e: Error) => {
|
|
354
|
-
snackbarController.open({
|
|
355
|
-
type: "error",
|
|
356
|
-
message: `${t("error_before_saving")}: ${e?.message}`
|
|
357
|
-
});
|
|
358
|
-
console.error(e);
|
|
359
|
-
}, [snackbarController]);
|
|
360
|
-
|
|
361
|
-
const afterSaveHookError = useCallback((e: Error) => {
|
|
362
|
-
snackbarController.open({
|
|
363
|
-
type: "error",
|
|
364
|
-
message: `${t("error_after_saving")}: ${e?.message}`
|
|
365
|
-
});
|
|
366
|
-
console.error(e);
|
|
367
|
-
}, [snackbarController]);
|
|
368
|
-
|
|
369
|
-
function clearDirtyCache() {
|
|
370
|
-
if (status === "new" || status === "copy") {
|
|
371
|
-
removeEntityFromMemoryCache(path + "#new");
|
|
372
|
-
removeEntityFromCache(path + "#new");
|
|
373
|
-
} else {
|
|
374
|
-
removeEntityFromMemoryCache(path + "/" + entityId);
|
|
375
|
-
removeEntityFromCache(path + "/" + entityId);
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
|
|
379
235
|
const afterSave = (updatedEntity: Entity<M>) => {
|
|
380
|
-
|
|
381
|
-
clearDirtyCache();
|
|
236
|
+
onResetProp?.();
|
|
382
237
|
onValuesModified?.(false, updatedEntity.values);
|
|
383
238
|
onEntityChange?.(updatedEntity);
|
|
384
239
|
updateStatus("existing");
|
|
@@ -395,76 +250,6 @@ export function EntityForm<M extends Record<string, unknown>>({
|
|
|
395
250
|
}
|
|
396
251
|
};
|
|
397
252
|
|
|
398
|
-
const afterSaveError = useCallback((e: Error) => {
|
|
399
|
-
snackbarController.open({
|
|
400
|
-
type: "error",
|
|
401
|
-
title: t("error_saving_entity"),
|
|
402
|
-
message: e?.message
|
|
403
|
-
});
|
|
404
|
-
console.error("Error saving entity", path, entityId, e);
|
|
405
|
-
console.error(e);
|
|
406
|
-
}, [entityId, path, snackbarController]);
|
|
407
|
-
|
|
408
|
-
const saveEntity = ({
|
|
409
|
-
values,
|
|
410
|
-
previousValues,
|
|
411
|
-
entityId,
|
|
412
|
-
collection,
|
|
413
|
-
path
|
|
414
|
-
}: {
|
|
415
|
-
collection: EntityCollection<M>,
|
|
416
|
-
path: string,
|
|
417
|
-
entityId: string | number | undefined,
|
|
418
|
-
values: M,
|
|
419
|
-
previousValues?: M,
|
|
420
|
-
}): Promise<Entity<M>> => {
|
|
421
|
-
return saveEntityWithCallbacks({
|
|
422
|
-
path: path,
|
|
423
|
-
entityId,
|
|
424
|
-
values,
|
|
425
|
-
previousValues,
|
|
426
|
-
collection,
|
|
427
|
-
status,
|
|
428
|
-
data: dataClient,
|
|
429
|
-
context,
|
|
430
|
-
afterSave,
|
|
431
|
-
afterSaveError
|
|
432
|
-
});
|
|
433
|
-
};
|
|
434
|
-
|
|
435
|
-
type EntityFormSaveParams<M extends Record<string, unknown>> = {
|
|
436
|
-
collection: EntityCollection<M>,
|
|
437
|
-
path: string,
|
|
438
|
-
entityId: string | number | undefined,
|
|
439
|
-
values: EntityValues<M>,
|
|
440
|
-
previousValues?: EntityValues<M>,
|
|
441
|
-
autoSave: boolean
|
|
442
|
-
};
|
|
443
|
-
|
|
444
|
-
const onSaveEntityRequest = async ({
|
|
445
|
-
collection,
|
|
446
|
-
path,
|
|
447
|
-
entityId,
|
|
448
|
-
values,
|
|
449
|
-
previousValues,
|
|
450
|
-
autoSave
|
|
451
|
-
}: EntityFormSaveParams<M>): Promise<Entity<M> | void> => {
|
|
452
|
-
if (!status)
|
|
453
|
-
return;
|
|
454
|
-
if (autoSave) {
|
|
455
|
-
setValuesToBeSaved(values);
|
|
456
|
-
return Promise.resolve();
|
|
457
|
-
} else {
|
|
458
|
-
return saveEntity({
|
|
459
|
-
collection,
|
|
460
|
-
path,
|
|
461
|
-
entityId,
|
|
462
|
-
values,
|
|
463
|
-
previousValues
|
|
464
|
-
});
|
|
465
|
-
}
|
|
466
|
-
};
|
|
467
|
-
|
|
468
253
|
const lastSavedValues = useRef<EntityValues<M> | undefined>(entity?.values);
|
|
469
254
|
const save = async (values: EntityValues<M>): Promise<Entity<M> | void> => {
|
|
470
255
|
const valuesToSave = status === "existing"
|
|
@@ -476,29 +261,34 @@ export function EntityForm<M extends Record<string, unknown>>({
|
|
|
476
261
|
}
|
|
477
262
|
|
|
478
263
|
lastSavedValues.current = values;
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
264
|
+
|
|
265
|
+
if (!onSubmitProp) {
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const autoSaveEnabled = autoSave ?? false;
|
|
270
|
+
|
|
271
|
+
if (autoSaveEnabled) {
|
|
272
|
+
setValuesToBeSaved(values);
|
|
273
|
+
return Promise.resolve();
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return Promise.resolve(onSubmitProp(valuesToSave as M, formex))
|
|
277
|
+
.then((result) => {
|
|
278
|
+
if (result) {
|
|
279
|
+
afterSave(result as Entity<M>);
|
|
280
|
+
}
|
|
281
|
+
return result as Entity<M> | void;
|
|
282
|
+
})
|
|
283
|
+
.catch(e => {
|
|
284
|
+
console.error(e);
|
|
285
|
+
setSavingError(e);
|
|
286
|
+
});
|
|
496
287
|
};
|
|
497
288
|
|
|
498
289
|
const disabled = formex.isSubmitting || Boolean(disabledProp);
|
|
499
290
|
|
|
500
291
|
const formContext: FormContext<M> = {
|
|
501
|
-
|
|
502
292
|
setFieldValue: useCallback((key: string, value: unknown) => formex.setFieldValue(key, value), []),
|
|
503
293
|
values: formex.values,
|
|
504
294
|
collection,
|
|
@@ -519,24 +309,6 @@ export function EntityForm<M extends Record<string, unknown>>({
|
|
|
519
309
|
}, [formex.version, collection, entityId, path]);
|
|
520
310
|
|
|
521
311
|
const actionsDisabled = disabled || formex.isSubmitting || (status === "existing" && !formex.dirty) || Boolean(disabledProp);
|
|
522
|
-
const parentCollectionSlugs = collectionRegistryController.getParentCollectionSlugs(path);
|
|
523
|
-
const parentEntityIds = collectionRegistryController.getParentEntityIds(path);
|
|
524
|
-
|
|
525
|
-
const formActionProps: PluginFormActionProps = {
|
|
526
|
-
entityId,
|
|
527
|
-
parentCollectionSlugs,
|
|
528
|
-
parentEntityIds,
|
|
529
|
-
path: path,
|
|
530
|
-
status,
|
|
531
|
-
collection: collection as EntityCollection,
|
|
532
|
-
context,
|
|
533
|
-
formContext: formContext as FormContext<Record<string, unknown>>,
|
|
534
|
-
openEntityMode,
|
|
535
|
-
disabled: actionsDisabled
|
|
536
|
-
};
|
|
537
|
-
const pluginFormActions = useSlot("form.actions", formActionProps);
|
|
538
|
-
const pluginFormBefore = useSlot("form.before", formActionProps);
|
|
539
|
-
const pluginFormAfter = useSlot("form.after", formActionProps);
|
|
540
312
|
|
|
541
313
|
const titlePropertyKey = getEntityTitlePropertyKey(collection, customizationController.propertyConfigs);
|
|
542
314
|
const rawTitle = formex.values && titlePropertyKey ? getValueInPath(formex.values, titlePropertyKey) : undefined;
|
|
@@ -544,31 +316,23 @@ parentEntityIds,
|
|
|
544
316
|
? resolveTitleToString(rawTitle)
|
|
545
317
|
: (collection.singularName ?? collection.name);
|
|
546
318
|
|
|
319
|
+
const modified = formex.dirty;
|
|
320
|
+
|
|
547
321
|
useEffect(() => {
|
|
548
322
|
if (!autoSave) {
|
|
549
323
|
onValuesModified?.(modified, formex.values);
|
|
550
324
|
}
|
|
551
325
|
}, [formex.dirty]);
|
|
552
326
|
|
|
553
|
-
|
|
327
|
+
// Default no-op unique field validator — always passes
|
|
328
|
+
const defaultUniqueFieldValidator: CustomFieldValidator = useCallback(async () => true, []);
|
|
554
329
|
|
|
555
|
-
const uniqueFieldValidator: CustomFieldValidator =
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
}) => {
|
|
559
|
-
try {
|
|
560
|
-
const accessor = dataClient.collection(path);
|
|
561
|
-
const { data } = await accessor.find({
|
|
562
|
-
where: { [name]: `eq.${value}` },
|
|
563
|
-
limit: 2
|
|
564
|
-
});
|
|
565
|
-
const otherEntities = entityId ? data.filter(e => e.id !== entityId) : data;
|
|
566
|
-
return otherEntities.length === 0;
|
|
567
|
-
} catch (e: unknown) {
|
|
568
|
-
console.error("Error checking unique field", e);
|
|
569
|
-
return true;
|
|
330
|
+
const uniqueFieldValidator: CustomFieldValidator = useMemo(() => {
|
|
331
|
+
if (uniqueFieldValidatorProp) {
|
|
332
|
+
return uniqueFieldValidatorProp as CustomFieldValidator;
|
|
570
333
|
}
|
|
571
|
-
|
|
334
|
+
return defaultUniqueFieldValidator;
|
|
335
|
+
}, [uniqueFieldValidatorProp, defaultUniqueFieldValidator]);
|
|
572
336
|
|
|
573
337
|
const validationSchema = useMemo(() => getEntitySchema(
|
|
574
338
|
entityId,
|
|
@@ -580,7 +344,7 @@ parentEntityIds,
|
|
|
580
344
|
|
|
581
345
|
useEffect(() => {
|
|
582
346
|
if (!autoSave && !formex.isSubmitting && underlyingChanges && entity) {
|
|
583
|
-
// we update the form fields from the
|
|
347
|
+
// we update the form fields from the driver data
|
|
584
348
|
// if they were not touched
|
|
585
349
|
Object.entries(underlyingChanges).forEach(([key, value]) => {
|
|
586
350
|
const formValue = formex.values[key];
|
|
@@ -604,6 +368,9 @@ parentEntityIds,
|
|
|
604
368
|
formContext={formContext}
|
|
605
369
|
/>;
|
|
606
370
|
}
|
|
371
|
+
const isNewEntity = status === "new" || status === "copy";
|
|
372
|
+
let firstFocusableIndex = -1;
|
|
373
|
+
|
|
607
374
|
return (
|
|
608
375
|
<FormLayout>
|
|
609
376
|
{formFieldKeys.map((key) => {
|
|
@@ -621,6 +388,10 @@ parentEntityIds,
|
|
|
621
388
|
const hidden = isHidden(property);
|
|
622
389
|
if (hidden) return null;
|
|
623
390
|
const widthPercentage = property.ui?.widthPercentage ?? 100;
|
|
391
|
+
|
|
392
|
+
const shouldAutoFocus = isNewEntity && !disabled && firstFocusableIndex === -1;
|
|
393
|
+
if (shouldAutoFocus) firstFocusableIndex = 0;
|
|
394
|
+
|
|
624
395
|
const cmsFormFieldProps: PropertyFieldBindingProps<M> = {
|
|
625
396
|
propertyKey: key,
|
|
626
397
|
disabled,
|
|
@@ -630,7 +401,7 @@ parentEntityIds,
|
|
|
630
401
|
context: formContext,
|
|
631
402
|
partOfArray: false,
|
|
632
403
|
minimalistView: false,
|
|
633
|
-
autoFocus:
|
|
404
|
+
autoFocus: shouldAutoFocus
|
|
634
405
|
};
|
|
635
406
|
|
|
636
407
|
return (
|
|
@@ -644,16 +415,17 @@ parentEntityIds,
|
|
|
644
415
|
|
|
645
416
|
const additionalField = collection.additionalFields?.find(f => f.key === key);
|
|
646
417
|
if (additionalField && entity) {
|
|
647
|
-
const
|
|
648
|
-
if (!
|
|
418
|
+
const AdditionalFieldBuilder = additionalField.Builder;
|
|
419
|
+
if (!AdditionalFieldBuilder && !additionalField.value) {
|
|
649
420
|
throw new Error("When using additional fields you need to provide a Builder or a value");
|
|
650
421
|
}
|
|
651
|
-
const
|
|
652
|
-
|
|
422
|
+
const additionalFieldContext = formContext as unknown as AdditionalFieldDelegateProps['context'];
|
|
423
|
+
const child = AdditionalFieldBuilder
|
|
424
|
+
? <AdditionalFieldBuilder entity={entity} context={additionalFieldContext}/>
|
|
653
425
|
: <div className={"w-full"}>
|
|
654
426
|
{additionalField.value?.({
|
|
655
427
|
entity,
|
|
656
|
-
context
|
|
428
|
+
context: additionalFieldContext
|
|
657
429
|
})?.toString()}
|
|
658
430
|
</div>;
|
|
659
431
|
|
|
@@ -683,9 +455,11 @@ parentEntityIds,
|
|
|
683
455
|
|
|
684
456
|
const formRef = useRef<HTMLDivElement>(null);
|
|
685
457
|
|
|
458
|
+
const hasFormErrors = Object.keys(formex.errors).length > 0 && formex.submitCount > 0;
|
|
459
|
+
|
|
686
460
|
const formView = <ErrorBoundary>
|
|
687
461
|
<>
|
|
688
|
-
{
|
|
462
|
+
{beforeFields}
|
|
689
463
|
|
|
690
464
|
{!Builder && <div className={"w-full flex flex-col items-start my-4 lg:my-6"}>
|
|
691
465
|
<Typography
|
|
@@ -696,7 +470,7 @@ parentEntityIds,
|
|
|
696
470
|
|
|
697
471
|
{!entity?.values && initialStatus === "existing" &&
|
|
698
472
|
<Alert color={"warning"} size={"small"} outerClassName={"w-full mb-4 text-xs"}>
|
|
699
|
-
|
|
473
|
+
This entity does not exist yet
|
|
700
474
|
</Alert>}
|
|
701
475
|
|
|
702
476
|
{showEntityPath && <Alert color={"base"} outerClassName={"w-full"} size={"small"}>
|
|
@@ -710,7 +484,11 @@ parentEntityIds,
|
|
|
710
484
|
{children}
|
|
711
485
|
|
|
712
486
|
{initialEntityId && !entity && initialStatus !== "new" && <Alert color={"info"} size={"small"}>
|
|
713
|
-
|
|
487
|
+
This entity does not exist yet
|
|
488
|
+
</Alert>}
|
|
489
|
+
|
|
490
|
+
{hasFormErrors && <Alert color={"error"} size={"small"} outerClassName={"w-full mt-2"}>
|
|
491
|
+
Please fix the highlighted errors before saving.
|
|
714
492
|
</Alert>}
|
|
715
493
|
|
|
716
494
|
{formContext && <>
|
|
@@ -720,9 +498,31 @@ parentEntityIds,
|
|
|
720
498
|
</div>
|
|
721
499
|
</>}
|
|
722
500
|
|
|
723
|
-
{
|
|
501
|
+
{afterFields}
|
|
724
502
|
|
|
725
503
|
{forceActionsAtTheBottom && <div className="h-16"/>}
|
|
504
|
+
|
|
505
|
+
<Dialog open={discardDialogOpen} onOpenChange={setDiscardDialogOpen} maxWidth={"sm"}>
|
|
506
|
+
<DialogTitle>{status === "existing" ? "Discard changes?" : "Clear form?"}</DialogTitle>
|
|
507
|
+
<DialogContent>
|
|
508
|
+
<Typography>
|
|
509
|
+
{status === "existing"
|
|
510
|
+
? "All unsaved changes will be lost. This cannot be undone."
|
|
511
|
+
: "All entered values will be cleared. This cannot be undone."}
|
|
512
|
+
</Typography>
|
|
513
|
+
</DialogContent>
|
|
514
|
+
<DialogActions>
|
|
515
|
+
<Button variant={"text"} onClick={() => setDiscardDialogOpen(false)}>
|
|
516
|
+
Cancel
|
|
517
|
+
</Button>
|
|
518
|
+
<Button variant={"filled"} color={"error"} onClick={() => {
|
|
519
|
+
setDiscardDialogOpen(false);
|
|
520
|
+
formex.resetForm({ values: baseInitialValues as M });
|
|
521
|
+
}}>
|
|
522
|
+
{status === "existing" ? "Discard" : "Clear"}
|
|
523
|
+
</Button>
|
|
524
|
+
</DialogActions>
|
|
525
|
+
</Dialog>
|
|
726
526
|
</>
|
|
727
527
|
</ErrorBoundary>;
|
|
728
528
|
|
|
@@ -746,7 +546,7 @@ parentEntityIds,
|
|
|
746
546
|
formex={formex as FormexController<Record<string, unknown>>}
|
|
747
547
|
disabled={actionsDisabled}
|
|
748
548
|
status={status}
|
|
749
|
-
pluginActions={
|
|
549
|
+
pluginActions={pluginActionsProp ?? []}
|
|
750
550
|
openEntityMode={openEntityMode}
|
|
751
551
|
showDefaultActions={showDefaultActions}
|
|
752
552
|
navigateBack={navigateBack}
|
|
@@ -757,9 +557,14 @@ parentEntityIds,
|
|
|
757
557
|
<Formex value={formex}>
|
|
758
558
|
<form
|
|
759
559
|
onSubmit={formex.handleSubmit}
|
|
760
|
-
onReset={() =>
|
|
761
|
-
|
|
762
|
-
|
|
560
|
+
onReset={(e) => {
|
|
561
|
+
e.preventDefault();
|
|
562
|
+
if (formex.dirty) {
|
|
563
|
+
setDiscardDialogOpen(true);
|
|
564
|
+
} else {
|
|
565
|
+
formex.resetForm({ values: baseInitialValues as M });
|
|
566
|
+
}
|
|
567
|
+
}}
|
|
763
568
|
noValidate
|
|
764
569
|
className={cls("@container flex-1 flex flex-row w-full overflow-y-auto justify-center", className)}>
|
|
765
570
|
<div
|
|
@@ -773,28 +578,37 @@ parentEntityIds,
|
|
|
773
578
|
: "pt-12 pb-16 px-4 sm:px-8 md:px-10"
|
|
774
579
|
)}>
|
|
775
580
|
<div
|
|
776
|
-
className={"flex flex-row gap-4
|
|
581
|
+
className={"flex flex-row gap-4 justify-end h-0 overflow-visible sticky top-4 z-10"}>
|
|
777
582
|
|
|
778
|
-
{manualApplyLocalChanges && hasLocalChanges &&
|
|
583
|
+
{manualApplyLocalChanges && hasLocalChanges && localChangesCacheKey &&
|
|
779
584
|
<LocalChangesMenu<M>
|
|
780
|
-
cacheKey={
|
|
585
|
+
cacheKey={localChangesCacheKey}
|
|
781
586
|
properties={collection.properties}
|
|
782
587
|
cachedData={localChangesData as Partial<M>}
|
|
783
588
|
formex={formex}
|
|
784
|
-
onClearLocalChanges={() =>
|
|
589
|
+
onClearLocalChanges={() => {
|
|
590
|
+
setLocalChangesCleared(true);
|
|
591
|
+
onClearLocalChanges?.();
|
|
592
|
+
}}
|
|
785
593
|
/>}
|
|
786
594
|
|
|
787
|
-
{
|
|
788
|
-
? <Tooltip title={
|
|
789
|
-
<Chip size={"small"} className={"py-1"} colorScheme={"
|
|
790
|
-
<
|
|
595
|
+
{isSavingAutoSave
|
|
596
|
+
? <Tooltip title={"Saving…"}>
|
|
597
|
+
<Chip size={"small"} className={"py-1"} colorScheme={"blueDarker"}>
|
|
598
|
+
<LoaderIcon size={iconSize.smallest} className={"animate-spin"}/>
|
|
791
599
|
</Chip>
|
|
792
600
|
</Tooltip>
|
|
793
|
-
:
|
|
794
|
-
<
|
|
795
|
-
<
|
|
796
|
-
|
|
797
|
-
|
|
601
|
+
: formex.dirty
|
|
602
|
+
? <Tooltip title={"Form has been modified"}>
|
|
603
|
+
<Chip size={"small"} className={"py-1"} colorScheme={"orangeDarker"}>
|
|
604
|
+
<PencilIcon size={iconSize.smallest}/>
|
|
605
|
+
</Chip>
|
|
606
|
+
</Tooltip>
|
|
607
|
+
: <Tooltip title={"Form is in sync"}>
|
|
608
|
+
<Chip size={"small"} className={"py-1"}>
|
|
609
|
+
<CheckIcon size={iconSize.smallest}/>
|
|
610
|
+
</Chip>
|
|
611
|
+
</Tooltip>}
|
|
798
612
|
</div>
|
|
799
613
|
|
|
800
614
|
{formView}
|
|
@@ -811,57 +625,7 @@ parentEntityIds,
|
|
|
811
625
|
);
|
|
812
626
|
}
|
|
813
627
|
|
|
814
|
-
|
|
815
|
-
authController: AuthController,
|
|
816
|
-
collection: EntityCollection,
|
|
817
|
-
path: string,
|
|
818
|
-
status: "new" | "existing" | "copy",
|
|
819
|
-
entity: Entity<M> | undefined,
|
|
820
|
-
propertyConfigs?: Record<string, PropertyConfig>
|
|
821
|
-
): Partial<EntityValues<M>> {
|
|
822
|
-
const properties = collection.properties;
|
|
823
|
-
if ((status === "existing" || status === "copy") && entity) {
|
|
824
|
-
let values: Partial<EntityValues<M>>;
|
|
825
|
-
if (!collection.alwaysApplyDefaultValues) {
|
|
826
|
-
values = entity.values ?? getDefaultValuesFor(properties);
|
|
827
|
-
} else {
|
|
828
|
-
const defaultValues = getDefaultValuesFor(properties);
|
|
829
|
-
values = mergeDeep(defaultValues, entity.values ?? {});
|
|
830
|
-
}
|
|
831
|
-
// When copying, clear ID fields so the database generates new IDs
|
|
832
|
-
if (status === "copy") {
|
|
833
|
-
const result = { ...values };
|
|
834
|
-
for (const [key, property] of Object.entries(properties)) {
|
|
835
|
-
if (property && "isId" in property && property.isId) {
|
|
836
|
-
delete (result as Record<string, unknown>)[key];
|
|
837
|
-
}
|
|
838
|
-
}
|
|
839
|
-
return result;
|
|
840
|
-
}
|
|
841
|
-
return values;
|
|
842
|
-
} else if (status === "new") {
|
|
843
|
-
return getDefaultValuesFor(properties);
|
|
844
|
-
} else {
|
|
845
|
-
console.error({
|
|
846
|
-
status,
|
|
847
|
-
entity
|
|
848
|
-
});
|
|
849
|
-
throw new Error("Form has not been initialised with the correct parameters");
|
|
850
|
-
}
|
|
851
|
-
}
|
|
852
|
-
|
|
853
|
-
export function zodToFormErrors(zodError: z.ZodError): Record<string, string> {
|
|
854
|
-
let errors: Record<string, string> = {};
|
|
855
|
-
for (const issue of zodError.issues) {
|
|
856
|
-
const path = issue.path.join(".");
|
|
857
|
-
if (path && !getIn(errors, path)) {
|
|
858
|
-
errors = setIn(errors, path, issue.message) as Record<string, string>;
|
|
859
|
-
}
|
|
860
|
-
}
|
|
861
|
-
return errors;
|
|
862
|
-
}
|
|
863
|
-
|
|
864
|
-
function useOnAutoSave(autoSave: undefined | boolean, formex: FormexController<any>, lastSavedValues: React.MutableRefObject<any>, save: (values: EntityValues<any>) => Promise<Entity<any> | void>) {
|
|
628
|
+
function useOnAutoSave<M extends Record<string, unknown>>(autoSave: undefined | boolean, formex: FormexController<M>, lastSavedValues: React.MutableRefObject<EntityValues<M> | undefined>, save: (values: EntityValues<M>) => Promise<Entity<M> | void>) {
|
|
865
629
|
useEffect(() => {
|
|
866
630
|
if (!autoSave) return;
|
|
867
631
|
if (formex.values && !equal(formex.values, lastSavedValues.current)) {
|
|
@@ -869,4 +633,3 @@ function useOnAutoSave(autoSave: undefined | boolean, formex: FormexController<a
|
|
|
869
633
|
}
|
|
870
634
|
}, [autoSave, formex.values]);
|
|
871
635
|
}
|
|
872
|
-
|