@rebasepro/app 0.17.3 → 0.18.1

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 (185) hide show
  1. package/README.md +4 -1
  2. package/dist/components/index.d.ts +0 -3
  3. package/dist/contexts/AuthControllerContext.d.ts +2 -1
  4. package/dist/contexts/CustomizationControllerContext.d.ts +2 -1
  5. package/dist/contexts/DialogsProvider.d.ts +2 -1
  6. package/dist/contexts/RebaseDataContext.d.ts +10 -1
  7. package/dist/contexts/StorageSourceContext.d.ts +11 -1
  8. package/dist/core/RebaseProps.d.ts +2 -12
  9. package/dist/debug/index.d.ts +19 -0
  10. package/dist/debug.js +3631 -0
  11. package/dist/hooks/data/delete.d.ts +16 -7
  12. package/dist/hooks/data/save.d.ts +18 -4
  13. package/dist/hooks/index.d.ts +1 -0
  14. package/dist/hooks/useAdminModeController.d.ts +5 -4
  15. package/dist/hooks/useNavigationGroupLabel.d.ts +17 -0
  16. package/dist/hooks/useStudioBridge.d.ts +33 -0
  17. package/dist/index.es.js +2599 -6980
  18. package/dist/index.es.js.map +1 -1
  19. package/dist/vitePlugin.d.ts +22 -0
  20. package/dist/vitePlugin.js +40 -1
  21. package/package.json +42 -31
  22. package/src/auth/api.ts +0 -134
  23. package/src/auth/index.ts +0 -26
  24. package/src/auth/types.ts +0 -115
  25. package/src/auth/useRebaseAuthController.ts +0 -345
  26. package/src/collections/collection_view_config.ts +0 -76
  27. package/src/collections/entity-display-cache.ts +0 -195
  28. package/src/collections/entity-display.ts +0 -77
  29. package/src/collections/entity_image_preview.ts +0 -61
  30. package/src/collections/filter-operator-resolution.ts +0 -119
  31. package/src/collections/form-layout.ts +0 -394
  32. package/src/collections/index.ts +0 -13
  33. package/src/collections/navigation_from_path.ts +0 -123
  34. package/src/collections/navigation_utils.ts +0 -171
  35. package/src/collections/parent_references_from_path.ts +0 -59
  36. package/src/collections/property-path.ts +0 -30
  37. package/src/collections/property_presentation.ts +0 -264
  38. package/src/collections/summary-property.ts +0 -114
  39. package/src/collections/title-property.ts +0 -312
  40. package/src/components/AIIcon.tsx +0 -47
  41. package/src/components/AuthSimulationMenu.tsx +0 -0
  42. package/src/components/ConfirmationDialog.tsx +0 -48
  43. package/src/components/Debug/UIReferenceView.tsx +0 -1053
  44. package/src/components/Debug/UIStyleGuide.tsx +0 -159
  45. package/src/components/Debug/collection-views/CardViewDemo.tsx +0 -87
  46. package/src/components/Debug/collection-views/CollectionTableDemo.tsx +0 -107
  47. package/src/components/Debug/collection-views/KanbanBoardDemo.tsx +0 -89
  48. package/src/components/Debug/collection-views/index.ts +0 -3
  49. package/src/components/Debug/collection-views/sample_data.ts +0 -44
  50. package/src/components/Debug/crm-dashboard/CalendarWidget.tsx +0 -387
  51. package/src/components/Debug/crm-dashboard/CrmDashboardDemo.tsx +0 -1100
  52. package/src/components/Debug/crm-dashboard/DashboardMetrics.tsx +0 -131
  53. package/src/components/Debug/crm-dashboard/PipelineOverview.tsx +0 -288
  54. package/src/components/Debug/crm-dashboard/RecentActivity.tsx +0 -213
  55. package/src/components/Debug/crm-dashboard/TaskFilters.tsx +0 -182
  56. package/src/components/Debug/crm-dashboard/TaskTable.tsx +0 -425
  57. package/src/components/Debug/crm-dashboard/TasksView.tsx +0 -234
  58. package/src/components/ErrorTooltip.tsx +0 -12
  59. package/src/components/ErrorView.tsx +0 -94
  60. package/src/components/LanguageToggle.tsx +0 -59
  61. package/src/components/LoginView/LoginView.tsx +0 -1235
  62. package/src/components/LoginView/index.ts +0 -2
  63. package/src/components/LoginView/oauth-redirect-flow.ts +0 -184
  64. package/src/components/NotFoundPage.tsx +0 -27
  65. package/src/components/RebaseAuth.tsx +0 -27
  66. package/src/components/RebaseLogo.tsx +0 -88
  67. package/src/components/SchemaDriftBanner.tsx +0 -102
  68. package/src/components/UnsavedChangesDialog.tsx +0 -46
  69. package/src/components/UserDisplay.tsx +0 -54
  70. package/src/components/UserSelectPopover.tsx +0 -399
  71. package/src/components/UserSettingsView.tsx +0 -318
  72. package/src/components/common/index.ts +0 -6
  73. package/src/components/common/table_height.tsx +0 -21
  74. package/src/components/common/types.tsx +0 -66
  75. package/src/components/common/useColumnsIds.tsx +0 -261
  76. package/src/components/common/useDataTableController.tsx +0 -571
  77. package/src/components/common/useDebouncedData.ts +0 -49
  78. package/src/components/common/useScrollRestoration.tsx +0 -68
  79. package/src/components/index.tsx +0 -26
  80. package/src/contexts/AdminModeController.tsx +0 -11
  81. package/src/contexts/AnalyticsContext.tsx +0 -4
  82. package/src/contexts/AuthControllerContext.tsx +0 -4
  83. package/src/contexts/CollectionResolverContext.tsx +0 -24
  84. package/src/contexts/CollectionScopeContext.tsx +0 -68
  85. package/src/contexts/ComponentOverrideContext.tsx +0 -81
  86. package/src/contexts/CustomizationControllerContext.tsx +0 -4
  87. package/src/contexts/DataDriverContext.tsx +0 -4
  88. package/src/contexts/DataSourcesContext.tsx +0 -36
  89. package/src/contexts/DatabaseAdminContext.tsx +0 -4
  90. package/src/contexts/DialogsProvider.tsx +0 -56
  91. package/src/contexts/EffectiveRoleController.tsx +0 -12
  92. package/src/contexts/ModeController.tsx +0 -11
  93. package/src/contexts/RebaseClientInstanceContext.tsx +0 -7
  94. package/src/contexts/RebaseDataContext.tsx +0 -4
  95. package/src/contexts/SnackbarProvider.tsx +0 -15
  96. package/src/contexts/StorageSourceContext.tsx +0 -4
  97. package/src/contexts/StorageSourcesContext.tsx +0 -30
  98. package/src/contexts/UserConfigurationPersistenceContext.tsx +0 -4
  99. package/src/contexts/index.ts +0 -18
  100. package/src/core/PluginLifecycleManager.tsx +0 -96
  101. package/src/core/PluginProviderStack.tsx +0 -40
  102. package/src/core/Rebase.tsx +0 -488
  103. package/src/core/RebaseProps.tsx +0 -333
  104. package/src/core/RebaseRouter.tsx +0 -18
  105. package/src/core/RebaseRoutes.tsx +0 -24
  106. package/src/core/index.tsx +0 -5
  107. package/src/hooks/ApiConfigContext.tsx +0 -83
  108. package/src/hooks/data/collectionQuery.ts +0 -55
  109. package/src/hooks/data/delete.ts +0 -56
  110. package/src/hooks/data/save.ts +0 -78
  111. package/src/hooks/data/useCollection.tsx +0 -183
  112. package/src/hooks/data/useData.tsx +0 -18
  113. package/src/hooks/data/useDataOrder.ts +0 -26
  114. package/src/hooks/data/useFetch.tsx +0 -156
  115. package/src/hooks/data/useRelationSelector.tsx +0 -280
  116. package/src/hooks/index.tsx +0 -45
  117. package/src/hooks/useAdminModeController.tsx +0 -23
  118. package/src/hooks/useAnalyticsController.tsx +0 -8
  119. package/src/hooks/useAuthController.tsx +0 -15
  120. package/src/hooks/useAuthSubscription.ts +0 -130
  121. package/src/hooks/useBackendStorageSource.ts +0 -295
  122. package/src/hooks/useBridgeRegistration.tsx +0 -32
  123. package/src/hooks/useBrowserTitleAndIcon.tsx +0 -23
  124. package/src/hooks/useBuildAdminModeController.tsx +0 -29
  125. package/src/hooks/useBuildEffectiveRoleController.tsx +0 -30
  126. package/src/hooks/useBuildLocalConfigurationPersistence.tsx +0 -83
  127. package/src/hooks/useBuildModeController.tsx +0 -73
  128. package/src/hooks/useClipboard.tsx +0 -158
  129. package/src/hooks/useCollapsedGroups.ts +0 -116
  130. package/src/hooks/useComponentOverride.tsx +0 -59
  131. package/src/hooks/useCustomizationController.tsx +0 -14
  132. package/src/hooks/useDialogsController.tsx +0 -15
  133. package/src/hooks/useEffectiveRoleController.tsx +0 -10
  134. package/src/hooks/useLargeLayout.tsx +0 -65
  135. package/src/hooks/useModeController.tsx +0 -23
  136. package/src/hooks/useNavigationBlocker.tsx +0 -108
  137. package/src/hooks/usePermissions.ts +0 -45
  138. package/src/hooks/useRebaseClient.tsx +0 -10
  139. package/src/hooks/useRebaseContext.tsx +0 -84
  140. package/src/hooks/useRebaseRegistry.tsx +0 -91
  141. package/src/hooks/useResolvedComponent.tsx +0 -195
  142. package/src/hooks/useSlot.tsx +0 -64
  143. package/src/hooks/useSnackbarController.tsx +0 -80
  144. package/src/hooks/useStorageSource.tsx +0 -11
  145. package/src/hooks/useStudioBridge.tsx +0 -258
  146. package/src/hooks/useTranslation.ts +0 -38
  147. package/src/hooks/useUnsavedChangesDialog.tsx +0 -67
  148. package/src/hooks/useUserConfigurationPersistence.tsx +0 -11
  149. package/src/i18n/RebaseI18nProvider.tsx +0 -159
  150. package/src/index.ts +0 -32
  151. package/src/internal/common.tsx +0 -28
  152. package/src/internal/useRestoreScroll.tsx +0 -69
  153. package/src/locales/de.ts +0 -941
  154. package/src/locales/en.ts +0 -1021
  155. package/src/locales/es.ts +0 -962
  156. package/src/locales/fr.ts +0 -941
  157. package/src/locales/hi.ts +0 -941
  158. package/src/locales/it.ts +0 -941
  159. package/src/locales/pt.ts +0 -949
  160. package/src/util/constants.ts +0 -7
  161. package/src/util/createFormexStub.tsx +0 -66
  162. package/src/util/entity_cache.ts +0 -242
  163. package/src/util/enums.ts +0 -34
  164. package/src/util/icon_list.ts +0 -22
  165. package/src/util/icons.tsx +0 -129
  166. package/src/util/index.ts +0 -13
  167. package/src/util/local_storage.ts +0 -66
  168. package/src/util/previews.ts +0 -127
  169. package/src/util/useStorageUploadController.tsx +0 -354
  170. package/src/vitePlugin.ts +0 -215
  171. /package/dist/{components/Debug → debug}/UIReferenceView.d.ts +0 -0
  172. /package/dist/{components/Debug → debug}/UIStyleGuide.d.ts +0 -0
  173. /package/dist/{components/Debug → debug}/collection-views/CardViewDemo.d.ts +0 -0
  174. /package/dist/{components/Debug → debug}/collection-views/CollectionTableDemo.d.ts +0 -0
  175. /package/dist/{components/Debug → debug}/collection-views/KanbanBoardDemo.d.ts +0 -0
  176. /package/dist/{components/Debug → debug}/collection-views/index.d.ts +0 -0
  177. /package/dist/{components/Debug → debug}/collection-views/sample_data.d.ts +0 -0
  178. /package/dist/{components/Debug → debug}/crm-dashboard/CalendarWidget.d.ts +0 -0
  179. /package/dist/{components/Debug → debug}/crm-dashboard/CrmDashboardDemo.d.ts +0 -0
  180. /package/dist/{components/Debug → debug}/crm-dashboard/DashboardMetrics.d.ts +0 -0
  181. /package/dist/{components/Debug → debug}/crm-dashboard/PipelineOverview.d.ts +0 -0
  182. /package/dist/{components/Debug → debug}/crm-dashboard/RecentActivity.d.ts +0 -0
  183. /package/dist/{components/Debug → debug}/crm-dashboard/TaskFilters.d.ts +0 -0
  184. /package/dist/{components/Debug → debug}/crm-dashboard/TaskTable.d.ts +0 -0
  185. /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
- }
@@ -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";