@rebasepro/app 0.17.3 → 0.18.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/README.md +4 -1
- package/dist/components/index.d.ts +0 -3
- package/dist/contexts/AuthControllerContext.d.ts +2 -1
- package/dist/contexts/CustomizationControllerContext.d.ts +2 -1
- package/dist/contexts/DialogsProvider.d.ts +2 -1
- package/dist/contexts/RebaseDataContext.d.ts +10 -1
- package/dist/contexts/StorageSourceContext.d.ts +11 -1
- package/dist/core/RebaseProps.d.ts +2 -12
- package/dist/debug/index.d.ts +19 -0
- package/dist/debug.js +3631 -0
- package/dist/hooks/data/delete.d.ts +16 -7
- package/dist/hooks/data/save.d.ts +18 -4
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/useAdminModeController.d.ts +5 -4
- package/dist/hooks/useNavigationGroupLabel.d.ts +17 -0
- package/dist/hooks/useStudioBridge.d.ts +33 -0
- package/dist/index.es.js +2599 -6980
- package/dist/index.es.js.map +1 -1
- package/dist/vitePlugin.d.ts +22 -0
- package/dist/vitePlugin.js +40 -1
- package/package.json +42 -31
- package/src/auth/api.ts +0 -134
- package/src/auth/index.ts +0 -26
- package/src/auth/types.ts +0 -115
- package/src/auth/useRebaseAuthController.ts +0 -345
- package/src/collections/collection_view_config.ts +0 -76
- package/src/collections/entity-display-cache.ts +0 -195
- package/src/collections/entity-display.ts +0 -77
- package/src/collections/entity_image_preview.ts +0 -61
- package/src/collections/filter-operator-resolution.ts +0 -119
- package/src/collections/form-layout.ts +0 -394
- package/src/collections/index.ts +0 -13
- package/src/collections/navigation_from_path.ts +0 -123
- package/src/collections/navigation_utils.ts +0 -171
- package/src/collections/parent_references_from_path.ts +0 -59
- package/src/collections/property-path.ts +0 -30
- package/src/collections/property_presentation.ts +0 -264
- package/src/collections/summary-property.ts +0 -114
- package/src/collections/title-property.ts +0 -312
- package/src/components/AIIcon.tsx +0 -47
- package/src/components/AuthSimulationMenu.tsx +0 -0
- package/src/components/ConfirmationDialog.tsx +0 -48
- package/src/components/Debug/UIReferenceView.tsx +0 -1053
- package/src/components/Debug/UIStyleGuide.tsx +0 -159
- package/src/components/Debug/collection-views/CardViewDemo.tsx +0 -87
- package/src/components/Debug/collection-views/CollectionTableDemo.tsx +0 -107
- package/src/components/Debug/collection-views/KanbanBoardDemo.tsx +0 -89
- package/src/components/Debug/collection-views/index.ts +0 -3
- package/src/components/Debug/collection-views/sample_data.ts +0 -44
- package/src/components/Debug/crm-dashboard/CalendarWidget.tsx +0 -387
- package/src/components/Debug/crm-dashboard/CrmDashboardDemo.tsx +0 -1100
- package/src/components/Debug/crm-dashboard/DashboardMetrics.tsx +0 -131
- package/src/components/Debug/crm-dashboard/PipelineOverview.tsx +0 -288
- package/src/components/Debug/crm-dashboard/RecentActivity.tsx +0 -213
- package/src/components/Debug/crm-dashboard/TaskFilters.tsx +0 -182
- package/src/components/Debug/crm-dashboard/TaskTable.tsx +0 -425
- package/src/components/Debug/crm-dashboard/TasksView.tsx +0 -234
- package/src/components/ErrorTooltip.tsx +0 -12
- package/src/components/ErrorView.tsx +0 -94
- package/src/components/LanguageToggle.tsx +0 -59
- package/src/components/LoginView/LoginView.tsx +0 -1235
- package/src/components/LoginView/index.ts +0 -2
- package/src/components/LoginView/oauth-redirect-flow.ts +0 -184
- package/src/components/NotFoundPage.tsx +0 -27
- package/src/components/RebaseAuth.tsx +0 -27
- package/src/components/RebaseLogo.tsx +0 -88
- package/src/components/SchemaDriftBanner.tsx +0 -102
- package/src/components/UnsavedChangesDialog.tsx +0 -46
- package/src/components/UserDisplay.tsx +0 -54
- package/src/components/UserSelectPopover.tsx +0 -399
- package/src/components/UserSettingsView.tsx +0 -318
- package/src/components/common/index.ts +0 -6
- package/src/components/common/table_height.tsx +0 -21
- package/src/components/common/types.tsx +0 -66
- package/src/components/common/useColumnsIds.tsx +0 -261
- package/src/components/common/useDataTableController.tsx +0 -571
- package/src/components/common/useDebouncedData.ts +0 -49
- package/src/components/common/useScrollRestoration.tsx +0 -68
- package/src/components/index.tsx +0 -26
- package/src/contexts/AdminModeController.tsx +0 -11
- package/src/contexts/AnalyticsContext.tsx +0 -4
- package/src/contexts/AuthControllerContext.tsx +0 -4
- package/src/contexts/CollectionResolverContext.tsx +0 -24
- package/src/contexts/CollectionScopeContext.tsx +0 -68
- package/src/contexts/ComponentOverrideContext.tsx +0 -81
- package/src/contexts/CustomizationControllerContext.tsx +0 -4
- package/src/contexts/DataDriverContext.tsx +0 -4
- package/src/contexts/DataSourcesContext.tsx +0 -36
- package/src/contexts/DatabaseAdminContext.tsx +0 -4
- package/src/contexts/DialogsProvider.tsx +0 -56
- package/src/contexts/EffectiveRoleController.tsx +0 -12
- package/src/contexts/ModeController.tsx +0 -11
- package/src/contexts/RebaseClientInstanceContext.tsx +0 -7
- package/src/contexts/RebaseDataContext.tsx +0 -4
- package/src/contexts/SnackbarProvider.tsx +0 -15
- package/src/contexts/StorageSourceContext.tsx +0 -4
- package/src/contexts/StorageSourcesContext.tsx +0 -30
- package/src/contexts/UserConfigurationPersistenceContext.tsx +0 -4
- package/src/contexts/index.ts +0 -18
- package/src/core/PluginLifecycleManager.tsx +0 -96
- package/src/core/PluginProviderStack.tsx +0 -40
- package/src/core/Rebase.tsx +0 -488
- package/src/core/RebaseProps.tsx +0 -333
- package/src/core/RebaseRouter.tsx +0 -18
- package/src/core/RebaseRoutes.tsx +0 -24
- package/src/core/index.tsx +0 -5
- package/src/hooks/ApiConfigContext.tsx +0 -83
- package/src/hooks/data/collectionQuery.ts +0 -55
- package/src/hooks/data/delete.ts +0 -56
- package/src/hooks/data/save.ts +0 -78
- package/src/hooks/data/useCollection.tsx +0 -183
- package/src/hooks/data/useData.tsx +0 -18
- package/src/hooks/data/useDataOrder.ts +0 -26
- package/src/hooks/data/useFetch.tsx +0 -156
- package/src/hooks/data/useRelationSelector.tsx +0 -280
- package/src/hooks/index.tsx +0 -45
- package/src/hooks/useAdminModeController.tsx +0 -23
- package/src/hooks/useAnalyticsController.tsx +0 -8
- package/src/hooks/useAuthController.tsx +0 -15
- package/src/hooks/useAuthSubscription.ts +0 -130
- package/src/hooks/useBackendStorageSource.ts +0 -295
- package/src/hooks/useBridgeRegistration.tsx +0 -32
- package/src/hooks/useBrowserTitleAndIcon.tsx +0 -23
- package/src/hooks/useBuildAdminModeController.tsx +0 -29
- package/src/hooks/useBuildEffectiveRoleController.tsx +0 -30
- package/src/hooks/useBuildLocalConfigurationPersistence.tsx +0 -83
- package/src/hooks/useBuildModeController.tsx +0 -73
- package/src/hooks/useClipboard.tsx +0 -158
- package/src/hooks/useCollapsedGroups.ts +0 -116
- package/src/hooks/useComponentOverride.tsx +0 -59
- package/src/hooks/useCustomizationController.tsx +0 -14
- package/src/hooks/useDialogsController.tsx +0 -15
- package/src/hooks/useEffectiveRoleController.tsx +0 -10
- package/src/hooks/useLargeLayout.tsx +0 -65
- package/src/hooks/useModeController.tsx +0 -23
- package/src/hooks/useNavigationBlocker.tsx +0 -108
- package/src/hooks/usePermissions.ts +0 -45
- package/src/hooks/useRebaseClient.tsx +0 -10
- package/src/hooks/useRebaseContext.tsx +0 -84
- package/src/hooks/useRebaseRegistry.tsx +0 -91
- package/src/hooks/useResolvedComponent.tsx +0 -195
- package/src/hooks/useSlot.tsx +0 -64
- package/src/hooks/useSnackbarController.tsx +0 -80
- package/src/hooks/useStorageSource.tsx +0 -11
- package/src/hooks/useStudioBridge.tsx +0 -258
- package/src/hooks/useTranslation.ts +0 -38
- package/src/hooks/useUnsavedChangesDialog.tsx +0 -67
- package/src/hooks/useUserConfigurationPersistence.tsx +0 -11
- package/src/i18n/RebaseI18nProvider.tsx +0 -159
- package/src/index.ts +0 -32
- package/src/internal/common.tsx +0 -28
- package/src/internal/useRestoreScroll.tsx +0 -69
- package/src/locales/de.ts +0 -941
- package/src/locales/en.ts +0 -1021
- package/src/locales/es.ts +0 -962
- package/src/locales/fr.ts +0 -941
- package/src/locales/hi.ts +0 -941
- package/src/locales/it.ts +0 -941
- package/src/locales/pt.ts +0 -949
- package/src/util/constants.ts +0 -7
- package/src/util/createFormexStub.tsx +0 -66
- package/src/util/entity_cache.ts +0 -242
- package/src/util/enums.ts +0 -34
- package/src/util/icon_list.ts +0 -22
- package/src/util/icons.tsx +0 -129
- package/src/util/index.ts +0 -13
- package/src/util/local_storage.ts +0 -66
- package/src/util/previews.ts +0 -127
- package/src/util/useStorageUploadController.tsx +0 -354
- package/src/vitePlugin.ts +0 -215
- /package/dist/{components/Debug → debug}/UIReferenceView.d.ts +0 -0
- /package/dist/{components/Debug → debug}/UIStyleGuide.d.ts +0 -0
- /package/dist/{components/Debug → debug}/collection-views/CardViewDemo.d.ts +0 -0
- /package/dist/{components/Debug → debug}/collection-views/CollectionTableDemo.d.ts +0 -0
- /package/dist/{components/Debug → debug}/collection-views/KanbanBoardDemo.d.ts +0 -0
- /package/dist/{components/Debug → debug}/collection-views/index.d.ts +0 -0
- /package/dist/{components/Debug → debug}/collection-views/sample_data.d.ts +0 -0
- /package/dist/{components/Debug → debug}/crm-dashboard/CalendarWidget.d.ts +0 -0
- /package/dist/{components/Debug → debug}/crm-dashboard/CrmDashboardDemo.d.ts +0 -0
- /package/dist/{components/Debug → debug}/crm-dashboard/DashboardMetrics.d.ts +0 -0
- /package/dist/{components/Debug → debug}/crm-dashboard/PipelineOverview.d.ts +0 -0
- /package/dist/{components/Debug → debug}/crm-dashboard/RecentActivity.d.ts +0 -0
- /package/dist/{components/Debug → debug}/crm-dashboard/TaskFilters.d.ts +0 -0
- /package/dist/{components/Debug → debug}/crm-dashboard/TaskTable.d.ts +0 -0
- /package/dist/{components/Debug → debug}/crm-dashboard/TasksView.d.ts +0 -0
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
import { CollectionConfig } from "@rebasepro/types";
|
|
2
|
-
import type { AdminCollection } from "@rebasepro/cms-types";
|
|
3
|
-
import { getDisplayPropertyKey } from "./entity-display";
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* The property that fills a record's image slot.
|
|
7
|
-
*
|
|
8
|
-
* `admin.display.image` first, then six fallbacks in descending confidence —
|
|
9
|
-
* the first image-typed storage property, an array of them, a URL rendered as an
|
|
10
|
-
* image, and so on. The ladder stays for collections that say nothing; a
|
|
11
|
-
* collection that names its picture is not guessed at.
|
|
12
|
-
*/
|
|
13
|
-
export function getEntityImagePreviewPropertyKey<M extends Record<string, unknown>>(collection: CollectionConfig<M>): string | undefined {
|
|
14
|
-
|
|
15
|
-
const declared = getDisplayPropertyKey(collection as AdminCollection<M>, "image");
|
|
16
|
-
if (declared) return declared;
|
|
17
|
-
|
|
18
|
-
// find first storage property of type image
|
|
19
|
-
for (const key in collection.properties) {
|
|
20
|
-
const property = collection.properties[key];
|
|
21
|
-
if (property.type === "string" && property.storage?.acceptedFiles?.includes("image/*")) {
|
|
22
|
-
return key;
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
// alternatively, look for the first array of images
|
|
26
|
-
for (const key in collection.properties) {
|
|
27
|
-
const property = collection.properties[key];
|
|
28
|
-
if (property.type === "array" && !Array.isArray(property.of) && property.of?.type === "string" && property.of.storage?.acceptedFiles?.includes("image/*")) {
|
|
29
|
-
return key;
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
// also check for URL properties with image preview type
|
|
33
|
-
for (const key in collection.properties) {
|
|
34
|
-
const property = collection.properties[key];
|
|
35
|
-
if (property.type === "string" && property.admin?.urlPreview === "image") {
|
|
36
|
-
return key;
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
// and arrays of URL properties with image preview type
|
|
40
|
-
for (const key in collection.properties) {
|
|
41
|
-
const property = collection.properties[key];
|
|
42
|
-
if (property.type === "array" && property.of && !Array.isArray(property.of) && property.of.type === "string" && property.of.admin?.urlPreview === "image") {
|
|
43
|
-
return key;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
// fallback: any storage property without explicit acceptedFiles (e.g. a generic "picture" field)
|
|
47
|
-
for (const key in collection.properties) {
|
|
48
|
-
const property = collection.properties[key];
|
|
49
|
-
if (property.type === "string" && property.storage && !property.storage.acceptedFiles) {
|
|
50
|
-
return key;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
// fallback: any array of storage properties without explicit acceptedFiles
|
|
54
|
-
for (const key in collection.properties) {
|
|
55
|
-
const property = collection.properties[key];
|
|
56
|
-
if (property.type === "array" && !Array.isArray(property.of) && property.of?.type === "string" && property.of.storage && !property.of.storage.acceptedFiles) {
|
|
57
|
-
return key;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
return undefined;
|
|
61
|
-
}
|
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
ALL_WHERE_FILTER_OPS,
|
|
3
|
-
DataType,
|
|
4
|
-
DEFAULT_FILTERABLE_RELATION_KINDS,
|
|
5
|
-
getDataSourceCapabilities,
|
|
6
|
-
Property,
|
|
7
|
-
RelationProperty,
|
|
8
|
-
WhereFilterOp
|
|
9
|
-
} from "@rebasepro/types";
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Default operators offered per property type, before engine capabilities and
|
|
13
|
-
* per-property narrowing are applied. These mirror what the built-in filter
|
|
14
|
-
* fields can render.
|
|
15
|
-
*/
|
|
16
|
-
const COMPARISON_OPS: readonly WhereFilterOp[] = ["==", "!=", ">", ">=", "<", "<="];
|
|
17
|
-
const NULL_CHECK_OPS: readonly WhereFilterOp[] = ["is-null", "is-not-null"];
|
|
18
|
-
const MEMBERSHIP_OPS: readonly WhereFilterOp[] = ["in", "not-in"];
|
|
19
|
-
const PATTERN_OPS: readonly WhereFilterOp[] = ["like", "ilike", "not-like", "not-ilike"];
|
|
20
|
-
|
|
21
|
-
const DEFAULT_OPS_BY_TYPE: Partial<Record<DataType, readonly WhereFilterOp[]>> = {
|
|
22
|
-
string: [...COMPARISON_OPS, ...MEMBERSHIP_OPS, ...PATTERN_OPS, ...NULL_CHECK_OPS],
|
|
23
|
-
number: [...COMPARISON_OPS, ...MEMBERSHIP_OPS, ...NULL_CHECK_OPS],
|
|
24
|
-
date: [...COMPARISON_OPS, ...NULL_CHECK_OPS],
|
|
25
|
-
boolean: ["==", "!=", ...NULL_CHECK_OPS],
|
|
26
|
-
reference: ["==", "!=", ...MEMBERSHIP_OPS, ...NULL_CHECK_OPS],
|
|
27
|
-
relation: ["==", "!=", ...MEMBERSHIP_OPS, ...NULL_CHECK_OPS]
|
|
28
|
-
// geopoint, map, vector, binary, array (as a container): not filterable
|
|
29
|
-
// through the generic filter UI.
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
/** Operators offered when the property is an *array of* a filterable type. */
|
|
33
|
-
const ARRAY_OPS: readonly WhereFilterOp[] = ["array-contains", "array-contains-any"];
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Whether a relation is one the collection's engine can compile into a
|
|
37
|
-
* `WHERE`.
|
|
38
|
-
*
|
|
39
|
-
* Which kinds those are is the *driver's* answer, not this function's, so it
|
|
40
|
-
* comes from {@link DataSourceCapabilities.filterableRelationKinds}. The admin
|
|
41
|
-
* is one UI over Postgres, MongoDB, Firestore and whatever a developer
|
|
42
|
-
* registers, and "a many-to-many compiles to an `EXISTS` over the junction" is
|
|
43
|
-
* a fact about the Postgres driver — true today, and not the sort of thing to
|
|
44
|
-
* assert on an engine's behalf.
|
|
45
|
-
*
|
|
46
|
-
* Offering an uncompilable filter is not a cosmetic bug. The Postgres driver
|
|
47
|
-
* used to drop a filter key it could not resolve, which *widened* the result
|
|
48
|
-
* set — filtering a many-to-many column returned every row instead of none.
|
|
49
|
-
* That now fails closed with a 400, which is correct for a real schema drift
|
|
50
|
-
* and wrong as the answer to a control the admin itself put on screen.
|
|
51
|
-
*
|
|
52
|
-
* A relation whose kind cannot be determined at all — no inline `relation`
|
|
53
|
-
* block and no stamped `resolvedRelation` — stays filterable. The server
|
|
54
|
-
* resolves relations before the config reaches the admin, so this is the
|
|
55
|
-
* hand-built-property case, and silently dropping a working filter there would
|
|
56
|
-
* be its own regression.
|
|
57
|
-
*/
|
|
58
|
-
export function isFilterableRelation(property: Property, engine?: string): boolean {
|
|
59
|
-
if (property.type !== "relation") return true;
|
|
60
|
-
const relationProperty = property as RelationProperty;
|
|
61
|
-
const kind = relationProperty.relation?.kind ?? relationProperty.resolvedRelation?.kind;
|
|
62
|
-
if (!kind) return true;
|
|
63
|
-
const kinds = getDataSourceCapabilities(engine).filterableRelationKinds
|
|
64
|
-
?? DEFAULT_FILTERABLE_RELATION_KINDS;
|
|
65
|
-
return kinds.includes(kind);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export interface ResolveFilterOperatorsParams {
|
|
69
|
-
/**
|
|
70
|
-
* The property to filter on. For array properties, pass the **item**
|
|
71
|
-
* property (`property.of`) together with `isArray: true` — the same
|
|
72
|
-
* convention the filter field dispatchers use.
|
|
73
|
-
*/
|
|
74
|
-
property: Property;
|
|
75
|
-
/** True when filtering an array of `property`. */
|
|
76
|
-
isArray?: boolean;
|
|
77
|
-
/**
|
|
78
|
-
* The engine backing the collection (`collection.engine`, e.g.
|
|
79
|
-
* `"postgres"`, `"firestore"`). Falls back to the default engine's
|
|
80
|
-
* capabilities when omitted.
|
|
81
|
-
*/
|
|
82
|
-
engine?: string;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Resolve which filter operators the UI should offer for a property.
|
|
87
|
-
*
|
|
88
|
-
* The result is the **intersection** of three sets:
|
|
89
|
-
* 1. what the engine can execute — {@link DataSourceCapabilities.filterOperators}
|
|
90
|
-
* (e.g. Firestore cannot run the LIKE family);
|
|
91
|
-
* 2. what makes sense for the property type (e.g. no `>` on booleans);
|
|
92
|
-
* 3. the developer's optional narrowing — `property.admin.filterOperators`.
|
|
93
|
-
*
|
|
94
|
-
* Returns an empty array when the property is not filterable — by type, by
|
|
95
|
-
* developer narrowing (`filterOperators: []`), or because it is a relation the
|
|
96
|
-
* query layer has no column for (see {@link isFilterableRelation}).
|
|
97
|
-
*
|
|
98
|
-
* @group Models
|
|
99
|
-
*/
|
|
100
|
-
export function resolveFilterOperators({
|
|
101
|
-
property,
|
|
102
|
-
isArray,
|
|
103
|
-
engine
|
|
104
|
-
}: ResolveFilterOperatorsParams): WhereFilterOp[] {
|
|
105
|
-
if (!isFilterableRelation(property, engine)) return [];
|
|
106
|
-
|
|
107
|
-
const typeDefaults: readonly WhereFilterOp[] = isArray
|
|
108
|
-
? ARRAY_OPS
|
|
109
|
-
: DEFAULT_OPS_BY_TYPE[property.type] ?? [];
|
|
110
|
-
if (typeDefaults.length === 0) return [];
|
|
111
|
-
|
|
112
|
-
const engineOps = new Set(getDataSourceCapabilities(engine).filterOperators ?? ALL_WHERE_FILTER_OPS);
|
|
113
|
-
|
|
114
|
-
const narrowing = property.admin?.filterOperators;
|
|
115
|
-
const narrowingSet = narrowing !== undefined ? new Set(narrowing) : undefined;
|
|
116
|
-
|
|
117
|
-
return typeDefaults.filter(op =>
|
|
118
|
-
engineOps.has(op) && (narrowingSet === undefined || narrowingSet.has(op)));
|
|
119
|
-
}
|
|
@@ -1,394 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Form layout resolution.
|
|
3
|
-
*
|
|
4
|
-
* Turns a collection into the shape the entity form renders: an ordered list of
|
|
5
|
-
* sections holding grid-spanned fields, plus the metadata rail beside them.
|
|
6
|
-
*
|
|
7
|
-
* The reason this is a pure function rather than logic inside the form is that
|
|
8
|
-
* the *defaults* are the interesting part. A collection that never writes an
|
|
9
|
-
* `admin.form` block still has to get a two-column layout out of this — the flat
|
|
10
|
-
* run of full-width fields it produced before was the single biggest cost in the
|
|
11
|
-
* form, and no amount of config would have fixed it for collections nobody
|
|
12
|
-
* hand-tunes. Deriving it here means it is testable in isolation, which matters
|
|
13
|
-
* because "what span does a date get" is exactly the kind of rule that rots.
|
|
14
|
-
*/
|
|
15
|
-
import type {
|
|
16
|
-
AdminCollection,
|
|
17
|
-
FormSection,
|
|
18
|
-
PropertySpan
|
|
19
|
-
} from "@rebasepro/cms-types";
|
|
20
|
-
import type { Property } from "@rebasepro/types";
|
|
21
|
-
import { getDisplayPropertyKey } from "./entity-display";
|
|
22
|
-
import { isHidden } from "./property_presentation";
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Columns in the form grid.
|
|
26
|
-
*
|
|
27
|
-
* A local literal rather than an import of `FORM_GRID_COLUMNS`: everything else
|
|
28
|
-
* this module takes from `@rebasepro/cms-types` is a *type*, erased at build
|
|
29
|
-
* time, and a runtime value would make this pure function depend on that
|
|
30
|
-
* package's built output — which is exactly what broke the dev server, since
|
|
31
|
-
* `@rebasepro/app` resolves that package to its dist. `satisfies PropertySpan`
|
|
32
|
-
* keeps the two honest: a full-width field and the column count are the same
|
|
33
|
-
* number by definition.
|
|
34
|
-
*/
|
|
35
|
-
const GRID_COLUMNS = 4 satisfies PropertySpan;
|
|
36
|
-
|
|
37
|
-
/** A field placed on the grid. */
|
|
38
|
-
export interface ResolvedFormField {
|
|
39
|
-
key: string;
|
|
40
|
-
/** Columns occupied, out of `GRID_COLUMNS`. Ignored in the rail. */
|
|
41
|
-
span: PropertySpan;
|
|
42
|
-
/** True for an `additionalFields` entry rather than a property. */
|
|
43
|
-
additional: boolean;
|
|
44
|
-
/**
|
|
45
|
-
* The span was written on the property rather than derived from its type.
|
|
46
|
-
* Row filling leaves these alone: an author who wrote a width meant it.
|
|
47
|
-
*/
|
|
48
|
-
spanExplicit?: boolean;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export interface ResolvedFormSection {
|
|
52
|
-
key: string;
|
|
53
|
-
title?: string;
|
|
54
|
-
collapsible: boolean;
|
|
55
|
-
/** Initial state only; the form owns it after first interaction. */
|
|
56
|
-
collapsed: boolean;
|
|
57
|
-
fields: ResolvedFormField[];
|
|
58
|
-
/**
|
|
59
|
-
* Declared arrangement for the read-only view. Carried through untouched —
|
|
60
|
-
* the resolver decides *which* fields a section holds, not how the surface
|
|
61
|
-
* that renders it stacks them, and only the read view honours this.
|
|
62
|
-
*/
|
|
63
|
-
readVariant?: "grid" | "summary";
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export interface ResolvedFormLayout {
|
|
67
|
-
sections: ResolvedFormSection[];
|
|
68
|
-
/** Fields shown in the rail. Empty means no rail fields. */
|
|
69
|
-
sidebar: ResolvedFormField[];
|
|
70
|
-
/** Show the read-only id/created/updated block at the foot of the rail. */
|
|
71
|
-
showRecordMeta: boolean;
|
|
72
|
-
/** True when there is anything at all to put in the rail. */
|
|
73
|
-
hasRail: boolean;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
export interface ResolveFormLayoutParams<M extends Record<string, unknown>> {
|
|
77
|
-
collection: AdminCollection<M>;
|
|
78
|
-
/**
|
|
79
|
-
* Field keys in render order, already filtered by `propertiesOrder` — i.e.
|
|
80
|
-
* the output of `getFormFieldKeys`. Passed in rather than recomputed so the
|
|
81
|
-
* form and the layout can never disagree about which fields exist.
|
|
82
|
-
*/
|
|
83
|
-
fieldKeys: string[];
|
|
84
|
-
/**
|
|
85
|
-
* Which fields the user may edit right now. A manual id is editable while
|
|
86
|
-
* creating and frozen afterwards, and that decides whether it belongs in the
|
|
87
|
-
* form at all or only in the record block.
|
|
88
|
-
*/
|
|
89
|
-
status: "new" | "existing" | "copy";
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/* -------------------------------------------------------------------------- */
|
|
93
|
-
/* span derivation */
|
|
94
|
-
/* -------------------------------------------------------------------------- */
|
|
95
|
-
|
|
96
|
-
/** Does this property's editor need the full width of the column? */
|
|
97
|
-
function needsFullWidth(property: Property): boolean {
|
|
98
|
-
switch (property.type) {
|
|
99
|
-
case "map":
|
|
100
|
-
case "binary":
|
|
101
|
-
case "vector":
|
|
102
|
-
return true;
|
|
103
|
-
case "array": {
|
|
104
|
-
// A tag-style array of plain short strings reads fine at half width.
|
|
105
|
-
// Anything else — files, nested maps, `oneOf` blocks — does not.
|
|
106
|
-
if (property.oneOf) return true;
|
|
107
|
-
const of = Array.isArray(property.of) ? undefined : property.of;
|
|
108
|
-
if (!of) return true;
|
|
109
|
-
if (of.type !== "string" && of.type !== "number") return true;
|
|
110
|
-
if (of.type === "string" && (of.storage || of.admin?.markdown || of.admin?.multiline)) return true;
|
|
111
|
-
return false;
|
|
112
|
-
}
|
|
113
|
-
case "string":
|
|
114
|
-
return Boolean(property.storage || property.admin?.markdown || property.admin?.multiline);
|
|
115
|
-
default:
|
|
116
|
-
return false;
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/**
|
|
121
|
-
* The span a property gets when nothing is declared.
|
|
122
|
-
*
|
|
123
|
-
* Deliberately coarse: three buckets, chosen by how much room the *editor*
|
|
124
|
-
* needs, not by how important the field is. Importance is what `admin.form`
|
|
125
|
-
* sections are for.
|
|
126
|
-
*/
|
|
127
|
-
export function deriveSpan(property: Property, isTitleProperty: boolean): PropertySpan {
|
|
128
|
-
if (needsFullWidth(property)) return 4;
|
|
129
|
-
// The record's name carries the row on its own; it is the first thing read.
|
|
130
|
-
if (isTitleProperty) return 4;
|
|
131
|
-
switch (property.type) {
|
|
132
|
-
case "number":
|
|
133
|
-
case "boolean":
|
|
134
|
-
return 1;
|
|
135
|
-
case "date":
|
|
136
|
-
case "geopoint":
|
|
137
|
-
case "reference":
|
|
138
|
-
case "relation":
|
|
139
|
-
return 2;
|
|
140
|
-
case "array":
|
|
141
|
-
return 2; // short primitive arrays only — the rest returned 4 above
|
|
142
|
-
case "string":
|
|
143
|
-
return 2;
|
|
144
|
-
default:
|
|
145
|
-
return 2;
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/* -------------------------------------------------------------------------- */
|
|
150
|
-
/* id handling */
|
|
151
|
-
/* -------------------------------------------------------------------------- */
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
* Can the user still type this id?
|
|
155
|
-
*
|
|
156
|
-
* A `manual` id is a real field while creating and frozen once the row exists.
|
|
157
|
-
* Every generated strategy (`uuid`, `cuid`, a raw SQL default) is never typed.
|
|
158
|
-
* Getting this wrong in the "frozen" direction would make manual-id collections
|
|
159
|
-
* uncreatable, so it fails towards showing the field.
|
|
160
|
-
*/
|
|
161
|
-
export function isIdPropertyEditable(property: Property, status: "new" | "existing" | "copy"): boolean {
|
|
162
|
-
if (!("isId" in property) || !property.isId) return true;
|
|
163
|
-
if (status === "existing") return false;
|
|
164
|
-
return property.isId === "manual";
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
function isIdProperty(property: Property | undefined): boolean {
|
|
168
|
-
return Boolean(property && "isId" in property && property.isId);
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
/**
|
|
172
|
-
* An audit timestamp the database maintains — `created_at`, `updated_at`.
|
|
173
|
-
*
|
|
174
|
-
* Not a guess from the name: `autoValue` says the column is written by the
|
|
175
|
-
* system on create or update, so the field is never typed. Rendering it as an
|
|
176
|
-
* input in the middle of the form promised an edit that cannot happen, and the
|
|
177
|
-
* record block already shows both values — the same two dates twice on one
|
|
178
|
-
* screen.
|
|
179
|
-
*/
|
|
180
|
-
export function isAuditTimestamp(property: Property | undefined): boolean {
|
|
181
|
-
return Boolean(
|
|
182
|
-
property
|
|
183
|
-
&& property.type === "date"
|
|
184
|
-
&& property.autoValue
|
|
185
|
-
);
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
/**
|
|
189
|
-
* Close the trailing gap in each row.
|
|
190
|
-
*
|
|
191
|
-
* Without this a section of half-width fields with an odd count leaves a hole —
|
|
192
|
-
* `name | sku` then `brand | ␣␣` — a field stranded beside half a row of
|
|
193
|
-
* nothing, which reads as a mistake rather than a layout. The remainder is
|
|
194
|
-
* spread across the row's derived fields rather than dumped on the last one, so
|
|
195
|
-
* a lone half-width field becomes full width and a pair grows evenly. Fields
|
|
196
|
-
* with an explicit span are never resized.
|
|
197
|
-
*/
|
|
198
|
-
export function fillRows(fields: ResolvedFormField[]): ResolvedFormField[] {
|
|
199
|
-
const rows: ResolvedFormField[][] = [];
|
|
200
|
-
let row: ResolvedFormField[] = [];
|
|
201
|
-
let used = 0;
|
|
202
|
-
|
|
203
|
-
for (const field of fields) {
|
|
204
|
-
if (used + field.span > GRID_COLUMNS && row.length) {
|
|
205
|
-
rows.push(row);
|
|
206
|
-
row = [];
|
|
207
|
-
used = 0;
|
|
208
|
-
}
|
|
209
|
-
row.push(field);
|
|
210
|
-
used += field.span;
|
|
211
|
-
}
|
|
212
|
-
if (row.length) rows.push(row);
|
|
213
|
-
|
|
214
|
-
return rows.flatMap(entries => {
|
|
215
|
-
const total = entries.reduce((sum, f) => sum + f.span, 0);
|
|
216
|
-
let gap = GRID_COLUMNS - total;
|
|
217
|
-
if (gap <= 0) return entries;
|
|
218
|
-
|
|
219
|
-
// Spread the remainder across the row's derived fields rather than
|
|
220
|
-
// dumping it all on the last one: two half-width fields beside a gap
|
|
221
|
-
// become two three-quarter fields, not one full-width and one half.
|
|
222
|
-
// Fields with an explicit span keep it — the author picked that width.
|
|
223
|
-
const growable = entries
|
|
224
|
-
.map((f, i) => [f, i] as const)
|
|
225
|
-
.filter(([f]) => !f.spanExplicit);
|
|
226
|
-
|
|
227
|
-
if (!growable.length) return entries;
|
|
228
|
-
|
|
229
|
-
const next = [...entries];
|
|
230
|
-
let cursor = 0;
|
|
231
|
-
while (gap > 0) {
|
|
232
|
-
const [, index] = growable[cursor % growable.length];
|
|
233
|
-
next[index] = { ...next[index], span: (next[index].span + 1) as PropertySpan };
|
|
234
|
-
gap -= 1;
|
|
235
|
-
cursor += 1;
|
|
236
|
-
}
|
|
237
|
-
return next;
|
|
238
|
-
});
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
/* -------------------------------------------------------------------------- */
|
|
242
|
-
/* resolution */
|
|
243
|
-
/* -------------------------------------------------------------------------- */
|
|
244
|
-
|
|
245
|
-
function buildField<M extends Record<string, unknown>>(
|
|
246
|
-
key: string,
|
|
247
|
-
collection: AdminCollection<M>,
|
|
248
|
-
titlePropertyKey: string | undefined
|
|
249
|
-
): ResolvedFormField | undefined {
|
|
250
|
-
const property = collection.properties?.[key] as Property | undefined;
|
|
251
|
-
|
|
252
|
-
if (!property) {
|
|
253
|
-
// An additionalFields entry: a rendered card, not an input. Full width,
|
|
254
|
-
// because we have no idea what the Builder puts inside it.
|
|
255
|
-
const additional = collection.additionalFields?.some(f => f.key === key);
|
|
256
|
-
return additional ? { key, span: 4, additional: true } : undefined;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
if (isHidden(property)) return undefined;
|
|
260
|
-
|
|
261
|
-
const admin = property.admin;
|
|
262
|
-
const explicit = admin?.span !== undefined;
|
|
263
|
-
const span: PropertySpan = admin?.span ?? deriveSpan(property, key === titlePropertyKey);
|
|
264
|
-
|
|
265
|
-
return { key, span, additional: false, spanExplicit: explicit };
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
/**
|
|
269
|
-
* Resolve the layout for a collection's generated form.
|
|
270
|
-
*
|
|
271
|
-
* Never drops a field: anything not named by a section lands in a trailing
|
|
272
|
-
* group, so adding a column to the database cannot make it silently invisible
|
|
273
|
-
* in the panel.
|
|
274
|
-
*/
|
|
275
|
-
export function resolveFormLayout<M extends Record<string, unknown>>({
|
|
276
|
-
collection,
|
|
277
|
-
fieldKeys,
|
|
278
|
-
status
|
|
279
|
-
}: ResolveFormLayoutParams<M>): ResolvedFormLayout {
|
|
280
|
-
|
|
281
|
-
const config = collection.form;
|
|
282
|
-
const titlePropertyKey = getDisplayPropertyKey(collection, "title");
|
|
283
|
-
|
|
284
|
-
const available = new Map<string, ResolvedFormField>();
|
|
285
|
-
for (const key of fieldKeys) {
|
|
286
|
-
const field = buildField(key, collection, titlePropertyKey);
|
|
287
|
-
if (field) available.set(key, field);
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
/* ---- the id ---------------------------------------------------------- */
|
|
291
|
-
// A single surrogate key is routed out of the form and into the record
|
|
292
|
-
// block, unless it is still typeable. This is what `hideIdFromForm` used to
|
|
293
|
-
// be for; that flag now only decides whether the record block shows it.
|
|
294
|
-
//
|
|
295
|
-
// A COMPOSITE key is not. Postgres has no `id` — a row is addressed by one
|
|
296
|
-
// or more real columns, and `entity.id` is a token we synthesise on top
|
|
297
|
-
// (`a:::b`, see `buildCompositeId`). When the key spans several columns
|
|
298
|
-
// those columns are data: on a junction row, `order_id` and `product_id`
|
|
299
|
-
// are *which order* and *which product*. Hiding them behind an address
|
|
300
|
-
// nobody can read would remove the only meaningful thing on the form.
|
|
301
|
-
const idKeys = [...available.keys()].filter(key =>
|
|
302
|
-
isIdProperty(collection.properties?.[key] as Property | undefined));
|
|
303
|
-
|
|
304
|
-
if (idKeys.length === 1) {
|
|
305
|
-
const property = collection.properties?.[idKeys[0]] as Property;
|
|
306
|
-
if (!isIdPropertyEditable(property, status)) {
|
|
307
|
-
available.delete(idKeys[0]);
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
/* ---- audit timestamps ------------------------------------------------ */
|
|
312
|
-
// Same treatment as the id, and for the same reason: the record block shows
|
|
313
|
-
// them, so leaving them in the column renders each date twice — once as a
|
|
314
|
-
// disabled input the user cannot change, once as metadata.
|
|
315
|
-
const showRecordMetaResolved = config?.showRecordMeta ?? !collection.hideIdFromForm;
|
|
316
|
-
if (showRecordMetaResolved) {
|
|
317
|
-
for (const key of [...available.keys()]) {
|
|
318
|
-
const property = collection.properties?.[key] as Property | undefined;
|
|
319
|
-
// An explicit `sidebar` entry wins — the author asked for it there.
|
|
320
|
-
if (config?.sidebar?.includes(key as never)) continue;
|
|
321
|
-
if (isAuditTimestamp(property)) available.delete(key);
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
/* ---- the rail -------------------------------------------------------- */
|
|
326
|
-
const sidebar: ResolvedFormField[] = [];
|
|
327
|
-
if (config?.sidebar) {
|
|
328
|
-
for (const key of config.sidebar as string[]) {
|
|
329
|
-
const field = available.get(key);
|
|
330
|
-
if (!field) continue; // unknown, hidden, or already the id
|
|
331
|
-
sidebar.push(field);
|
|
332
|
-
available.delete(key);
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
const showRecordMeta = showRecordMetaResolved;
|
|
337
|
-
|
|
338
|
-
/* ---- sections -------------------------------------------------------- */
|
|
339
|
-
const sections: ResolvedFormSection[] = [];
|
|
340
|
-
|
|
341
|
-
if (config?.sections?.length) {
|
|
342
|
-
for (const section of config.sections as FormSection<M>[]) {
|
|
343
|
-
const fields: ResolvedFormField[] = [];
|
|
344
|
-
for (const key of section.properties as string[]) {
|
|
345
|
-
const field = available.get(key);
|
|
346
|
-
if (!field) continue;
|
|
347
|
-
fields.push(field);
|
|
348
|
-
available.delete(key);
|
|
349
|
-
}
|
|
350
|
-
const titled = Boolean(section.title);
|
|
351
|
-
sections.push({
|
|
352
|
-
key: section.key,
|
|
353
|
-
title: section.title,
|
|
354
|
-
collapsible: section.collapsible ?? titled,
|
|
355
|
-
collapsed: titled ? Boolean(section.collapsed) : false,
|
|
356
|
-
fields,
|
|
357
|
-
readVariant: section.readVariant
|
|
358
|
-
});
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
// Whatever no section claimed. Appended rather than dropped — a new column
|
|
363
|
-
// showing up unstyled is recoverable, a new column vanishing is not.
|
|
364
|
-
const leftovers = fieldKeys
|
|
365
|
-
.map(key => available.get(key))
|
|
366
|
-
.filter((f): f is ResolvedFormField => Boolean(f));
|
|
367
|
-
|
|
368
|
-
if (leftovers.length) {
|
|
369
|
-
// Always a trailing group, never merged into an existing section. Folding
|
|
370
|
-
// them into the first untitled section put `created_at`/`updated_at`
|
|
371
|
-
// between the product's images and its pricing — the reader cannot tell
|
|
372
|
-
// "we grouped this here" from "nobody placed this yet", and the second is
|
|
373
|
-
// what actually happened.
|
|
374
|
-
sections.push({
|
|
375
|
-
key: sections.length ? "__other" : "__main",
|
|
376
|
-
collapsible: false,
|
|
377
|
-
collapsed: false,
|
|
378
|
-
fields: leftovers
|
|
379
|
-
});
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
// A configured section that ended up empty (every key hidden, unknown, or
|
|
383
|
-
// claimed by the rail) would render as a heading over nothing.
|
|
384
|
-
const nonEmpty = sections
|
|
385
|
-
.filter(s => s.fields.length > 0)
|
|
386
|
-
.map(s => ({ ...s, fields: fillRows(s.fields) }));
|
|
387
|
-
|
|
388
|
-
return {
|
|
389
|
-
sections: nonEmpty,
|
|
390
|
-
sidebar,
|
|
391
|
-
showRecordMeta,
|
|
392
|
-
hasRail: sidebar.length > 0 || showRecordMeta
|
|
393
|
-
};
|
|
394
|
-
}
|
package/src/collections/index.ts
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
export * from "./collection_view_config";
|
|
2
|
-
export * from "./entity_image_preview";
|
|
3
|
-
export * from "./entity-display";
|
|
4
|
-
export * from "./entity-display-cache";
|
|
5
|
-
export * from "./filter-operator-resolution";
|
|
6
|
-
export * from "./form-layout";
|
|
7
|
-
export * from "./navigation_from_path";
|
|
8
|
-
export * from "./navigation_utils";
|
|
9
|
-
export * from "./parent_references_from_path";
|
|
10
|
-
export * from "./property-path";
|
|
11
|
-
export * from "./property_presentation";
|
|
12
|
-
export * from "./summary-property";
|
|
13
|
-
export * from "./title-property";
|