@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
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
import { useEntitySelectionDialog } from "../../hooks/useEntitySelectionDialog";
|
|
2
2
|
import type { FieldProps } from "../../types/fields";
|
|
3
3
|
import type { RelationProperty } from "@rebasepro/types";
|
|
4
|
+
import { Entity, getCollectionDataPath, getDataSourceCapabilities, Relation } from "@rebasepro/types";
|
|
4
5
|
import React, { useCallback } from "react";
|
|
5
|
-
import { Entity, EntityRelation, getDataSourceCapabilities, Relation } from "@rebasepro/types";
|
|
6
6
|
import { FieldHelperText, LabelWithIconAndTooltip } from "../components";
|
|
7
7
|
import { EntityPreviewContainer } from "../../components/EntityPreview";
|
|
8
|
-
import { IconForView } from "@rebasepro/core";
|
|
8
|
+
import { ErrorView, IconForView } from "@rebasepro/core";
|
|
9
9
|
import { getIconForProperty } from "../../util/property_utils";
|
|
10
|
-
import { getRelationFrom,
|
|
11
|
-
|
|
12
|
-
import { ErrorView } from "@rebasepro/core";
|
|
10
|
+
import { getRelationFrom, normalizeToEntityRelation, resolveRelationProperty } from "@rebasepro/common";
|
|
13
11
|
import { cls } from "@rebasepro/ui";
|
|
14
12
|
import { RelationPreview } from "../../preview";
|
|
15
13
|
import { RelationSelector } from "../../components/RelationSelector";
|
|
@@ -22,7 +20,7 @@ export function RelationFieldBinding(props: FieldProps<RelationProperty>) {
|
|
|
22
20
|
throw Error("RelationFieldBinding expected a property containing a relation");
|
|
23
21
|
}
|
|
24
22
|
const collection = context.collection;
|
|
25
|
-
const capabilities = collection ? getDataSourceCapabilities(collection.
|
|
23
|
+
const capabilities = collection ? getDataSourceCapabilities(collection.engine) : undefined;
|
|
26
24
|
if (!collection || !capabilities?.supportsRelations || !("relations" in collection) || !collection.relations) {
|
|
27
25
|
throw Error("RelationFieldBinding expected a collection with relations support");
|
|
28
26
|
}
|
|
@@ -124,7 +122,7 @@ function SingleRelationFieldBinding({
|
|
|
124
122
|
|
|
125
123
|
const referenceDialogController = useEntitySelectionDialog({
|
|
126
124
|
multiselect: false,
|
|
127
|
-
path: collection
|
|
125
|
+
path: getCollectionDataPath(collection),
|
|
128
126
|
collection,
|
|
129
127
|
onSingleEntitySelected,
|
|
130
128
|
selectedEntityIds: validValue && normalizedValue ? [normalizedValue.id] : undefined,
|
|
@@ -10,6 +10,7 @@ import { FieldHelperText, LabelWithIconAndTooltip } from "../components";
|
|
|
10
10
|
import { isReadOnly } from "@rebasepro/common";
|
|
11
11
|
import { getIconForProperty } from "../../util/property_utils";
|
|
12
12
|
import { useAuthController, useSnackbarController, useStorageSource, useTranslation } from "@rebasepro/core";
|
|
13
|
+
import { StorageSourceContext } from "@rebasepro/core";
|
|
13
14
|
import {
|
|
14
15
|
closestCenter,
|
|
15
16
|
DndContext,
|
|
@@ -62,7 +63,7 @@ export function StorageUploadFieldBinding({
|
|
|
62
63
|
|
|
63
64
|
const authController = useAuthController();
|
|
64
65
|
|
|
65
|
-
const storageSource = useStorageSource(
|
|
66
|
+
const storageSource = useStorageSource();
|
|
66
67
|
const disabled = isReadOnly(property) || !!property.ui?.disabled || isSubmitting || context.disabled;
|
|
67
68
|
|
|
68
69
|
const {
|
|
@@ -72,7 +73,8 @@ export function StorageUploadFieldBinding({
|
|
|
72
73
|
storage,
|
|
73
74
|
onFileUploadComplete,
|
|
74
75
|
storagePathBuilder,
|
|
75
|
-
multipleFilesSupported
|
|
76
|
+
multipleFilesSupported,
|
|
77
|
+
resolvedStorageSource
|
|
76
78
|
} = useStorageUploadController({
|
|
77
79
|
entityValues: context.values,
|
|
78
80
|
entityId: context.entityId,
|
|
@@ -103,19 +105,21 @@ export function StorageUploadFieldBinding({
|
|
|
103
105
|
title={property.name ?? propertyKey}
|
|
104
106
|
className={"h-8 text-text-secondary dark:text-text-secondary-dark ml-3.5"}/>}
|
|
105
107
|
|
|
106
|
-
<
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
108
|
+
<StorageSourceContext.Provider value={resolvedStorageSource}>
|
|
109
|
+
<StorageUpload
|
|
110
|
+
value={internalValue}
|
|
111
|
+
name={propertyKey}
|
|
112
|
+
disabled={disabled ?? false}
|
|
113
|
+
autoFocus={autoFocus ?? false}
|
|
114
|
+
property={property}
|
|
115
|
+
onChange={setValue}
|
|
116
|
+
setInternalValue={setInternalValue}
|
|
117
|
+
onFilesAdded={onFilesAdded}
|
|
118
|
+
onFileUploadComplete={onFileUploadComplete}
|
|
119
|
+
storagePathBuilder={storagePathBuilder}
|
|
120
|
+
storage={storage}
|
|
121
|
+
multipleFilesSupported={multipleFilesSupported}/>
|
|
122
|
+
</StorageSourceContext.Provider>
|
|
119
123
|
|
|
120
124
|
<FieldHelperText includeDescription={includeDescription}
|
|
121
125
|
showError={showError}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import type { EntityCollection, PropertyConfig } from "@rebasepro/types";
|
|
2
|
+
import type { AuthController, Entity, EntityStatus, EntityValues } from "@rebasepro/types";
|
|
3
|
+
import { deepEqual as equal } from "fast-equals";
|
|
4
|
+
import { getIn, setIn } from "@rebasepro/formex";
|
|
5
|
+
import { getDefaultValuesFor } from "@rebasepro/common";
|
|
6
|
+
import { isObject, mergeDeep } from "@rebasepro/utils";
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
|
|
9
|
+
// extract touched values for nested touched trees and map to current values
|
|
10
|
+
export function extractTouchedValues(values: unknown, touched: Record<string, boolean>): Record<string, unknown> {
|
|
11
|
+
let acc: Record<string, unknown> = {};
|
|
12
|
+
if (!touched || typeof touched !== "object") {
|
|
13
|
+
return acc;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
Object.entries(touched).forEach(([key, value]) => {
|
|
17
|
+
if (value) {
|
|
18
|
+
acc = setIn(acc, key, getIn(values, key)) as Record<string, unknown>;
|
|
19
|
+
}
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
return acc;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Recursively removes empty plain objects `{}` and empty arrays `[]` from a value tree.
|
|
27
|
+
* This prevents ghost containers created by `setIn` intermediate path construction
|
|
28
|
+
* (e.g. `{ address: {} }` when only `address.city` was touched but value is undefined)
|
|
29
|
+
* from falsely triggering the unsaved local changes indicator.
|
|
30
|
+
*/
|
|
31
|
+
/**
|
|
32
|
+
* Check if a value is semantically empty (null, undefined, or empty string).
|
|
33
|
+
*/
|
|
34
|
+
function isSemanticEmpty(v: unknown): boolean {
|
|
35
|
+
return v === null || v === undefined || v === "";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function removeEmptyContainers(obj: unknown): unknown {
|
|
39
|
+
if (Array.isArray(obj)) {
|
|
40
|
+
const cleaned = obj.map(removeEmptyContainers);
|
|
41
|
+
// Keep arrays even if they contain only nulls/undefined — that's intentional data
|
|
42
|
+
return cleaned;
|
|
43
|
+
}
|
|
44
|
+
if (obj && typeof obj === "object" && Object.getPrototypeOf(obj) === Object.prototype) {
|
|
45
|
+
const result: Record<string, unknown> = {};
|
|
46
|
+
for (const key of Object.keys(obj)) {
|
|
47
|
+
const cleaned = removeEmptyContainers((obj as Record<string, unknown>)[key]);
|
|
48
|
+
// Skip empty plain objects
|
|
49
|
+
if (cleaned && typeof cleaned === "object" && !Array.isArray(cleaned)
|
|
50
|
+
&& Object.getPrototypeOf(cleaned) === Object.prototype
|
|
51
|
+
&& Object.keys(cleaned).length === 0) {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
result[key] = cleaned;
|
|
55
|
+
}
|
|
56
|
+
// After cleaning, check if all remaining values are semantically empty
|
|
57
|
+
// (null, undefined, or ""). This catches ghost objects like {type: "", value: null}
|
|
58
|
+
// created by oneOf block initialization that aren't meaningful changes.
|
|
59
|
+
if (Object.keys(result).length > 0 && Object.values(result).every(isSemanticEmpty)) {
|
|
60
|
+
return {};
|
|
61
|
+
}
|
|
62
|
+
return result;
|
|
63
|
+
}
|
|
64
|
+
return obj;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function getChanges<T extends object>(source: Partial<T>, comparison: Partial<T>): Partial<T> {
|
|
68
|
+
const changes: Partial<T> = {};
|
|
69
|
+
|
|
70
|
+
if (!source) {
|
|
71
|
+
return {};
|
|
72
|
+
}
|
|
73
|
+
if (!comparison) {
|
|
74
|
+
return source;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const allKeys = Array.from(new Set([...Object.keys(source), ...Object.keys(comparison)]));
|
|
78
|
+
|
|
79
|
+
for (const key of allKeys) {
|
|
80
|
+
const sourceValue = (source as Record<string, unknown>)[key];
|
|
81
|
+
const comparisonValue = (comparison as Record<string, unknown>)[key];
|
|
82
|
+
|
|
83
|
+
if (equal(sourceValue, comparisonValue)) {
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const sourceHasKey = source && typeof source === "object" && Object.prototype.hasOwnProperty.call(source, key);
|
|
88
|
+
const comparisonHasKey = comparison && typeof comparison === "object" && Object.prototype.hasOwnProperty.call(comparison, key);
|
|
89
|
+
|
|
90
|
+
if (comparisonHasKey && !sourceHasKey) {
|
|
91
|
+
(changes as Record<string, unknown>)[key] = undefined;
|
|
92
|
+
} else if (Array.isArray(sourceValue)) {
|
|
93
|
+
const comparisonArray = Array.isArray(comparisonValue) ? comparisonValue : [];
|
|
94
|
+
if (sourceValue.length !== comparisonArray.length) {
|
|
95
|
+
(changes as Record<string, unknown>)[key] = sourceValue;
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
const hasChanges = sourceValue.some((item, index) => !equal(item, comparisonArray[index]));
|
|
99
|
+
if (hasChanges) {
|
|
100
|
+
(changes as Record<string, unknown>)[key] = sourceValue;
|
|
101
|
+
}
|
|
102
|
+
} else if (isObject(sourceValue) && sourceValue && isObject(comparisonValue) && comparisonValue) {
|
|
103
|
+
const nestedChanges = getChanges(sourceValue, comparisonValue);
|
|
104
|
+
if (Object.keys(nestedChanges).length > 0) {
|
|
105
|
+
(changes as Record<string, unknown>)[key] = nestedChanges;
|
|
106
|
+
}
|
|
107
|
+
} else {
|
|
108
|
+
(changes as Record<string, unknown>)[key] = sourceValue;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return changes;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function getInitialEntityValues<M extends Record<string, unknown>>(
|
|
116
|
+
authController: AuthController,
|
|
117
|
+
collection: EntityCollection,
|
|
118
|
+
path: string,
|
|
119
|
+
status: "new" | "existing" | "copy",
|
|
120
|
+
entity: Entity<M> | undefined,
|
|
121
|
+
propertyConfigs?: Record<string, PropertyConfig>
|
|
122
|
+
): Partial<EntityValues<M>> {
|
|
123
|
+
const properties = collection.properties;
|
|
124
|
+
if ((status === "existing" || status === "copy") && entity) {
|
|
125
|
+
let values: Partial<EntityValues<M>>;
|
|
126
|
+
if (!collection.alwaysApplyDefaultValues) {
|
|
127
|
+
values = entity.values ?? getDefaultValuesFor(properties);
|
|
128
|
+
} else {
|
|
129
|
+
const defaultValues = getDefaultValuesFor(properties);
|
|
130
|
+
values = mergeDeep(defaultValues, entity.values ?? {});
|
|
131
|
+
}
|
|
132
|
+
// When copying, clear ID fields so the database generates new IDs
|
|
133
|
+
if (status === "copy") {
|
|
134
|
+
const result = { ...values };
|
|
135
|
+
for (const [key, property] of Object.entries(properties)) {
|
|
136
|
+
if (property && "isId" in property && property.isId) {
|
|
137
|
+
delete (result as Record<string, unknown>)[key];
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return result;
|
|
141
|
+
}
|
|
142
|
+
return values;
|
|
143
|
+
} else if (status === "new") {
|
|
144
|
+
return getDefaultValuesFor(properties);
|
|
145
|
+
} else {
|
|
146
|
+
console.error({
|
|
147
|
+
status,
|
|
148
|
+
entity
|
|
149
|
+
});
|
|
150
|
+
throw new Error("Form has not been initialised with the correct parameters");
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function zodToFormErrors(zodError: z.ZodError): Record<string, string> {
|
|
155
|
+
let errors: Record<string, string> = {};
|
|
156
|
+
for (const issue of zodError.issues) {
|
|
157
|
+
const path = issue.path.join(".");
|
|
158
|
+
if (path && !getIn(errors, path)) {
|
|
159
|
+
errors = setIn(errors, path, issue.message) as Record<string, string>;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return errors;
|
|
163
|
+
}
|
package/src/form/index.tsx
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
export {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
} from "./
|
|
5
|
-
|
|
1
|
+
export { EntityForm } from "./EntityForm";
|
|
2
|
+
export type { EntityFormProps, OnUpdateParams } from "../types/components/EntityFormProps";
|
|
3
|
+
|
|
4
|
+
export { EntityFormBinding } from "./EntityFormBinding";
|
|
5
|
+
export type { EntityFormBindingProps } from "./EntityFormBinding";
|
|
6
6
|
|
|
7
7
|
export { SelectFieldBinding } from "./field_bindings/SelectFieldBinding";
|
|
8
8
|
export { MultiSelectFieldBinding } from "./field_bindings/MultiSelectFieldBinding";
|
|
@@ -12,7 +12,6 @@ export { TextFieldBinding } from "./field_bindings/TextFieldBinding";
|
|
|
12
12
|
export { SwitchFieldBinding } from "./field_bindings/SwitchFieldBinding";
|
|
13
13
|
export { DateTimeFieldBinding } from "./field_bindings/DateTimeFieldBinding";
|
|
14
14
|
export { ReferenceFieldBinding } from "./field_bindings/ReferenceFieldBinding";
|
|
15
|
-
export { ReferenceAsStringFieldBinding } from "./field_bindings/ReferenceAsStringFieldBinding";
|
|
16
15
|
export { MapFieldBinding } from "./field_bindings/MapFieldBinding";
|
|
17
16
|
export { KeyValueFieldBinding } from "./field_bindings/KeyValueFieldBinding";
|
|
18
17
|
export { RepeatFieldBinding } from "./field_bindings/RepeatFieldBinding";
|
|
@@ -26,3 +25,12 @@ export * from "./components";
|
|
|
26
25
|
|
|
27
26
|
export { PropertyFieldBinding } from "./PropertyFieldBinding";
|
|
28
27
|
export * from "./useClearRestoreValue";
|
|
28
|
+
|
|
29
|
+
// Shared form utilities
|
|
30
|
+
export {
|
|
31
|
+
extractTouchedValues,
|
|
32
|
+
removeEmptyContainers,
|
|
33
|
+
getChanges,
|
|
34
|
+
getInitialEntityValues,
|
|
35
|
+
zodToFormErrors
|
|
36
|
+
} from "./form_utils";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { EntityCollection } from "@rebasepro/types";
|
|
2
2
|
import React, { createContext } from "react";
|
|
3
|
-
import { CollectionRegistryController
|
|
3
|
+
import { CollectionRegistryController } from "@rebasepro/types";
|
|
4
4
|
|
|
5
5
|
export const CollectionRegistryContext = createContext<CollectionRegistryController>({
|
|
6
6
|
getCollection: () => undefined,
|
|
@@ -12,7 +12,7 @@ export const CollectionRegistryContext = createContext<CollectionRegistryControl
|
|
|
12
12
|
initialised: false
|
|
13
13
|
});
|
|
14
14
|
|
|
15
|
-
export function useCollectionRegistryController<DB = Record<string, unknown>, EC extends EntityCollection = EntityCollection
|
|
15
|
+
export function useCollectionRegistryController<DB = Record<string, unknown>, EC extends EntityCollection = EntityCollection>(): CollectionRegistryController<DB, EC> {
|
|
16
16
|
const context = React.useContext(CollectionRegistryContext);
|
|
17
17
|
if (context === undefined) {
|
|
18
18
|
throw new Error("useCollectionRegistryController must be used within a CollectionRegistryContext.Provider");
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { NavigationStateController
|
|
1
|
+
import type { NavigationStateController } from "@rebasepro/types";
|
|
2
2
|
import React, { createContext } from "react";
|
|
3
|
-
|
|
3
|
+
|
|
4
4
|
|
|
5
5
|
export const NavigationStateContext = createContext<NavigationStateController>({
|
|
6
6
|
loading: true,
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState, useMemo } from "react";
|
|
2
|
+
|
|
3
|
+
export type UseAsyncResolverResult<T> = {
|
|
4
|
+
data: T;
|
|
5
|
+
loading: boolean;
|
|
6
|
+
error: Error | undefined;
|
|
7
|
+
refresh: () => void;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Generic hook that resolves an async value with loading/error/refresh
|
|
12
|
+
* state management, cancellation on unmount or deps change, and
|
|
13
|
+
* ref-based change detection to prevent unnecessary state updates.
|
|
14
|
+
*
|
|
15
|
+
* Extracted from the common pattern in useResolvedCollections and
|
|
16
|
+
* useResolvedViews.
|
|
17
|
+
*/
|
|
18
|
+
export function useAsyncResolver<T>({
|
|
19
|
+
resolver,
|
|
20
|
+
initialValue,
|
|
21
|
+
isEqual,
|
|
22
|
+
deps,
|
|
23
|
+
disabled,
|
|
24
|
+
}: {
|
|
25
|
+
/** Async function that resolves the data */
|
|
26
|
+
resolver: () => Promise<T>;
|
|
27
|
+
/** Initial value before first resolution */
|
|
28
|
+
initialValue: T;
|
|
29
|
+
/** Equality check to prevent unnecessary state updates */
|
|
30
|
+
isEqual: (a: T, b: T) => boolean;
|
|
31
|
+
/** Effect dependencies — when these change, resolver re-runs */
|
|
32
|
+
deps: React.DependencyList;
|
|
33
|
+
/** When true, skip resolution */
|
|
34
|
+
disabled?: boolean;
|
|
35
|
+
}): UseAsyncResolverResult<T> {
|
|
36
|
+
|
|
37
|
+
const [data, setData] = useState<T>(initialValue);
|
|
38
|
+
const [loading, setLoading] = useState(true);
|
|
39
|
+
const [error, setError] = useState<Error | undefined>(undefined);
|
|
40
|
+
const [refreshTrigger, setRefreshTrigger] = useState(0);
|
|
41
|
+
|
|
42
|
+
const refresh = useCallback(() => {
|
|
43
|
+
setRefreshTrigger(prev => prev + 1);
|
|
44
|
+
}, []);
|
|
45
|
+
|
|
46
|
+
// Ref for change detection — avoids state updates when data hasn't changed
|
|
47
|
+
const dataRef = useRef<T>(initialValue);
|
|
48
|
+
|
|
49
|
+
// Store resolver in a ref so we always call the latest version
|
|
50
|
+
// without adding it to effect deps (it's typically a new closure each render)
|
|
51
|
+
const resolverRef = useRef(resolver);
|
|
52
|
+
resolverRef.current = resolver;
|
|
53
|
+
|
|
54
|
+
// Same for isEqual
|
|
55
|
+
const isEqualRef = useRef(isEqual);
|
|
56
|
+
isEqualRef.current = isEqual;
|
|
57
|
+
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
if (disabled) return;
|
|
60
|
+
|
|
61
|
+
let cancelled = false;
|
|
62
|
+
|
|
63
|
+
(async () => {
|
|
64
|
+
try {
|
|
65
|
+
const result = await resolverRef.current();
|
|
66
|
+
|
|
67
|
+
if (cancelled) return;
|
|
68
|
+
|
|
69
|
+
// Only update state if data actually changed
|
|
70
|
+
if (!isEqualRef.current(dataRef.current, result)) {
|
|
71
|
+
dataRef.current = result;
|
|
72
|
+
setData(result);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
setError(undefined);
|
|
76
|
+
} catch (e) {
|
|
77
|
+
if (!cancelled) {
|
|
78
|
+
console.error(e);
|
|
79
|
+
setError(e as Error);
|
|
80
|
+
}
|
|
81
|
+
} finally {
|
|
82
|
+
if (!cancelled) {
|
|
83
|
+
setLoading(false);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
})();
|
|
87
|
+
|
|
88
|
+
return () => {
|
|
89
|
+
cancelled = true;
|
|
90
|
+
};
|
|
91
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
92
|
+
}, [...deps, refreshTrigger, disabled]);
|
|
93
|
+
|
|
94
|
+
return useMemo(() => ({
|
|
95
|
+
data,
|
|
96
|
+
loading,
|
|
97
|
+
error,
|
|
98
|
+
refresh
|
|
99
|
+
}), [data, loading, error, refresh]);
|
|
100
|
+
}
|
|
@@ -1,16 +1,21 @@
|
|
|
1
|
-
import type { EntityCollection } from "@rebasepro/types";
|
|
2
|
-
import { useCallback, useRef, useState, useMemo } from "react";
|
|
1
|
+
import type { EntityCollection, DataSourceDefinition } from "@rebasepro/types";
|
|
2
|
+
import { useCallback, useEffect, useRef, useState, useMemo } from "react";
|
|
3
3
|
import { CollectionRegistry, getParentReferencesFromPath as commonGetParentReferencesFromPath, removeInitialAndTrailingSlashes, getSubcollections } from "@rebasepro/common";
|
|
4
4
|
import { EntityReference, UserConfigurationPersistence, CollectionRegistryController } from "@rebasepro/types";
|
|
5
5
|
import { mergeDeep } from "@rebasepro/utils";
|
|
6
6
|
|
|
7
7
|
export function useBuildCollectionRegistryController(props: {
|
|
8
|
-
userConfigPersistence?: UserConfigurationPersistence
|
|
8
|
+
userConfigPersistence?: UserConfigurationPersistence,
|
|
9
|
+
dataSources?: Record<string, DataSourceDefinition>
|
|
9
10
|
}): CollectionRegistryController & {
|
|
10
11
|
collectionRegistryRef: React.MutableRefObject<CollectionRegistry>;
|
|
11
12
|
} {
|
|
12
|
-
const { userConfigPersistence } = props;
|
|
13
|
-
const collectionRegistryRef = useRef<CollectionRegistry>(new CollectionRegistry());
|
|
13
|
+
const { userConfigPersistence, dataSources } = props;
|
|
14
|
+
const collectionRegistryRef = useRef<CollectionRegistry>(new CollectionRegistry(undefined, dataSources));
|
|
15
|
+
// Keep the registry's data sources in sync (used to resolve engine during
|
|
16
|
+
// normalization). Applied synchronously before the async registration
|
|
17
|
+
// effect in useResolvedCollections runs.
|
|
18
|
+
if (dataSources) collectionRegistryRef.current.setDataSources(dataSources);
|
|
14
19
|
const [initialised, setInitialised] = useState(false);
|
|
15
20
|
|
|
16
21
|
const getCollection = useCallback((
|
|
@@ -156,9 +161,11 @@ export function useBuildCollectionRegistryController(props: {
|
|
|
156
161
|
|
|
157
162
|
// Determine initialised automatically based on whether collections exist,
|
|
158
163
|
// though the NavigationStateController also plays a role in overall init
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
164
|
+
useEffect(() => {
|
|
165
|
+
if (!initialised && collections.length > 0) {
|
|
166
|
+
setInitialised(true);
|
|
167
|
+
}
|
|
168
|
+
}, [initialised, collections.length]);
|
|
162
169
|
|
|
163
170
|
return useMemo(() => ({
|
|
164
171
|
collections,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AppView, EntityCollection, NavigationResult, RebasePlugin, NavigationStateController, UrlController, NavigationGroupMapping } from "@rebasepro/types";
|
|
2
|
-
import { useCallback, useMemo } from "react";
|
|
2
|
+
import { useCallback, useMemo, useRef } from "react";
|
|
3
3
|
|
|
4
4
|
import { AuthController, RebaseData, CollectionRegistryController, User } from "@rebasepro/types";
|
|
5
5
|
import type { EntityCollectionsBuilder, AppViewsBuilder, EffectiveRoleController } from "@rebasepro/types";
|
|
@@ -100,10 +100,16 @@ export function useBuildNavigationStateController<EC extends EntityCollection, U
|
|
|
100
100
|
collectionRegistryController
|
|
101
101
|
});
|
|
102
102
|
|
|
103
|
-
// Expose a combined refresh function
|
|
103
|
+
// Expose a combined refresh function with microtask batching
|
|
104
|
+
const pendingRefreshRef = useRef(false);
|
|
104
105
|
const refreshNavigation = useCallback(() => {
|
|
105
|
-
|
|
106
|
-
|
|
106
|
+
if (pendingRefreshRef.current) return;
|
|
107
|
+
pendingRefreshRef.current = true;
|
|
108
|
+
queueMicrotask(() => {
|
|
109
|
+
pendingRefreshRef.current = false;
|
|
110
|
+
refreshCollections();
|
|
111
|
+
refreshViews();
|
|
112
|
+
});
|
|
107
113
|
}, [refreshCollections, refreshViews]);
|
|
108
114
|
|
|
109
115
|
return useMemo(() => ({
|
|
@@ -112,8 +118,7 @@ export function useBuildNavigationStateController<EC extends EntityCollection, U
|
|
|
112
118
|
topLevelNavigation,
|
|
113
119
|
loading: collectionsLoading || viewsLoading,
|
|
114
120
|
navigationLoadingError: collectionsError ?? viewsError,
|
|
115
|
-
refreshNavigation
|
|
116
|
-
plugins
|
|
121
|
+
refreshNavigation
|
|
117
122
|
}), [
|
|
118
123
|
views,
|
|
119
124
|
adminViews,
|
|
@@ -122,7 +127,6 @@ export function useBuildNavigationStateController<EC extends EntityCollection, U
|
|
|
122
127
|
viewsLoading,
|
|
123
128
|
collectionsError,
|
|
124
129
|
viewsError,
|
|
125
|
-
refreshNavigation
|
|
126
|
-
plugins
|
|
130
|
+
refreshNavigation
|
|
127
131
|
]);
|
|
128
132
|
}
|
|
@@ -211,5 +211,38 @@ export async function resolveAppViews(
|
|
|
211
211
|
}
|
|
212
212
|
}
|
|
213
213
|
|
|
214
|
+
// Detect duplicate view slugs (dev warning)
|
|
215
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
216
|
+
const slugCounts = new Map<string, number>();
|
|
217
|
+
resolvedViews.forEach(v => {
|
|
218
|
+
slugCounts.set(v.slug, (slugCounts.get(v.slug) ?? 0) + 1);
|
|
219
|
+
});
|
|
220
|
+
slugCounts.forEach((count, slug) => {
|
|
221
|
+
if (count > 1) {
|
|
222
|
+
console.warn(
|
|
223
|
+
`[Rebase] Duplicate view slug "${slug}" detected (${count} views). ` +
|
|
224
|
+
`Last-write-wins. Ensure unique slugs across CMS views and plugins.`
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Filter by roles — applies to CMS, plugin, and builder-returned views
|
|
231
|
+
resolvedViews = filterViewsByRole(resolvedViews, authController);
|
|
232
|
+
|
|
214
233
|
return resolvedViews;
|
|
215
234
|
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Filter views by the `roles` field on AppView.
|
|
238
|
+
* When `roles` is set, the view is only included if the current user
|
|
239
|
+
* has at least one of the listed roles. Views without `roles` (or with
|
|
240
|
+
* an empty array) are always included.
|
|
241
|
+
*/
|
|
242
|
+
function filterViewsByRole(views: AppView[], authController: AuthController): AppView[] {
|
|
243
|
+
const userRoles = authController.user?.roles ?? [];
|
|
244
|
+
return views.filter(view => {
|
|
245
|
+
if (!view.roles || view.roles.length === 0) return true;
|
|
246
|
+
return view.roles.some(role => userRoles.includes(role));
|
|
247
|
+
});
|
|
248
|
+
}
|