@rebasepro/admin 0.6.0 → 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.
Files changed (149) hide show
  1. package/README.md +7 -3
  2. package/dist/{CollectionEditorDialog-jA-PLh6N.js → CollectionEditorDialog-iGgS4rLX.js} +233 -152
  3. package/dist/CollectionEditorDialog-iGgS4rLX.js.map +1 -0
  4. package/dist/{PropertyEditView-DEUddmg_.js → PropertyEditView-C9yhLyja.js} +129 -15
  5. package/dist/PropertyEditView-C9yhLyja.js.map +1 -0
  6. package/dist/{CollectionsStudioView-UKouBPOm.js → RouterCollectionsStudioView-Dqx27GD8.js} +61 -39
  7. package/dist/RouterCollectionsStudioView-Dqx27GD8.js.map +1 -0
  8. package/dist/collection_editor/extensibility_types.d.ts +135 -0
  9. package/dist/collection_editor/index.d.ts +6 -0
  10. package/dist/collection_editor/serializable_types.d.ts +374 -0
  11. package/dist/collection_editor/serializable_utils.d.ts +50 -0
  12. package/dist/collection_editor/ui/collection_editor/CollectionEditorDialog.d.ts +2 -1
  13. package/dist/collection_editor/ui/collection_editor/CollectionPropertiesEditorForm.d.ts +12 -1
  14. package/dist/collection_editor/ui/collection_editor/CollectionStudioView.d.ts +13 -1
  15. package/dist/collection_editor/ui/collection_editor/CollectionsStudioView.d.ts +24 -2
  16. package/dist/collection_editor/ui/collection_editor/DisplaySettingsForm.d.ts +2 -1
  17. package/dist/collection_editor/ui/collection_editor/GeneralSettingsForm.d.ts +4 -1
  18. package/dist/collection_editor/ui/collection_editor/PropertyEditView.d.ts +12 -1
  19. package/dist/collection_editor/ui/collection_editor/RouterCollectionStudioView.d.ts +15 -0
  20. package/dist/collection_editor/ui/collection_editor/RouterCollectionsStudioView.d.ts +14 -0
  21. package/dist/collection_editor/useJsonCollectionsConfigController.d.ts +37 -0
  22. package/dist/collection_editor/useSafeSnackbarController.d.ts +2 -0
  23. package/dist/collection_editor_ui.d.ts +4 -0
  24. package/dist/collection_editor_ui.js +33 -4
  25. package/dist/collection_editor_ui.js.map +1 -0
  26. package/dist/components/EntityCollectionView/EntityBoardCard.d.ts +3 -3
  27. package/dist/components/EntityCollectionView/hooks/useKanbanDragAndDrop.d.ts +3 -3
  28. package/dist/components/EntityEditView.d.ts +3 -2
  29. package/dist/components/RebaseCMS.d.ts +1 -1
  30. package/dist/editor/editor.d.ts +10 -4
  31. package/dist/editor.js +6 -4
  32. package/dist/editor.js.map +1 -1
  33. package/dist/form/EntityForm.d.ts +10 -9
  34. package/dist/form/EntityFormBinding.d.ts +27 -0
  35. package/dist/form/field_bindings/MarkdownEditorFieldBinding.d.ts +2 -2
  36. package/dist/form/form_utils.d.ts +8 -0
  37. package/dist/form/index.d.ts +5 -1
  38. package/dist/{history-BL49TIDb.js → history-ChHgyyvM.js} +2 -2
  39. package/dist/{history-BL49TIDb.js.map → history-ChHgyyvM.js.map} +1 -1
  40. package/dist/hooks/navigation/contexts/CollectionRegistryContext.d.ts +1 -1
  41. package/dist/hooks/navigation/useAsyncResolver.d.ts +26 -0
  42. package/dist/hooks/navigation/useBuildCollectionRegistryController.d.ts +2 -0
  43. package/dist/hooks/navigation/useTopLevelNavigation.d.ts +1 -1
  44. package/dist/hooks/navigation/utils.d.ts +1 -1
  45. package/dist/index.d.ts +3 -1
  46. package/dist/index.js +2 -2
  47. package/dist/routes/CustomViewRoute.d.ts +2 -1
  48. package/dist/{src-BJ8fkM8y.js → src-DOX2nOyh.js} +1634 -1330
  49. package/dist/src-DOX2nOyh.js.map +1 -0
  50. package/dist/types/components/EntityFormActionsProps.d.ts +1 -1
  51. package/dist/types/components/EntityFormProps.d.ts +59 -0
  52. package/dist/types/fields.d.ts +6 -1
  53. package/dist/util/dataControllerAdapter.d.ts +30 -0
  54. package/dist/util/navigation_utils.d.ts +1 -1
  55. package/dist/util/propertyConfigMapper.d.ts +17 -0
  56. package/package.json +18 -18
  57. package/src/collection_editor/extensibility_types.ts +168 -0
  58. package/src/collection_editor/index.ts +46 -0
  59. package/src/collection_editor/serializable_types.ts +487 -0
  60. package/src/collection_editor/serializable_utils.ts +588 -0
  61. package/src/collection_editor/ui/AddKanbanColumnAction.tsx +2 -0
  62. package/src/collection_editor/ui/collection_editor/AICollectionGeneratorPopover.tsx +4 -4
  63. package/src/collection_editor/ui/collection_editor/CollectionEditorDialog.tsx +134 -70
  64. package/src/collection_editor/ui/collection_editor/CollectionPropertiesEditorForm.tsx +95 -78
  65. package/src/collection_editor/ui/collection_editor/CollectionStudioView.tsx +23 -20
  66. package/src/collection_editor/ui/collection_editor/CollectionsStudioView.tsx +79 -27
  67. package/src/collection_editor/ui/collection_editor/DisplaySettingsForm.tsx +9 -7
  68. package/src/collection_editor/ui/collection_editor/GeneralSettingsForm.tsx +22 -5
  69. package/src/collection_editor/ui/collection_editor/GetCodeDialog.tsx +3 -3
  70. package/src/collection_editor/ui/collection_editor/PropertyEditView.tsx +155 -10
  71. package/src/collection_editor/ui/collection_editor/PropertyFieldPreview.tsx +6 -6
  72. package/src/collection_editor/ui/collection_editor/PropertyTree.tsx +1 -0
  73. package/src/collection_editor/ui/collection_editor/RouterCollectionStudioView.tsx +44 -0
  74. package/src/collection_editor/ui/collection_editor/RouterCollectionsStudioView.tsx +41 -0
  75. package/src/collection_editor/ui/collection_editor/properties/EnumPropertyField.tsx +4 -3
  76. package/src/collection_editor/useJsonCollectionsConfigController.tsx +286 -0
  77. package/src/collection_editor/useSafeSnackbarController.ts +9 -0
  78. package/src/collection_editor_ui.ts +6 -0
  79. package/src/components/ArrayContainer.tsx +5 -5
  80. package/src/components/DefaultDrawer.tsx +13 -3
  81. package/src/components/DrawerNavigationGroup.tsx +2 -2
  82. package/src/components/DrawerNavigationItem.tsx +4 -4
  83. package/src/components/EntityCollectionView/EntityBoardCard.tsx +6 -6
  84. package/src/components/EntityCollectionView/EntityCollectionBoardView.tsx +14 -11
  85. package/src/components/EntityCollectionView/EntityCollectionCardView.tsx +40 -190
  86. package/src/components/EntityCollectionView/EntityCollectionListView.tsx +54 -183
  87. package/src/components/EntityCollectionView/hooks/useKanbanDragAndDrop.ts +6 -6
  88. package/src/components/EntityEditView.tsx +5 -4
  89. package/src/components/EntityEditViewFormActions.tsx +32 -26
  90. package/src/components/HomePage/ContentHomePage.tsx +13 -4
  91. package/src/components/HomePage/NavigationCard.tsx +8 -14
  92. package/src/components/HomePage/NavigationGroup.tsx +1 -1
  93. package/src/components/HomePage/SmallNavigationCard.tsx +1 -5
  94. package/src/components/RebaseCMS.tsx +3 -2
  95. package/src/components/RebaseNavigation.tsx +71 -7
  96. package/src/components/SelectableTable/SelectableTable.tsx +2 -2
  97. package/src/components/SelectableTable/filters/StringNumberFilterField.tsx +28 -21
  98. package/src/components/SideDialogs.tsx +0 -7
  99. package/src/components/common/default_entity_actions.tsx +18 -10
  100. package/src/contexts/BreacrumbsContext.tsx +15 -6
  101. package/src/editor/editor.tsx +14 -8
  102. package/src/editor/selectors/ai-selector.tsx +0 -1
  103. package/src/form/EntityForm.tsx +191 -428
  104. package/src/form/EntityFormActions.tsx +33 -47
  105. package/src/form/EntityFormBinding.tsx +323 -0
  106. package/src/form/components/FormLayout.tsx +1 -1
  107. package/src/form/field_bindings/MarkdownEditorFieldBinding.tsx +4 -4
  108. package/src/form/field_bindings/ReadOnlyFieldBinding.tsx +7 -2
  109. package/src/form/field_bindings/StorageUploadFieldBinding.tsx +1 -1
  110. package/src/form/form_utils.ts +163 -0
  111. package/src/form/index.tsx +14 -5
  112. package/src/hooks/navigation/contexts/CollectionRegistryContext.tsx +2 -2
  113. package/src/hooks/navigation/contexts/NavigationStateContext.tsx +2 -2
  114. package/src/hooks/navigation/contexts/UrlContext.tsx +1 -2
  115. package/src/hooks/navigation/useAsyncResolver.ts +100 -0
  116. package/src/hooks/navigation/useBuildCollectionRegistryController.tsx +15 -8
  117. package/src/hooks/navigation/useBuildNavigationStateController.tsx +12 -8
  118. package/src/hooks/navigation/useNavigationResolution.ts +33 -0
  119. package/src/hooks/navigation/useResolvedCollections.ts +29 -71
  120. package/src/hooks/navigation/useResolvedViews.tsx +40 -70
  121. package/src/hooks/navigation/useTopLevelNavigation.ts +20 -16
  122. package/src/hooks/navigation/utils.ts +7 -4
  123. package/src/index.ts +7 -2
  124. package/src/routes/CustomViewRoute.tsx +14 -6
  125. package/src/types/components/EntityFormActionsProps.tsx +1 -1
  126. package/src/types/components/EntityFormProps.tsx +68 -0
  127. package/src/types/fields.tsx +7 -1
  128. package/src/util/dataControllerAdapter.ts +93 -0
  129. package/src/util/navigation_utils.ts +1 -1
  130. package/src/util/propertyConfigMapper.ts +134 -0
  131. package/LICENSE +0 -21
  132. package/dist/CollectionEditorDialog-jA-PLh6N.js.map +0 -1
  133. package/dist/CollectionsStudioView-UKouBPOm.js.map +0 -1
  134. package/dist/PropertyEditView-DEUddmg_.js.map +0 -1
  135. package/dist/components/EntityCollectionView/Board.d.ts +0 -3
  136. package/dist/components/EntityCollectionView/BoardColumn.d.ts +0 -42
  137. package/dist/components/EntityCollectionView/BoardColumnTitle.d.ts +0 -9
  138. package/dist/components/EntityCollectionView/BoardSortableList.d.ts +0 -14
  139. package/dist/components/EntityCollectionView/board_types.d.ts +0 -105
  140. package/dist/hooks/navigation/useNavigationRegistry.d.ts +0 -12
  141. package/dist/hooks/navigation/useNavigationURLs.d.ts +0 -11
  142. package/dist/src-BJ8fkM8y.js.map +0 -1
  143. package/src/components/EntityCollectionView/Board.tsx +0 -475
  144. package/src/components/EntityCollectionView/BoardColumn.tsx +0 -165
  145. package/src/components/EntityCollectionView/BoardColumnTitle.tsx +0 -46
  146. package/src/components/EntityCollectionView/BoardSortableList.tsx +0 -177
  147. package/src/components/EntityCollectionView/board_types.ts +0 -113
  148. package/src/hooks/navigation/useNavigationRegistry.ts +0 -164
  149. 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, SideEntityController } from "@rebasepro/types";
4
+ import type { EntityAction } from "@rebasepro/types";
5
5
  import React from "react";
6
- import { Entity, RebaseContext } from "@rebasepro/types";
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: "side_panel" | "full_screen" | "split" | "dialog";
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" ? t("discard") : t("clear")}
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
- <LoadingButton fullWidth={true}
201
- variant="filled"
202
- color="primary"
203
- type="submit"
204
- startIcon={hasErrors ? <AlertCircleIcon/> : undefined}
205
- disabled={disabled || formex.isSubmitting}>
206
- {status === "existing" && t("save")}
207
- {status === "copy" && t("create_copy")}
208
- {status === "new" && t("create")}
209
- </LoadingButton>
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" ? t("discard") : t("clear")}
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
+ }
@@ -9,7 +9,7 @@ export function FormLayout({
9
9
  className?: string;
10
10
  }) {
11
11
  return (
12
- <div className={cls("flex flex-wrap gap-x-4 w-full space-y-8", className)}>
12
+ <div className={cls("flex flex-wrap gap-x-4 w-full space-y-6", className)}>
13
13
  {children}
14
14
  </div>
15
15
  );
@@ -13,13 +13,13 @@ import {
13
13
  Skeleton,
14
14
  XIcon
15
15
  } from "@rebasepro/ui";
16
- import type { RebaseEditorProps } from "../../editor";
16
+ import type { RichTextEditorProps } from "../../editor";
17
17
  import { resolveStorageFilenameString, resolveStoragePathString } from "@rebasepro/common";
18
18
  import { randomString } from "@rebasepro/utils";
19
19
 
20
20
  // Lazy-load ProseMirror editor + markdown parser/serializer (~300KB)
21
21
  // Only fetched when a markdown field is actually rendered.
22
- const RebaseEditor = lazy(() => import("../../editor").then(m => ({ default: m.RebaseEditor })));
22
+ const RichTextEditor = lazy(() => import("../../editor").then(m => ({ default: m.RichTextEditor })));
23
23
  const loadMarkdownUtils = () => import("../../editor/markdown");
24
24
  let _markdownUtils: Awaited<ReturnType<typeof loadMarkdownUtils>> | null = null;
25
25
  const getMarkdownUtils = async () => {
@@ -29,7 +29,7 @@ const getMarkdownUtils = async () => {
29
29
 
30
30
  interface MarkdownEditorFieldProps {
31
31
  highlight?: { from: number, to: number };
32
- editorProps?: Partial<RebaseEditorProps>
32
+ editorProps?: Partial<RichTextEditorProps>
33
33
  }
34
34
 
35
35
  /**
@@ -173,7 +173,7 @@ export function MarkdownEditorFieldBinding({
173
173
  }, [entityId, entityValues, path, property, propertyKey, storage]);
174
174
 
175
175
  const editor = <Suspense fallback={<Skeleton height={200} className="w-full rounded-md"/>}>
176
- <RebaseEditor
176
+ <RichTextEditor
177
177
  content={value}
178
178
  onMarkdownContentChange={onContentChange}
179
179
  version={context.formex.version + fieldVersion}
@@ -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, paperMixin } from "@rebasepro/ui";
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(!skipCardWrapper && paperMixin, "w-full min-h-14 overflow-x-scroll no-scrollbar", !skipCardWrapper && "p-4 md:p-6")}>
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}
@@ -481,7 +481,7 @@ export function StorageUpload({
481
481
  onDragStart={handleDragStart}
482
482
  onDragEnd={handleDragEnd}
483
483
  >
484
- <SortableContext items={value.map(v => v.id)} strategy={horizontalListSortingStrategy}>
484
+ <SortableContext items={(value ?? []).map(v => v.id)} strategy={horizontalListSortingStrategy}>
485
485
  <FileDropComponent {...fileDropProps}/>
486
486
  </SortableContext>
487
487
  </DndContext>