@rebasepro/admin 0.6.1 → 0.7.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-iGgS4rLX.js} +233 -152
- package/dist/CollectionEditorDialog-iGgS4rLX.js.map +1 -0
- package/dist/{PropertyEditView-C2wp8kQ9.js → PropertyEditView-C9yhLyja.js} +129 -15
- package/dist/PropertyEditView-C9yhLyja.js.map +1 -0
- package/dist/{CollectionsStudioView-B1mN33GB.js → RouterCollectionsStudioView-Dqx27GD8.js} +61 -39
- package/dist/RouterCollectionsStudioView-Dqx27GD8.js.map +1 -0
- 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 +374 -0
- package/dist/collection_editor/serializable_utils.d.ts +50 -0
- package/dist/collection_editor/ui/collection_editor/CollectionEditorDialog.d.ts +2 -1
- 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/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/form/EntityForm.d.ts +10 -9
- package/dist/form/EntityFormBinding.d.ts +27 -0
- package/dist/form/form_utils.d.ts +8 -0
- package/dist/form/index.d.ts +5 -1
- package/dist/{history-BO6VT85a.js → history-ChHgyyvM.js} +2 -2
- package/dist/{history-BO6VT85a.js.map → history-ChHgyyvM.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 +2 -0
- package/dist/index.js +2 -2
- package/dist/routes/CustomViewRoute.d.ts +2 -1
- package/dist/{src-BYniefVu.js → src-DOX2nOyh.js} +1586 -1283
- package/dist/src-DOX2nOyh.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/fields.d.ts +6 -1
- 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 +18 -18
- package/src/collection_editor/extensibility_types.ts +168 -0
- package/src/collection_editor/index.ts +46 -0
- package/src/collection_editor/serializable_types.ts +487 -0
- package/src/collection_editor/serializable_utils.ts +588 -0
- package/src/collection_editor/ui/collection_editor/AICollectionGeneratorPopover.tsx +4 -4
- package/src/collection_editor/ui/collection_editor/CollectionEditorDialog.tsx +134 -70
- 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 +155 -10
- 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/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 +13 -3
- package/src/components/DrawerNavigationGroup.tsx +2 -2
- package/src/components/EntityCollectionView/EntityBoardCard.tsx +6 -6
- package/src/components/EntityCollectionView/EntityCollectionBoardView.tsx +12 -11
- package/src/components/EntityCollectionView/EntityCollectionCardView.tsx +40 -190
- package/src/components/EntityCollectionView/EntityCollectionListView.tsx +54 -183
- package/src/components/EntityCollectionView/hooks/useKanbanDragAndDrop.ts +6 -6
- package/src/components/EntityEditView.tsx +5 -4
- package/src/components/EntityEditViewFormActions.tsx +32 -26
- package/src/components/HomePage/ContentHomePage.tsx +13 -4
- package/src/components/RebaseCMS.tsx +3 -2
- package/src/components/RebaseNavigation.tsx +71 -7
- package/src/components/SelectableTable/SelectableTable.tsx +2 -2
- package/src/components/SideDialogs.tsx +0 -7
- package/src/components/common/default_entity_actions.tsx +9 -9
- package/src/contexts/BreacrumbsContext.tsx +15 -6
- package/src/editor/selectors/ai-selector.tsx +0 -1
- package/src/form/EntityForm.tsx +191 -428
- package/src/form/EntityFormActions.tsx +33 -47
- package/src/form/EntityFormBinding.tsx +323 -0
- package/src/form/components/FormLayout.tsx +1 -1
- package/src/form/field_bindings/ReadOnlyFieldBinding.tsx +7 -2
- package/src/form/form_utils.ts +163 -0
- package/src/form/index.tsx +14 -5
- 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 +5 -0
- 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/fields.tsx +7 -1
- package/src/util/dataControllerAdapter.ts +93 -0
- package/src/util/navigation_utils.ts +1 -1
- package/src/util/propertyConfigMapper.ts +134 -0
- package/LICENSE +0 -21
- 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/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/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/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/hooks/navigation/useNavigationRegistry.ts +0 -164
- package/src/hooks/navigation/useNavigationURLs.ts +0 -56
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
|
|
2
2
|
import type { EntityCollection } from "@rebasepro/types";
|
|
3
3
|
import type { FormContext } from "../types/fields";
|
|
4
|
-
import type { EntityAction
|
|
4
|
+
import type { EntityAction } from "@rebasepro/types";
|
|
5
5
|
import React from "react";
|
|
6
|
-
import { Entity
|
|
6
|
+
import { Entity } from "@rebasepro/types";
|
|
7
7
|
import type { EntityFormActionsProps } from "../types/components/EntityFormActionsProps";
|
|
8
8
|
import {
|
|
9
9
|
AlertCircleIcon,
|
|
@@ -13,11 +13,10 @@ import {
|
|
|
13
13
|
DialogActions,
|
|
14
14
|
IconButton,
|
|
15
15
|
LoadingButton,
|
|
16
|
+
Tooltip,
|
|
16
17
|
Typography
|
|
17
18
|
} from "@rebasepro/ui";
|
|
18
|
-
import { useTranslation } from "@rebasepro/core";
|
|
19
19
|
import { FormexController } from "@rebasepro/formex";
|
|
20
|
-
import { useCMSContext } from "../index";
|
|
21
20
|
|
|
22
21
|
export function EntityFormActions({
|
|
23
22
|
path,
|
|
@@ -34,17 +33,11 @@ export function EntityFormActions({
|
|
|
34
33
|
formContext
|
|
35
34
|
}: EntityFormActionsProps) {
|
|
36
35
|
|
|
37
|
-
const context = useCMSContext();
|
|
38
|
-
const sideEntityController = context.sideEntityController;
|
|
39
|
-
const { t } = useTranslation();
|
|
40
|
-
|
|
41
36
|
const bottomActionsProps = {
|
|
42
37
|
path,
|
|
43
38
|
savingError,
|
|
44
39
|
entity,
|
|
45
40
|
collection,
|
|
46
|
-
context,
|
|
47
|
-
sideEntityController,
|
|
48
41
|
disabled,
|
|
49
42
|
status,
|
|
50
43
|
pluginActions,
|
|
@@ -52,7 +45,6 @@ export function EntityFormActions({
|
|
|
52
45
|
navigateBack,
|
|
53
46
|
formContext,
|
|
54
47
|
formex,
|
|
55
|
-
t,
|
|
56
48
|
className: layout === "responsive" ? "@6xl:hidden" : undefined
|
|
57
49
|
};
|
|
58
50
|
|
|
@@ -61,8 +53,6 @@ export function EntityFormActions({
|
|
|
61
53
|
savingError,
|
|
62
54
|
entity,
|
|
63
55
|
collection,
|
|
64
|
-
context,
|
|
65
|
-
sideEntityController,
|
|
66
56
|
disabled,
|
|
67
57
|
status,
|
|
68
58
|
pluginActions,
|
|
@@ -70,7 +60,6 @@ export function EntityFormActions({
|
|
|
70
60
|
navigateBack,
|
|
71
61
|
formContext,
|
|
72
62
|
formex,
|
|
73
|
-
t,
|
|
74
63
|
className: layout === "responsive" ? "hidden @6xl:flex" : undefined
|
|
75
64
|
};
|
|
76
65
|
|
|
@@ -93,16 +82,13 @@ type ActionsViewProps<M extends Record<string, unknown>> = {
|
|
|
93
82
|
entity: Entity<M> | undefined,
|
|
94
83
|
formActions?: EntityAction[],
|
|
95
84
|
collection: EntityCollection,
|
|
96
|
-
context: RebaseContext,
|
|
97
|
-
sideEntityController: SideEntityController,
|
|
98
85
|
disabled: boolean,
|
|
99
86
|
status: "new" | "existing" | "copy",
|
|
100
87
|
pluginActions?: React.ReactNode[],
|
|
101
|
-
openEntityMode
|
|
88
|
+
openEntityMode?: "side_panel" | "full_screen" | "split" | "dialog";
|
|
102
89
|
navigateBack: () => void;
|
|
103
90
|
formContext: FormContext,
|
|
104
91
|
formex: FormexController<Record<string, unknown>>;
|
|
105
|
-
t: (key: string) => string;
|
|
106
92
|
className?: string;
|
|
107
93
|
};
|
|
108
94
|
|
|
@@ -112,8 +98,6 @@ function buildBottomActions<M extends Record<string, unknown>>({
|
|
|
112
98
|
path,
|
|
113
99
|
formActions,
|
|
114
100
|
collection,
|
|
115
|
-
context,
|
|
116
|
-
sideEntityController,
|
|
117
101
|
disabled,
|
|
118
102
|
status,
|
|
119
103
|
pluginActions,
|
|
@@ -121,7 +105,6 @@ function buildBottomActions<M extends Record<string, unknown>>({
|
|
|
121
105
|
navigateBack,
|
|
122
106
|
formContext,
|
|
123
107
|
formex,
|
|
124
|
-
t,
|
|
125
108
|
className
|
|
126
109
|
}: ActionsViewProps<M>) {
|
|
127
110
|
|
|
@@ -146,8 +129,8 @@ function buildBottomActions<M extends Record<string, unknown>>({
|
|
|
146
129
|
entity,
|
|
147
130
|
path: path ?? collection.slug,
|
|
148
131
|
collection: collection,
|
|
149
|
-
context,
|
|
150
|
-
sideEntityController,
|
|
132
|
+
context: undefined,
|
|
133
|
+
sideEntityController: undefined,
|
|
151
134
|
openEntityMode: openEntityMode,
|
|
152
135
|
navigateBack,
|
|
153
136
|
formContext
|
|
@@ -161,17 +144,20 @@ function buildBottomActions<M extends Record<string, unknown>>({
|
|
|
161
144
|
<Button variant="text" disabled={disabled || formex.isSubmitting}
|
|
162
145
|
color={"primary"}
|
|
163
146
|
type="reset">
|
|
164
|
-
{status === "existing" ?
|
|
165
|
-
</Button>
|
|
166
|
-
<Button variant={"filled"}
|
|
167
|
-
color="primary"
|
|
168
|
-
type="submit"
|
|
169
|
-
disabled={disabled || formex.isSubmitting}
|
|
170
|
-
startIcon={hasErrors ? <AlertCircleIcon/> : undefined}>
|
|
171
|
-
{status === "existing" && t("save")}
|
|
172
|
-
{status === "copy" && t("create_copy")}
|
|
173
|
-
{status === "new" && t("create")}
|
|
147
|
+
{status === "existing" ? "Discard" : "Clear"}
|
|
174
148
|
</Button>
|
|
149
|
+
<Tooltip title={hasErrors ? "Fix highlighted errors before saving" : undefined}>
|
|
150
|
+
<LoadingButton variant={"filled"}
|
|
151
|
+
color="primary"
|
|
152
|
+
type="submit"
|
|
153
|
+
loading={formex.isSubmitting}
|
|
154
|
+
disabled={disabled || formex.isSubmitting}
|
|
155
|
+
startIcon={hasErrors ? <AlertCircleIcon/> : undefined}>
|
|
156
|
+
{status === "existing" && "Save"}
|
|
157
|
+
{status === "copy" && "Create copy"}
|
|
158
|
+
{status === "new" && "Create"}
|
|
159
|
+
</LoadingButton>
|
|
160
|
+
</Tooltip>
|
|
175
161
|
|
|
176
162
|
</DialogActions>;
|
|
177
163
|
}
|
|
@@ -183,13 +169,10 @@ function buildSideActions<M extends Record<string, unknown>>({
|
|
|
183
169
|
path,
|
|
184
170
|
openEntityMode,
|
|
185
171
|
collection,
|
|
186
|
-
context,
|
|
187
|
-
sideEntityController,
|
|
188
172
|
disabled,
|
|
189
173
|
status,
|
|
190
174
|
pluginActions,
|
|
191
175
|
formex,
|
|
192
|
-
t,
|
|
193
176
|
className
|
|
194
177
|
}: ActionsViewProps<M>) {
|
|
195
178
|
|
|
@@ -197,18 +180,21 @@ function buildSideActions<M extends Record<string, unknown>>({
|
|
|
197
180
|
|
|
198
181
|
return <div
|
|
199
182
|
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, className)}>
|
|
200
|
-
<
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
183
|
+
<Tooltip title={hasErrors ? "Fix highlighted errors before saving" : undefined}>
|
|
184
|
+
<LoadingButton fullWidth={true}
|
|
185
|
+
variant="filled"
|
|
186
|
+
color="primary"
|
|
187
|
+
type="submit"
|
|
188
|
+
loading={formex.isSubmitting}
|
|
189
|
+
startIcon={hasErrors ? <AlertCircleIcon/> : undefined}
|
|
190
|
+
disabled={disabled || formex.isSubmitting}>
|
|
191
|
+
{status === "existing" && "Save"}
|
|
192
|
+
{status === "copy" && "Create copy"}
|
|
193
|
+
{status === "new" && "Create"}
|
|
194
|
+
</LoadingButton>
|
|
195
|
+
</Tooltip>
|
|
210
196
|
<Button fullWidth={true} variant="text" disabled={disabled || formex.isSubmitting} type="reset">
|
|
211
|
-
{status === "existing" ?
|
|
197
|
+
{status === "existing" ? "Discard" : "Clear"}
|
|
212
198
|
</Button>
|
|
213
199
|
|
|
214
200
|
{pluginActions}
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import type { EntityCollection, PluginFormActionProps } from "@rebasepro/types";
|
|
2
|
+
import type { FormContext } from "../types/fields";
|
|
3
|
+
import React, { useCallback, useMemo, useState } from "react";
|
|
4
|
+
import { AnalyticsEvent, Entity, EntityStatus, EntityValues } from "@rebasepro/types";
|
|
5
|
+
import type { EntityFormProps, OnUpdateParams } from "../types/components/EntityFormProps";
|
|
6
|
+
import type { EntityFormActionsProps } from "../types/components/EntityFormActionsProps";
|
|
7
|
+
|
|
8
|
+
import { getLocalChangesBackup } from "@rebasepro/common";
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
saveEntityWithCallbacks,
|
|
12
|
+
useAuthController,
|
|
13
|
+
useCustomizationController,
|
|
14
|
+
useData,
|
|
15
|
+
useSnackbarController,
|
|
16
|
+
useTranslation,
|
|
17
|
+
useSlot
|
|
18
|
+
} from "@rebasepro/core";
|
|
19
|
+
import { FormexController } from "@rebasepro/formex";
|
|
20
|
+
import { useAnalyticsController } from "@rebasepro/core";
|
|
21
|
+
|
|
22
|
+
import {
|
|
23
|
+
getEntityFromCache,
|
|
24
|
+
removeEntityFromCache,
|
|
25
|
+
removeEntityFromMemoryCache,
|
|
26
|
+
saveEntityToCache
|
|
27
|
+
} from "@rebasepro/core";
|
|
28
|
+
|
|
29
|
+
import { useCollectionRegistryController, useSideEntityController, useCMSContext } from "../index";
|
|
30
|
+
import { EntityForm } from "./EntityForm";
|
|
31
|
+
import {
|
|
32
|
+
extractTouchedValues,
|
|
33
|
+
removeEmptyContainers,
|
|
34
|
+
getChanges,
|
|
35
|
+
getInitialEntityValues,
|
|
36
|
+
} from "./form_utils";
|
|
37
|
+
import { mergeDeep } from "@rebasepro/utils";
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Props for the CMS-connected EntityFormBinding wrapper.
|
|
41
|
+
* Extends EntityFormProps — you can pass all the same props as EntityForm,
|
|
42
|
+
* but the binding provides CMS context (save, caching, analytics, plugin slots)
|
|
43
|
+
* automatically.
|
|
44
|
+
*/
|
|
45
|
+
export type EntityFormBindingProps<M extends Record<string, unknown>> = Omit<EntityFormProps<M>,
|
|
46
|
+
| "onSubmit"
|
|
47
|
+
| "onValuesChangeDeferred"
|
|
48
|
+
| "onReset"
|
|
49
|
+
| "uniqueFieldValidator"
|
|
50
|
+
| "beforeFields"
|
|
51
|
+
| "afterFields"
|
|
52
|
+
| "pluginActions"
|
|
53
|
+
| "computedInitialValues"
|
|
54
|
+
| "hasLocalChanges"
|
|
55
|
+
| "localChangesData"
|
|
56
|
+
| "manualApplyLocalChanges"
|
|
57
|
+
| "localChangesCacheKey"
|
|
58
|
+
| "onClearLocalChanges"
|
|
59
|
+
>;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* CMS-connected wrapper around EntityForm.
|
|
63
|
+
*
|
|
64
|
+
* Provides all backend concerns that the headless EntityForm delegates via callbacks:
|
|
65
|
+
* - Save via the data layer (`saveEntityWithCallbacks`)
|
|
66
|
+
* - Unique field validation via the data layer
|
|
67
|
+
* - Entity caching (local changes backup/restore)
|
|
68
|
+
* - Plugin slots (form.before, form.after, form.actions)
|
|
69
|
+
* - Snackbar notifications on save success/error
|
|
70
|
+
* - Analytics events
|
|
71
|
+
* - Navigation via side entity controller
|
|
72
|
+
*
|
|
73
|
+
* Use this component inside the CMS (`<RebaseShell>`).
|
|
74
|
+
* For headless usage outside the CMS, use `EntityForm` directly.
|
|
75
|
+
*
|
|
76
|
+
* @group Components
|
|
77
|
+
*/
|
|
78
|
+
export function EntityFormBinding<M extends Record<string, unknown>>({
|
|
79
|
+
path,
|
|
80
|
+
entityId: entityIdProp,
|
|
81
|
+
collection,
|
|
82
|
+
entity,
|
|
83
|
+
initialStatus,
|
|
84
|
+
onSaved,
|
|
85
|
+
onValuesModified,
|
|
86
|
+
onEntityChange,
|
|
87
|
+
onStatusChange,
|
|
88
|
+
openEntityMode = "full_screen",
|
|
89
|
+
navigateBack: navigateBackProp,
|
|
90
|
+
initialDirtyValues,
|
|
91
|
+
...restProps
|
|
92
|
+
}: EntityFormBindingProps<M>) {
|
|
93
|
+
|
|
94
|
+
const { t } = useTranslation();
|
|
95
|
+
const sideEntityController = useSideEntityController();
|
|
96
|
+
const collectionRegistryController = useCollectionRegistryController();
|
|
97
|
+
const authController = useAuthController();
|
|
98
|
+
const dataClient = useData();
|
|
99
|
+
const snackbarController = useSnackbarController();
|
|
100
|
+
const customizationController = useCustomizationController();
|
|
101
|
+
const context = useCMSContext();
|
|
102
|
+
const analyticsController = useAnalyticsController();
|
|
103
|
+
|
|
104
|
+
const [status, setStatus] = useState<EntityStatus>(initialStatus);
|
|
105
|
+
const [entityId, setEntityId] = useState<string | number | undefined>(() => {
|
|
106
|
+
if (status === "new" || status === "copy") return undefined;
|
|
107
|
+
return entityIdProp;
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// --- Compute initial values with local changes ---
|
|
111
|
+
const baseInitialValues = useMemo(
|
|
112
|
+
() => getInitialEntityValues(authController, collection, path, status, entity, customizationController.propertyConfigs),
|
|
113
|
+
[authController, collection, path, status, entity, customizationController.propertyConfigs]
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
const localChangesBackup = getLocalChangesBackup(collection);
|
|
117
|
+
const autoApplyLocalChanges = localChangesBackup === "auto_apply";
|
|
118
|
+
const manualApplyLocalChanges = localChangesBackup === "manual_apply";
|
|
119
|
+
|
|
120
|
+
const localChangesDataRaw = useMemo(() => entityId
|
|
121
|
+
? getEntityFromCache(path + "/" + entityId)
|
|
122
|
+
: getEntityFromCache(path + "#new"), [entityId, path]);
|
|
123
|
+
|
|
124
|
+
const [localChangesCleared, setLocalChangesCleared] = useState<boolean>(false);
|
|
125
|
+
|
|
126
|
+
const computedInitialValues = useMemo(() => {
|
|
127
|
+
const withLocalChanges = autoApplyLocalChanges && localChangesDataRaw
|
|
128
|
+
? mergeDeep(baseInitialValues, localChangesDataRaw as Partial<M>)
|
|
129
|
+
: baseInitialValues;
|
|
130
|
+
return initialDirtyValues ? mergeDeep(withLocalChanges, initialDirtyValues) : withLocalChanges;
|
|
131
|
+
}, [autoApplyLocalChanges, localChangesDataRaw, baseInitialValues, initialDirtyValues]);
|
|
132
|
+
|
|
133
|
+
const localChangesData = useMemo(() => {
|
|
134
|
+
if (!localChangesDataRaw) return undefined;
|
|
135
|
+
const changes = getChanges(localChangesDataRaw, computedInitialValues);
|
|
136
|
+
const cleaned = removeEmptyContainers(changes);
|
|
137
|
+
if (cleaned && typeof cleaned === "object" && Object.keys(cleaned).length === 0) {
|
|
138
|
+
return undefined;
|
|
139
|
+
}
|
|
140
|
+
return cleaned;
|
|
141
|
+
}, [localChangesDataRaw, computedInitialValues]);
|
|
142
|
+
|
|
143
|
+
const hasLocalChanges = !localChangesCleared && !!localChangesData && Object.keys(localChangesData as object).length > 0;
|
|
144
|
+
|
|
145
|
+
const localChangesCacheKey = useMemo(() => {
|
|
146
|
+
return (status === "new" || status === "copy") ? path + "#new" : path + "/" + entityId;
|
|
147
|
+
}, [status, path, entityId]);
|
|
148
|
+
|
|
149
|
+
// --- Navigate back ---
|
|
150
|
+
const navigateBack = useCallback(() => {
|
|
151
|
+
if (navigateBackProp) {
|
|
152
|
+
navigateBackProp();
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
if (openEntityMode === "side_panel" || openEntityMode === "dialog") {
|
|
156
|
+
sideEntityController.close();
|
|
157
|
+
} else {
|
|
158
|
+
window.history.back();
|
|
159
|
+
}
|
|
160
|
+
}, [navigateBackProp, openEntityMode, sideEntityController]);
|
|
161
|
+
|
|
162
|
+
// --- Cache operations ---
|
|
163
|
+
const clearDirtyCache = useCallback(() => {
|
|
164
|
+
if (status === "new" || status === "copy") {
|
|
165
|
+
removeEntityFromMemoryCache(path + "#new");
|
|
166
|
+
removeEntityFromCache(path + "#new");
|
|
167
|
+
} else {
|
|
168
|
+
removeEntityFromMemoryCache(path + "/" + entityId);
|
|
169
|
+
removeEntityFromCache(path + "/" + entityId);
|
|
170
|
+
}
|
|
171
|
+
}, [status, path, entityId]);
|
|
172
|
+
|
|
173
|
+
const onReset = useCallback(() => {
|
|
174
|
+
clearDirtyCache();
|
|
175
|
+
}, [clearDirtyCache]);
|
|
176
|
+
|
|
177
|
+
const onValuesChangeDeferred = useCallback((values: M, controller: FormexController<M>) => {
|
|
178
|
+
const key = (status === "new" || status === "copy") ? path + "#new" : path + "/" + entityId;
|
|
179
|
+
if (controller.dirty) {
|
|
180
|
+
const touchedValues = removeEmptyContainers(extractTouchedValues(values, controller.touched));
|
|
181
|
+
if (touchedValues && Object.keys(touchedValues).length > 0) {
|
|
182
|
+
saveEntityToCache(key, touchedValues);
|
|
183
|
+
} else {
|
|
184
|
+
removeEntityFromCache(key);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}, [status, path, entityId]);
|
|
188
|
+
|
|
189
|
+
// --- Save via data layer ---
|
|
190
|
+
const onSubmit = useCallback(async (values: M, formex: FormexController<M>): Promise<Entity<M> | void> => {
|
|
191
|
+
try {
|
|
192
|
+
const savedEntity = await saveEntityWithCallbacks<M>({
|
|
193
|
+
path,
|
|
194
|
+
entityId,
|
|
195
|
+
values,
|
|
196
|
+
previousValues: entity?.values,
|
|
197
|
+
collection,
|
|
198
|
+
status,
|
|
199
|
+
data: dataClient,
|
|
200
|
+
context,
|
|
201
|
+
afterSave: (updatedEntity: Entity<M>) => {
|
|
202
|
+
clearDirtyCache();
|
|
203
|
+
onValuesModified?.(false, updatedEntity.values);
|
|
204
|
+
onEntityChange?.(updatedEntity);
|
|
205
|
+
setStatus("existing");
|
|
206
|
+
setEntityId(updatedEntity.id);
|
|
207
|
+
onStatusChange?.("existing");
|
|
208
|
+
|
|
209
|
+
if (onSaved) {
|
|
210
|
+
onSaved({
|
|
211
|
+
entity: updatedEntity,
|
|
212
|
+
status,
|
|
213
|
+
path,
|
|
214
|
+
entityId: updatedEntity.id,
|
|
215
|
+
collection
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
},
|
|
219
|
+
afterSaveError: (e: Error) => {
|
|
220
|
+
snackbarController.open({
|
|
221
|
+
type: "error",
|
|
222
|
+
title: t("error_saving_entity"),
|
|
223
|
+
message: e?.message
|
|
224
|
+
});
|
|
225
|
+
console.error("Error saving entity", path, entityId, e);
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
const autoSave = collection.formAutoSave;
|
|
230
|
+
if (!autoSave) {
|
|
231
|
+
snackbarController.open({
|
|
232
|
+
type: "success",
|
|
233
|
+
message: `${collection.singularName ?? collection.name}: ${t("saved_correctly")}`
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const eventName: AnalyticsEvent = status === "new"
|
|
238
|
+
? "new_entity_saved"
|
|
239
|
+
: (status === "copy" ? "entity_copied" : (status === "existing" ? "entity_edited" : "unmapped_event"));
|
|
240
|
+
analyticsController.onAnalyticsEvent?.(eventName, { path });
|
|
241
|
+
|
|
242
|
+
return savedEntity;
|
|
243
|
+
} catch (e: unknown) {
|
|
244
|
+
console.error(e);
|
|
245
|
+
throw e;
|
|
246
|
+
}
|
|
247
|
+
}, [path, entityId, entity, collection, status, dataClient, context, clearDirtyCache, onValuesModified, onEntityChange, onSaved, onStatusChange, snackbarController, analyticsController, t]);
|
|
248
|
+
|
|
249
|
+
// --- Unique field validation via data layer ---
|
|
250
|
+
const uniqueFieldValidator = useCallback(async ({
|
|
251
|
+
name,
|
|
252
|
+
value
|
|
253
|
+
}: { name: string; value: unknown }) => {
|
|
254
|
+
try {
|
|
255
|
+
const accessor = dataClient.collection(path);
|
|
256
|
+
const { data } = await accessor.find({
|
|
257
|
+
where: { [name]: `eq.${value}` },
|
|
258
|
+
limit: 2
|
|
259
|
+
});
|
|
260
|
+
const otherEntities = entityId ? data.filter(e => e.id !== entityId) : data;
|
|
261
|
+
return otherEntities.length === 0;
|
|
262
|
+
} catch (e: unknown) {
|
|
263
|
+
console.error("Error checking unique field", e);
|
|
264
|
+
return true;
|
|
265
|
+
}
|
|
266
|
+
}, [dataClient, path, entityId]);
|
|
267
|
+
|
|
268
|
+
// --- Plugin slots ---
|
|
269
|
+
const parentCollectionSlugs = collectionRegistryController.getParentCollectionSlugs(path);
|
|
270
|
+
const parentEntityIds = collectionRegistryController.getParentEntityIds(path);
|
|
271
|
+
|
|
272
|
+
// Build a minimal formContext for plugin slot props — the full one is built inside EntityForm
|
|
273
|
+
const formActionProps: PluginFormActionProps = useMemo(() => ({
|
|
274
|
+
entityId,
|
|
275
|
+
parentCollectionSlugs,
|
|
276
|
+
parentEntityIds,
|
|
277
|
+
path,
|
|
278
|
+
status,
|
|
279
|
+
collection: collection as EntityCollection,
|
|
280
|
+
context,
|
|
281
|
+
formContext: undefined as unknown as FormContext<Record<string, unknown>>,
|
|
282
|
+
openEntityMode,
|
|
283
|
+
disabled: false
|
|
284
|
+
}), [entityId, parentCollectionSlugs, parentEntityIds, path, status, collection, context, openEntityMode]);
|
|
285
|
+
|
|
286
|
+
const pluginFormActions = useSlot("form.actions", formActionProps);
|
|
287
|
+
const pluginFormBefore = useSlot("form.before", formActionProps);
|
|
288
|
+
const pluginFormAfter = useSlot("form.after", formActionProps);
|
|
289
|
+
|
|
290
|
+
return (
|
|
291
|
+
<EntityForm<M>
|
|
292
|
+
{...restProps}
|
|
293
|
+
path={path}
|
|
294
|
+
entityId={entityIdProp}
|
|
295
|
+
collection={collection}
|
|
296
|
+
entity={entity}
|
|
297
|
+
initialStatus={initialStatus}
|
|
298
|
+
initialDirtyValues={initialDirtyValues}
|
|
299
|
+
onSaved={onSaved}
|
|
300
|
+
onValuesModified={onValuesModified}
|
|
301
|
+
onEntityChange={onEntityChange}
|
|
302
|
+
onStatusChange={onStatusChange}
|
|
303
|
+
openEntityMode={openEntityMode}
|
|
304
|
+
navigateBack={navigateBack}
|
|
305
|
+
// Headless callbacks
|
|
306
|
+
onSubmit={onSubmit}
|
|
307
|
+
onValuesChangeDeferred={onValuesChangeDeferred}
|
|
308
|
+
onReset={onReset}
|
|
309
|
+
uniqueFieldValidator={uniqueFieldValidator}
|
|
310
|
+
// Slots
|
|
311
|
+
beforeFields={pluginFormBefore}
|
|
312
|
+
afterFields={pluginFormAfter}
|
|
313
|
+
pluginActions={pluginFormActions ?? []}
|
|
314
|
+
// Local changes
|
|
315
|
+
computedInitialValues={computedInitialValues as Partial<M>}
|
|
316
|
+
hasLocalChanges={hasLocalChanges}
|
|
317
|
+
localChangesData={localChangesData as Partial<M>}
|
|
318
|
+
manualApplyLocalChanges={manualApplyLocalChanges}
|
|
319
|
+
localChangesCacheKey={localChangesCacheKey}
|
|
320
|
+
onClearLocalChanges={() => setLocalChangesCleared(true)}
|
|
321
|
+
/>
|
|
322
|
+
);
|
|
323
|
+
}
|
|
@@ -8,7 +8,7 @@ import { PropertyPreview } from "../../preview";
|
|
|
8
8
|
import { FieldHelperText, LabelWithIconAndTooltip } from "../components";
|
|
9
9
|
import { ErrorBoundary } from "@rebasepro/ui";
|
|
10
10
|
import { getIconForProperty } from "../../util/property_utils";
|
|
11
|
-
import { cls
|
|
11
|
+
import { cls } from "@rebasepro/ui";
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
*
|
|
@@ -47,7 +47,12 @@ export function ReadOnlyFieldBinding({
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
<div
|
|
50
|
-
className={cls(
|
|
50
|
+
className={cls(
|
|
51
|
+
"w-full overflow-x-scroll no-scrollbar flex items-center",
|
|
52
|
+
skipCardWrapper
|
|
53
|
+
? ""
|
|
54
|
+
: "rounded-lg border border-surface-200 dark:border-surface-700 px-4 md:px-6 min-h-10 opacity-80"
|
|
55
|
+
)}>
|
|
51
56
|
|
|
52
57
|
<ErrorBoundary>
|
|
53
58
|
<PropertyPreview propertyKey={propertyKey}
|