@rebasepro/admin 0.16.0 → 0.16.1-canary.g0d7af95

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 (180) hide show
  1. package/dist/{CollectionEditorDialog-D2Vf4C3-.js → CollectionEditorDialog-Dt_Wv20U.js} +7 -3
  2. package/dist/{CollectionEditorDialog-D2Vf4C3-.js.map → CollectionEditorDialog-Dt_Wv20U.js.map} +1 -1
  3. package/dist/{PropertyEditView-D3NBsK9_.js → PropertyEditView-C4IltWX5.js} +2 -2
  4. package/dist/{PropertyEditView-D3NBsK9_.js.map → PropertyEditView-C4IltWX5.js.map} +1 -1
  5. package/dist/{RouterCollectionsStudioView-hPB54l3Q.js → RouterCollectionsStudioView-C-BHWJ0J.js} +4 -4
  6. package/dist/{RouterCollectionsStudioView-hPB54l3Q.js.map → RouterCollectionsStudioView-C-BHWJ0J.js.map} +1 -1
  7. package/dist/SchemaChangeDialog-DceT8QFX.js +394 -0
  8. package/dist/SchemaChangeDialog-DceT8QFX.js.map +1 -0
  9. package/dist/collection_editor/ConfigControllerProvider.d.ts +4 -4
  10. package/dist/collection_editor/_cms_internals.d.ts +23 -23
  11. package/dist/collection_editor/api/index.d.ts +1 -1
  12. package/dist/collection_editor/extensibility_types.d.ts +1 -1
  13. package/dist/collection_editor/index.d.ts +21 -21
  14. package/dist/collection_editor/liveSchemaClient.d.ts +182 -0
  15. package/dist/collection_editor/serializable_types.d.ts +0 -1
  16. package/dist/collection_editor/serializable_utils.d.ts +1 -1
  17. package/dist/collection_editor/types/collection_editor_controller.d.ts +1 -1
  18. package/dist/collection_editor/types/config_controller.d.ts +11 -0
  19. package/dist/collection_editor/ui/collection_editor/AICollectionGeneratorPopover.d.ts +1 -1
  20. package/dist/collection_editor/ui/collection_editor/AIModifiedPathsContext.d.ts +1 -1
  21. package/dist/collection_editor/ui/collection_editor/CollectionEditorDialog.d.ts +4 -4
  22. package/dist/collection_editor/ui/collection_editor/CollectionEditorWelcomeView.d.ts +1 -1
  23. package/dist/collection_editor/ui/collection_editor/CollectionPropertiesEditorForm.d.ts +2 -2
  24. package/dist/collection_editor/ui/collection_editor/CollectionStudioView.d.ts +1 -1
  25. package/dist/collection_editor/ui/collection_editor/CollectionsStudioView.d.ts +2 -2
  26. package/dist/collection_editor/ui/collection_editor/GeneralSettingsForm.d.ts +1 -1
  27. package/dist/collection_editor/ui/collection_editor/PropertyEditView.d.ts +2 -2
  28. package/dist/collection_editor/ui/collection_editor/RouterCollectionStudioView.d.ts +1 -1
  29. package/dist/collection_editor/ui/collection_editor/RouterCollectionsStudioView.d.ts +1 -1
  30. package/dist/collection_editor/ui/collection_editor/SchemaChangeDialog.d.ts +85 -0
  31. package/dist/collection_editor/ui/collection_editor/SubcollectionsEditTab.d.ts +2 -2
  32. package/dist/collection_editor/ui/collection_editor/import/CollectionEditorImportDataPreview.d.ts +1 -1
  33. package/dist/collection_editor/ui/collection_editor/import/CollectionEditorImportMapping.d.ts +1 -1
  34. package/dist/collection_editor/ui/collection_editor/import/clean_import_data.d.ts +1 -1
  35. package/dist/collection_editor/ui/collection_editor/properties/conditions/index.d.ts +6 -6
  36. package/dist/collection_editor/useCollectionEditorController.d.ts +1 -1
  37. package/dist/collection_editor/useCollectionsConfigController.d.ts +1 -1
  38. package/dist/collection_editor/useJsonCollectionsConfigController.d.ts +2 -2
  39. package/dist/collection_editor/useLiveSchemaEditing.d.ts +71 -0
  40. package/dist/collection_editor/useLocalCollectionsConfigController.d.ts +1 -1
  41. package/dist/collection_editor_ui.d.ts +12 -12
  42. package/dist/collection_editor_ui.js +4 -4
  43. package/dist/components/CollectionTableBinding/CollectionTableBinding.d.ts +1 -1
  44. package/dist/components/CollectionTableBinding/PropertyTableCell.d.ts +1 -1
  45. package/dist/components/CollectionTableBinding/fields/TableStorageUpload.d.ts +1 -1
  46. package/dist/components/CollectionTableBinding/index.d.ts +5 -5
  47. package/dist/components/CollectionTableBinding/internal/popup_field/PopupFormField.d.ts +1 -1
  48. package/dist/components/CollectionViewBinding/SlotValue.d.ts +2 -2
  49. package/dist/components/CollectionViewBinding/hooks/useKanbanDragAndDrop.d.ts +1 -1
  50. package/dist/components/DrawerNavigationGroup.d.ts +0 -9
  51. package/dist/components/DrawerNavigationItem.d.ts +0 -4
  52. package/dist/components/EditFormActions.d.ts +1 -1
  53. package/dist/components/EditViewBinding.d.ts +3 -3
  54. package/dist/components/EntityInspector.d.ts +1 -1
  55. package/dist/components/EntityPreviewBinding.d.ts +1 -1
  56. package/dist/components/EntityViewBinding.d.ts +1 -1
  57. package/dist/components/HomePage/index.d.ts +4 -4
  58. package/dist/components/PropertyCollectionView.d.ts +1 -1
  59. package/dist/components/ReferenceWidget.d.ts +1 -1
  60. package/dist/components/RelationSelector.d.ts +0 -2
  61. package/dist/components/admin/index.d.ts +1 -1
  62. package/dist/components/app/AppBar.d.ts +1 -1
  63. package/dist/components/app/index.d.ts +4 -4
  64. package/dist/components/common/index.d.ts +1 -1
  65. package/dist/components/field_configs.d.ts +1 -1
  66. package/dist/components/history/EntityHistoryEntry.d.ts +2 -2
  67. package/dist/components/history/index.d.ts +5 -5
  68. package/dist/components/index.d.ts +45 -45
  69. package/dist/data_export/export/index.d.ts +4 -4
  70. package/dist/data_export/index.d.ts +1 -1
  71. package/dist/data_import/components/DataNewPropertiesMapping.d.ts +1 -1
  72. package/dist/data_import/components/ImportSaveInProgress.d.ts +1 -1
  73. package/dist/data_import/components/index.d.ts +4 -4
  74. package/dist/data_import/hooks/index.d.ts +1 -1
  75. package/dist/data_import/hooks/useImportConfig.d.ts +1 -1
  76. package/dist/data_import/import/ImportCollectionAction.d.ts +1 -1
  77. package/dist/data_import/import/index.d.ts +1 -1
  78. package/dist/data_import/index.d.ts +5 -5
  79. package/dist/data_import/types/index.d.ts +1 -1
  80. package/dist/data_import/utils/file_to_json.d.ts +1 -1
  81. package/dist/data_import/utils/index.d.ts +5 -5
  82. package/dist/editor/components/SlashCommandMenu.d.ts +2 -2
  83. package/dist/editor/components/index.d.ts +5 -5
  84. package/dist/editor/editor.d.ts +2 -2
  85. package/dist/editor/index.d.ts +2 -2
  86. package/dist/editor/nodeViews/ImageComponent.d.ts +1 -1
  87. package/dist/editor/nodeViews/TaskItemComponent.d.ts +1 -1
  88. package/dist/editor/types.d.ts +1 -1
  89. package/dist/{export-oauWlxuu.js → export-CF1-p5nY.js} +2 -2
  90. package/dist/{export-oauWlxuu.js.map → export-CF1-p5nY.js.map} +1 -1
  91. package/dist/form/EntityForm.d.ts +1 -1
  92. package/dist/form/EntityFormActions.d.ts +1 -1
  93. package/dist/form/EntityFormBinding.d.ts +1 -1
  94. package/dist/form/PropertyFieldBinding.d.ts +1 -1
  95. package/dist/form/components/StorageItemPreview.d.ts +1 -1
  96. package/dist/form/components/index.d.ts +6 -6
  97. package/dist/form/field_bindings/ArrayCustomShapedFieldBinding.d.ts +1 -1
  98. package/dist/form/field_bindings/ArrayOfReferencesFieldBinding.d.ts +1 -1
  99. package/dist/form/field_bindings/BinaryFieldBinding.d.ts +1 -1
  100. package/dist/form/field_bindings/BlockFieldBinding.d.ts +1 -1
  101. package/dist/form/field_bindings/DateTimeFieldBinding.d.ts +1 -1
  102. package/dist/form/field_bindings/GeopointFieldBinding.d.ts +1 -1
  103. package/dist/form/field_bindings/KeyValueFieldBinding.d.ts +1 -1
  104. package/dist/form/field_bindings/MapFieldBinding.d.ts +1 -1
  105. package/dist/form/field_bindings/MarkdownEditorFieldBinding.d.ts +2 -2
  106. package/dist/form/field_bindings/MultiSelectFieldBinding.d.ts +1 -1
  107. package/dist/form/field_bindings/MultipleRelationFieldBinding.d.ts +1 -1
  108. package/dist/form/field_bindings/ReadOnlyFieldBinding.d.ts +1 -1
  109. package/dist/form/field_bindings/ReferenceFieldBinding.d.ts +1 -1
  110. package/dist/form/field_bindings/RelationFieldBinding.d.ts +1 -1
  111. package/dist/form/field_bindings/RepeatFieldBinding.d.ts +1 -1
  112. package/dist/form/field_bindings/SelectFieldBinding.d.ts +1 -1
  113. package/dist/form/field_bindings/StorageUploadFieldBinding.d.ts +1 -1
  114. package/dist/form/field_bindings/SwitchFieldBinding.d.ts +1 -1
  115. package/dist/form/field_bindings/TextFieldBinding.d.ts +1 -1
  116. package/dist/form/field_bindings/UserSelectFieldBinding.d.ts +1 -1
  117. package/dist/form/field_bindings/VectorFieldBinding.d.ts +1 -1
  118. package/dist/form/index.d.ts +24 -24
  119. package/dist/{history-C2cqc0bw.js → history-BoT8GhO1.js} +2 -2
  120. package/dist/{history-C2cqc0bw.js.map → history-BoT8GhO1.js.map} +1 -1
  121. package/dist/hooks/index.d.ts +16 -16
  122. package/dist/hooks/navigation/contexts/index.d.ts +3 -3
  123. package/dist/hooks/useBuildSideDialogsController.d.ts +1 -1
  124. package/dist/hooks/useSelectionDialog.d.ts +1 -1
  125. package/dist/{import-Bxv5JPm-.js → import-Dg4X01sA.js} +2 -2
  126. package/dist/{import-Bxv5JPm-.js.map → import-Dg4X01sA.js.map} +1 -1
  127. package/dist/index.d.ts +17 -15
  128. package/dist/index.js +262 -21
  129. package/dist/index.js.map +1 -1
  130. package/dist/preview/PropertyPreview.d.ts +1 -1
  131. package/dist/preview/components/ArrayEnumPreview.d.ts +1 -1
  132. package/dist/preview/components/BooleanPreview.d.ts +1 -1
  133. package/dist/preview/components/ImagePreview.d.ts +1 -1
  134. package/dist/preview/components/ReferencePreview.d.ts +1 -1
  135. package/dist/preview/components/RelationPreview.d.ts +1 -1
  136. package/dist/preview/components/StorageThumbnail.d.ts +1 -1
  137. package/dist/preview/components/UrlComponentPreview.d.ts +2 -2
  138. package/dist/preview/components/UserPreview.d.ts +1 -1
  139. package/dist/preview/index.d.ts +24 -24
  140. package/dist/preview/property_previews/ArrayOfMapsPreview.d.ts +1 -1
  141. package/dist/preview/property_previews/ArrayOfReferencesPreview.d.ts +1 -1
  142. package/dist/preview/property_previews/ArrayOfRelationsPreview.d.ts +1 -1
  143. package/dist/preview/property_previews/ArrayOfStorageComponentsPreview.d.ts +1 -1
  144. package/dist/preview/property_previews/ArrayOfStringsPreview.d.ts +1 -1
  145. package/dist/preview/property_previews/ArrayOneOfPreview.d.ts +1 -1
  146. package/dist/preview/property_previews/ArrayPropertyEnumPreview.d.ts +1 -1
  147. package/dist/preview/property_previews/ArrayPropertyPreview.d.ts +1 -1
  148. package/dist/preview/property_previews/MapPropertyPreview.d.ts +1 -1
  149. package/dist/preview/property_previews/NumberPropertyPreview.d.ts +1 -1
  150. package/dist/preview/property_previews/SkeletonPropertyComponent.d.ts +1 -1
  151. package/dist/preview/property_previews/StringPropertyPreview.d.ts +1 -1
  152. package/dist/preview/util.d.ts +1 -1
  153. package/dist/routes/index.d.ts +2 -2
  154. package/dist/types/components/EntityFormActionsProps.d.ts +2 -2
  155. package/dist/types/components/EntityFormProps.d.ts +3 -3
  156. package/dist/types/components/index.d.ts +3 -3
  157. package/dist/types/fields.d.ts +1 -1
  158. package/dist/types/index.d.ts +2 -2
  159. package/dist/util/index.d.ts +5 -5
  160. package/dist/util/previews.d.ts +2 -2
  161. package/dist/{util-C4qsI1QV.js → util-DUIsQ2zh.js} +132 -9
  162. package/dist/util-DUIsQ2zh.js.map +1 -0
  163. package/package.json +11 -11
  164. package/src/collection_editor/ConfigControllerProvider.tsx +5 -0
  165. package/src/collection_editor/liveSchemaClient.ts +285 -0
  166. package/src/collection_editor/serializable_types.ts +0 -1
  167. package/src/collection_editor/serializable_utils.ts +0 -1
  168. package/src/collection_editor/types/config_controller.tsx +11 -0
  169. package/src/collection_editor/ui/collection_editor/CollectionEditorDialog.tsx +10 -0
  170. package/src/collection_editor/ui/collection_editor/SchemaChangeDialog.tsx +470 -0
  171. package/src/collection_editor/useLiveSchemaEditing.tsx +315 -0
  172. package/src/collection_editor/useLocalCollectionsConfigController.tsx +109 -10
  173. package/src/components/DrawerNavigationGroup.tsx +0 -9
  174. package/src/components/DrawerNavigationItem.tsx +0 -4
  175. package/src/components/RelationSelector.tsx +0 -2
  176. package/src/data_import/utils/data.ts +1 -1
  177. package/src/data_import/utils/file_to_json.ts +0 -3
  178. package/src/index.ts +6 -0
  179. package/src/util/previews.ts +4 -2
  180. package/dist/util-C4qsI1QV.js.map +0 -1
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@rebasepro/admin",
3
3
  "type": "module",
4
- "version": "0.16.0",
4
+ "version": "0.16.1-canary.g0d7af95",
5
5
  "description": "Rebase CMS — content management views, forms, and routing",
6
6
  "funding": {
7
7
  "url": "https://github.com/sponsors/rebaseco"
@@ -83,14 +83,14 @@
83
83
  "react-dropzone": "^19.1.1",
84
84
  "react-use-measure": "^2.1.7",
85
85
  "zod": "^4.4.3",
86
- "@rebasepro/admin-types": "0.16.0",
87
- "@rebasepro/app": "0.16.0",
88
- "@rebasepro/common": "0.16.0",
89
- "@rebasepro/forms": "0.16.0",
90
- "@rebasepro/inference": "0.16.0",
91
- "@rebasepro/ui": "0.16.0",
92
- "@rebasepro/types": "0.16.0",
93
- "@rebasepro/utils": "0.16.0"
86
+ "@rebasepro/common": "0.16.1-canary.g0d7af95",
87
+ "@rebasepro/forms": "0.16.1-canary.g0d7af95",
88
+ "@rebasepro/app": "0.16.1-canary.g0d7af95",
89
+ "@rebasepro/inference": "0.16.1-canary.g0d7af95",
90
+ "@rebasepro/types": "0.16.1-canary.g0d7af95",
91
+ "@rebasepro/ui": "0.16.1-canary.g0d7af95",
92
+ "@rebasepro/utils": "0.16.1-canary.g0d7af95",
93
+ "@rebasepro/admin-types": "0.16.1-canary.g0d7af95"
94
94
  },
95
95
  "peerDependencies": {
96
96
  "react": ">=19.2.7",
@@ -131,7 +131,7 @@
131
131
  "jest": {
132
132
  "transform": {
133
133
  "^.+\\.tsx?$": "ts-jest",
134
- "^.+\\.m?jsx?$": "<rootDir>/../../scripts/jest/react-router-esm-transform.cjs"
134
+ "^.+\\.m?jsx?$": "<rootDir>/../../tooling/scripts/jest/react-router-esm-transform.cjs"
135
135
  },
136
136
  "testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.tsx?$",
137
137
  "moduleFileExtensions": [
@@ -162,7 +162,7 @@
162
162
  },
163
163
  "scripts": {
164
164
  "watch": "vite build --watch",
165
- "build": "vite build && tsc --emitDeclarationOnly -p tsconfig.prod.json && node ../../scripts/assert-build-output.mjs",
165
+ "build": "vite build && tsc --emitDeclarationOnly -p tsconfig.prod.json && node ../../tooling/scripts/add-dts-extensions.mjs dist && node ../../tooling/scripts/assert-build-output.mjs",
166
166
  "test:lint": "eslint \"src/**\" --quiet",
167
167
  "test": "jest --passWithNoTests",
168
168
  "clean": "rm -rf dist && find ./src -name '*.js' -type f | xargs rm -f",
@@ -383,6 +383,11 @@ parentEntityIds,
383
383
  <CollectionEditorContext.Provider value={collectionEditorContextValue}>
384
384
  <CollectionEditorDialogsContext.Provider value={dialogsState}>
385
385
  {children}
386
+ {/* Rendered here rather than by each consumer: a
387
+ controller that has to ask before it writes needs
388
+ somewhere to ask, and this is the one component that
389
+ wraps every editor surface. */}
390
+ {collectionConfigController.dialog}
386
391
  </CollectionEditorDialogsContext.Provider>
387
392
  </CollectionEditorContext.Provider>
388
393
  </ConfigControllerContext.Provider>
@@ -0,0 +1,285 @@
1
+ /**
2
+ * The panel's client for live schema editing.
3
+ *
4
+ * Three calls, mirroring the routes: ask whether this backend can do it, ask
5
+ * what a change would do, then do it. `plan` and `apply` are deliberately
6
+ * separate here as well as on the server, because a UI that could only
7
+ * try-and-see would be asking somebody to discover that their change is refused
8
+ * by pressing a button on a live database.
9
+ *
10
+ * ## Partial payloads are the caller's problem, not this module's
11
+ *
12
+ * Every call takes the **whole** collection. Live editing computes the proposed
13
+ * state by replacing one collection in the set, so a patch would read as a set
14
+ * of removals — every property it does not mention — and be refused.
15
+ *
16
+ * The editor's own save path posts partials (`saveProperty` follows up with a
17
+ * one-key `collection/save` carrying `partial: true`), so
18
+ * `useLocalCollectionsConfigController` assembles the whole collection before
19
+ * it gets here. Accepting a patch and merging it in this module would put that
20
+ * reconstruction one layer away from the collections it needs to read.
21
+ */
22
+
23
+ /** A constraint the change asks for that the statements will not carry. */
24
+ export interface WithheldConstraint {
25
+ target: string;
26
+ kind: "not-null";
27
+ reason: string;
28
+ remedy: string;
29
+ }
30
+
31
+ /** What the server says a change would do. Mirrors `SchemaChangePlan`. */
32
+ export interface LiveSchemaPlan {
33
+ applicable: boolean;
34
+ verdict: "safe" | "diverges" | "needs-migration";
35
+ changes: Array<{
36
+ kind: string;
37
+ verdict: "safe" | "diverges" | "needs-migration";
38
+ collection: string;
39
+ property?: string;
40
+ detail: string;
41
+ remedy?: string;
42
+ }>;
43
+ statements: string[];
44
+ files: string[];
45
+ message: string;
46
+ /**
47
+ * Applicable, and still not fully enforced.
48
+ *
49
+ * Not a refusal, so it does not belong in `changes` — but it is the one
50
+ * thing on a plan somebody might want to stop and read, so the dialog gives
51
+ * it its own place rather than another bullet in the list.
52
+ */
53
+ withheldConstraints: WithheldConstraint[];
54
+ /**
55
+ * What still has to happen after this lands.
56
+ *
57
+ * Empty almost always. The case it exists for: a project that replays
58
+ * versioned migrations, where applying here changes *this* database and
59
+ * commits `drizzle/schema.sql`, but writes no migration — so the next
60
+ * environment built from migrations would not have the change.
61
+ */
62
+ followUp?: string[];
63
+ }
64
+
65
+ export interface LiveSchemaResult {
66
+ applied: boolean;
67
+ applyError?: string;
68
+ committed: { sha: string; branch: string; files: string[] };
69
+ statements: string[];
70
+ summary: string;
71
+ withheldConstraints: WithheldConstraint[];
72
+ followUp?: string[];
73
+ }
74
+
75
+ /**
76
+ * Whether this backend can edit its schema, and what is missing when it cannot.
77
+ *
78
+ * `canPlan` is separate from `enabled` deliberately: previewing a change needs
79
+ * no repository, so a deployment running from a bundle can still show somebody
80
+ * exactly what their change would do. Greying out the preview as well would be
81
+ * a worse answer than the truth.
82
+ */
83
+ export interface LiveSchemaStatus {
84
+ enabled: boolean;
85
+ canPlan: boolean;
86
+ /**
87
+ * Whether *this caller* may apply, as opposed to whether the server can.
88
+ *
89
+ * Applying commits to the repository under an author, and a credential is
90
+ * not an author — so an API key or the service key may preview a change and
91
+ * not make one. Reported here so the panel can grey out the button and say
92
+ * why, rather than letting somebody read a plan, decide, press, and only
93
+ * then be refused.
94
+ */
95
+ canApply: boolean;
96
+ applyRefusedBecause?: string;
97
+ applyRefusedCode?: string;
98
+ repository?: string;
99
+ /** The branch a commit would land on. */
100
+ branch?: string;
101
+ code?: string;
102
+ reason?: string;
103
+ }
104
+
105
+ /**
106
+ * Why live editing is not available here.
107
+ *
108
+ * Distinguished from an ordinary failure because both are expected states with
109
+ * their own explanation, and a UI should say which rather than showing a
110
+ * stack: a Mongo backend will never support this, and a bundle deployment has
111
+ * no source to edit.
112
+ */
113
+ export type LiveSchemaUnavailable =
114
+ | { reason: "unsupported"; message: string }
115
+ | { reason: "no-repository"; message: string };
116
+
117
+ export class LiveSchemaError extends Error {
118
+ constructor(
119
+ message: string,
120
+ readonly code: string | undefined,
121
+ readonly changes?: LiveSchemaPlan["changes"]
122
+ ) {
123
+ super(message);
124
+ this.name = "LiveSchemaError";
125
+ }
126
+ }
127
+
128
+ const UNAVAILABLE: Record<string, LiveSchemaUnavailable["reason"]> = {
129
+ SCHEMA_EDITING_UNSUPPORTED: "unsupported",
130
+ SCHEMA_EDITING_NO_REPOSITORY: "no-repository"
131
+ };
132
+
133
+ export interface LiveSchemaClientOptions {
134
+ /** Base for the routes, e.g. `https://api.example.com/api/schema`. */
135
+ baseUrl: string;
136
+ getAuthToken?: () => Promise<string | null> | string | null;
137
+ fetchImpl?: typeof fetch;
138
+ }
139
+
140
+ export interface ProposedCollectionChange {
141
+ collectionId: string;
142
+ /**
143
+ * The **whole** collection as it should end up — not a patch. See the
144
+ * module comment: a partial reads as a set of removals.
145
+ */
146
+ collection: Record<string, unknown>;
147
+ }
148
+
149
+ export function createLiveSchemaClient(options: LiveSchemaClientOptions) {
150
+ const doFetch = options.fetchImpl ?? fetch;
151
+ const base = options.baseUrl.replace(/\/$/, "");
152
+
153
+ const post = async <T>(path: string, body: ProposedCollectionChange): Promise<T> => {
154
+ const token = await options.getAuthToken?.();
155
+ const headers: Record<string, string> = { "Content-Type": "application/json" };
156
+ if (token) headers.Authorization = `Bearer ${token}`;
157
+
158
+ const response = await doFetch(`${base}${path}`, {
159
+ method: "POST",
160
+ headers,
161
+ body: JSON.stringify(body)
162
+ });
163
+
164
+ if (response.ok) return await response.json() as T;
165
+
166
+ // Parsed rather than assumed: the server answers a structured error, and
167
+ // the code is what tells an expected refusal from a broken request.
168
+ let payload: { error?: { code?: string; message?: string; details?: { changes?: LiveSchemaPlan["changes"] } } } = {};
169
+ try {
170
+ payload = await response.json() as typeof payload;
171
+ } catch {
172
+ payload = { error: { message: await response.text().catch(() => "") } };
173
+ }
174
+
175
+ throw new LiveSchemaError(
176
+ payload.error?.message || `Schema request failed with ${response.status}`,
177
+ payload.error?.code,
178
+ payload.error?.details?.changes
179
+ );
180
+ };
181
+
182
+ return {
183
+ /**
184
+ * Whether this backend can edit its schema.
185
+ *
186
+ * Never throws. Every way of failing to get an answer — a backend too
187
+ * old to have the endpoint, an unreachable one, a session that is not
188
+ * an admin — means the same thing to a caller deciding whether to offer
189
+ * the control, and turning that into an exception each caller has to
190
+ * catch would only produce four copies of this `catch`.
191
+ */
192
+ status: async (): Promise<LiveSchemaStatus> => {
193
+ try {
194
+ const token = await options.getAuthToken?.();
195
+ const response = await doFetch(`${base}/status`, {
196
+ headers: token ? { Authorization: `Bearer ${token}` } : {}
197
+ });
198
+ const body = await response.json().catch(() => ({})) as
199
+ Partial<LiveSchemaStatus> & { error?: { code?: string; message?: string } };
200
+
201
+ if (response.ok) {
202
+ return {
203
+ enabled: body.enabled === true,
204
+ canPlan: body.canPlan === true,
205
+ // Absent on a backend from before capabilities existed.
206
+ // Read as "yes" there, because on that server being
207
+ // through the admin gate *was* the whole permission —
208
+ // defaulting to no would take the feature away from
209
+ // every deployment that has not upgraded.
210
+ canApply: body.canApply ?? body.enabled === true,
211
+ applyRefusedBecause: body.applyRefusedBecause,
212
+ applyRefusedCode: body.applyRefusedCode,
213
+ repository: body.repository,
214
+ branch: body.branch,
215
+ code: body.code,
216
+ reason: body.reason
217
+ };
218
+ }
219
+ return {
220
+ enabled: false,
221
+ canPlan: false,
222
+ canApply: false,
223
+ code: body.error?.code,
224
+ reason: body.error?.message
225
+ ?? `The backend refused to say whether its schema is editable (HTTP ${response.status}).`
226
+ };
227
+ } catch (err) {
228
+ return {
229
+ enabled: false,
230
+ canPlan: false,
231
+ canApply: false,
232
+ reason: err instanceof Error ? err.message : "The backend could not be reached."
233
+ };
234
+ }
235
+ },
236
+
237
+ /** What the change would do. No side effects. */
238
+ plan: (change: ProposedCollectionChange) => post<LiveSchemaPlan>("/plan", change),
239
+
240
+ /** Commit the change, then apply it. */
241
+ apply: (change: ProposedCollectionChange) => post<LiveSchemaResult>("/apply", change)
242
+ };
243
+ }
244
+
245
+ /** The client's shape, for the hook and the tests that fake it. */
246
+ export type LiveSchemaClient = ReturnType<typeof createLiveSchemaClient>;
247
+
248
+ /**
249
+ * Whether an error means live editing is unavailable here, rather than that the
250
+ * change was wrong.
251
+ *
252
+ * Returns the reason so a caller can say which, and `undefined` for anything
253
+ * else — a refused change is not an unavailable feature, and conflating them
254
+ * would hide the refusal that the user needs to read.
255
+ */
256
+ export function asUnavailable(err: unknown): LiveSchemaUnavailable | undefined {
257
+ if (!(err instanceof LiveSchemaError) || !err.code) return undefined;
258
+ const reason = UNAVAILABLE[err.code];
259
+ return reason ? { reason, message: err.message } : undefined;
260
+ }
261
+
262
+ /** The person closed the dialog without applying. Not an error; an answer. */
263
+ export class SchemaChangeCancelled extends Error {
264
+ constructor() {
265
+ super("The schema change was not applied.");
266
+ this.name = "SchemaChangeCancelled";
267
+ }
268
+ }
269
+
270
+ /**
271
+ * Was this rejection the person saying no?
272
+ *
273
+ * A save has to *reject* when it is cancelled — resolving would leave the form
274
+ * believing it saved. But a caller that treats every rejection as a failure
275
+ * then reports the user's own choice back to them as an error, which is what
276
+ * the collection editor did: a console error and a red snackbar reading "Error
277
+ * persisting collection: The schema change was not applied."
278
+ *
279
+ * Matched by name rather than `instanceof`, which is unreliable the moment two
280
+ * copies of this module end up in one bundle — the class is then a different
281
+ * identity and every check silently answers false.
282
+ */
283
+ export function isSchemaChangeCancelled(err: unknown): boolean {
284
+ return err instanceof Error && err.name === "SchemaChangeCancelled";
285
+ }
@@ -425,7 +425,6 @@ export interface SerializableCollectionConfig {
425
425
  propertiesOrder?: string[];
426
426
  previewProperties?: string[];
427
427
  listProperties?: string[];
428
- titleProperty?: string;
429
428
 
430
429
  // ── Display config ────────────────────────────────────────────────
431
430
  openEntityMode?: "side_panel" | "full_screen" | "split" | "dialog";
@@ -483,7 +483,6 @@ export function toSerializableCollectionConfig(collection: AdminCollection): Ser
483
483
  if (collection.engine) result.engine = collection.engine;
484
484
  if (collection.dataSource) result.dataSource = collection.dataSource;
485
485
  if (collection.databaseId) result.databaseId = collection.databaseId;
486
- if (collection.titleProperty) result.titleProperty = collection.titleProperty as string;
487
486
  if (collection.ownerId) result.ownerId = collection.ownerId;
488
487
  if (collection.metadata) result.metadata = collection.metadata;
489
488
  if (isRelationalCollectionConfig(collection) && collection.table) result.table = collection.table;
@@ -1,3 +1,4 @@
1
+ import type React from "react";
1
2
  import { Property } from "@rebasepro/types";
2
3
  import { NavigationGroupMapping, AdminCollection } from "@rebasepro/admin-types";
3
4
 
@@ -56,6 +57,16 @@ export interface CollectionsConfigController {
56
57
  navigationEntries: NavigationGroupMapping[];
57
58
  saveNavigationEntries: (entries: NavigationGroupMapping[]) => Promise<void>;
58
59
 
60
+ /**
61
+ * UI this controller needs on screen in order to do its job.
62
+ *
63
+ * A controller whose writes require a confirmation has to be able to ask,
64
+ * and a hook cannot render. `ConfigControllerProvider` renders this beside
65
+ * its children, so the confirmation belongs to whoever supplied the
66
+ * controller rather than to every consumer of it. Optional: most
67
+ * controllers write without asking anybody.
68
+ */
69
+ dialog?: React.ReactNode;
59
70
  }
60
71
 
61
72
  export type UpdateCollectionParams<M extends Record<string, unknown> = Record<string, unknown>> = {
@@ -37,6 +37,7 @@ import { EngineProperties, Entity, getDataSourceCapabilities, MapProperty, Prope
37
37
  import { PropertyConfig, AdminCollection } from "@rebasepro/admin-types";
38
38
  import { getSubcollections, getTableName, isPropertyBuilder } from "@rebasepro/common";
39
39
  import { removeInitialAndTrailingSlashes } from "@rebasepro/app";
40
+ import { isSchemaChangeCancelled } from "../../liveSchemaClient";
40
41
  import { CollectionEditorSchema } from "./CollectionYupValidation";
41
42
  import { GeneralSettingsForm } from "./GeneralSettingsForm";
42
43
  import { DisplaySettingsForm } from "./DisplaySettingsForm";
@@ -431,6 +432,15 @@ function CollectionEditorInternal<M extends Record<string, unknown>>({
431
432
  return true;
432
433
  })
433
434
  .catch((e) => {
435
+ // Cancelling the schema-change review rejects, so that the form
436
+ // does not go on believing it saved. It is still the person's
437
+ // own choice, and reporting it back to them as "Error
438
+ // persisting collection" — in the console and in a red
439
+ // snackbar — describes their decision as a fault.
440
+ if (isSchemaChangeCancelled(e)) {
441
+ setError(undefined);
442
+ return false;
443
+ }
434
444
  setError(e);
435
445
  console.error(e);
436
446
  snackbarController.open({