@rebasepro/admin 0.16.0 → 0.16.1-canary.g2d1aec8

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 (167) hide show
  1. package/dist/{CollectionEditorDialog-D2Vf4C3-.js → CollectionEditorDialog-Cec5jU6R.js} +7 -3
  2. package/dist/{CollectionEditorDialog-D2Vf4C3-.js.map → CollectionEditorDialog-Cec5jU6R.js.map} +1 -1
  3. package/dist/{PropertyEditView-D3NBsK9_.js → PropertyEditView-BuSPZwnQ.js} +2 -2
  4. package/dist/{PropertyEditView-D3NBsK9_.js.map → PropertyEditView-BuSPZwnQ.js.map} +1 -1
  5. package/dist/{RouterCollectionsStudioView-hPB54l3Q.js → RouterCollectionsStudioView-BiNFpEii.js} +4 -4
  6. package/dist/{RouterCollectionsStudioView-hPB54l3Q.js.map → RouterCollectionsStudioView-BiNFpEii.js.map} +1 -1
  7. package/dist/SchemaChangeDialog-WKIbzS6P.js +327 -0
  8. package/dist/SchemaChangeDialog-WKIbzS6P.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 +170 -0
  15. package/dist/collection_editor/serializable_utils.d.ts +1 -1
  16. package/dist/collection_editor/types/collection_editor_controller.d.ts +1 -1
  17. package/dist/collection_editor/types/config_controller.d.ts +11 -0
  18. package/dist/collection_editor/ui/collection_editor/AICollectionGeneratorPopover.d.ts +1 -1
  19. package/dist/collection_editor/ui/collection_editor/AIModifiedPathsContext.d.ts +1 -1
  20. package/dist/collection_editor/ui/collection_editor/CollectionEditorDialog.d.ts +4 -4
  21. package/dist/collection_editor/ui/collection_editor/CollectionEditorWelcomeView.d.ts +1 -1
  22. package/dist/collection_editor/ui/collection_editor/CollectionPropertiesEditorForm.d.ts +2 -2
  23. package/dist/collection_editor/ui/collection_editor/CollectionStudioView.d.ts +1 -1
  24. package/dist/collection_editor/ui/collection_editor/CollectionsStudioView.d.ts +2 -2
  25. package/dist/collection_editor/ui/collection_editor/GeneralSettingsForm.d.ts +1 -1
  26. package/dist/collection_editor/ui/collection_editor/PropertyEditView.d.ts +2 -2
  27. package/dist/collection_editor/ui/collection_editor/RouterCollectionStudioView.d.ts +1 -1
  28. package/dist/collection_editor/ui/collection_editor/RouterCollectionsStudioView.d.ts +1 -1
  29. package/dist/collection_editor/ui/collection_editor/SchemaChangeDialog.d.ts +66 -0
  30. package/dist/collection_editor/ui/collection_editor/SubcollectionsEditTab.d.ts +2 -2
  31. package/dist/collection_editor/ui/collection_editor/import/CollectionEditorImportDataPreview.d.ts +1 -1
  32. package/dist/collection_editor/ui/collection_editor/import/CollectionEditorImportMapping.d.ts +1 -1
  33. package/dist/collection_editor/ui/collection_editor/import/clean_import_data.d.ts +1 -1
  34. package/dist/collection_editor/ui/collection_editor/properties/conditions/index.d.ts +6 -6
  35. package/dist/collection_editor/useCollectionEditorController.d.ts +1 -1
  36. package/dist/collection_editor/useCollectionsConfigController.d.ts +1 -1
  37. package/dist/collection_editor/useJsonCollectionsConfigController.d.ts +2 -2
  38. package/dist/collection_editor/useLiveSchemaEditing.d.ts +71 -0
  39. package/dist/collection_editor/useLocalCollectionsConfigController.d.ts +1 -1
  40. package/dist/collection_editor_ui.d.ts +12 -12
  41. package/dist/collection_editor_ui.js +4 -4
  42. package/dist/components/CollectionTableBinding/CollectionTableBinding.d.ts +1 -1
  43. package/dist/components/CollectionTableBinding/PropertyTableCell.d.ts +1 -1
  44. package/dist/components/CollectionTableBinding/fields/TableStorageUpload.d.ts +1 -1
  45. package/dist/components/CollectionTableBinding/index.d.ts +5 -5
  46. package/dist/components/CollectionTableBinding/internal/popup_field/PopupFormField.d.ts +1 -1
  47. package/dist/components/CollectionViewBinding/SlotValue.d.ts +2 -2
  48. package/dist/components/CollectionViewBinding/hooks/useKanbanDragAndDrop.d.ts +1 -1
  49. package/dist/components/EditFormActions.d.ts +1 -1
  50. package/dist/components/EditViewBinding.d.ts +3 -3
  51. package/dist/components/EntityInspector.d.ts +1 -1
  52. package/dist/components/EntityPreviewBinding.d.ts +1 -1
  53. package/dist/components/EntityViewBinding.d.ts +1 -1
  54. package/dist/components/HomePage/index.d.ts +4 -4
  55. package/dist/components/PropertyCollectionView.d.ts +1 -1
  56. package/dist/components/ReferenceWidget.d.ts +1 -1
  57. package/dist/components/admin/index.d.ts +1 -1
  58. package/dist/components/app/AppBar.d.ts +1 -1
  59. package/dist/components/app/index.d.ts +4 -4
  60. package/dist/components/common/index.d.ts +1 -1
  61. package/dist/components/field_configs.d.ts +1 -1
  62. package/dist/components/history/EntityHistoryEntry.d.ts +2 -2
  63. package/dist/components/history/index.d.ts +5 -5
  64. package/dist/components/index.d.ts +45 -45
  65. package/dist/data_export/export/index.d.ts +4 -4
  66. package/dist/data_export/index.d.ts +1 -1
  67. package/dist/data_import/components/DataNewPropertiesMapping.d.ts +1 -1
  68. package/dist/data_import/components/ImportSaveInProgress.d.ts +1 -1
  69. package/dist/data_import/components/index.d.ts +4 -4
  70. package/dist/data_import/hooks/index.d.ts +1 -1
  71. package/dist/data_import/hooks/useImportConfig.d.ts +1 -1
  72. package/dist/data_import/import/ImportCollectionAction.d.ts +1 -1
  73. package/dist/data_import/import/index.d.ts +1 -1
  74. package/dist/data_import/index.d.ts +5 -5
  75. package/dist/data_import/types/index.d.ts +1 -1
  76. package/dist/data_import/utils/file_to_json.d.ts +1 -1
  77. package/dist/data_import/utils/index.d.ts +5 -5
  78. package/dist/editor/components/SlashCommandMenu.d.ts +2 -2
  79. package/dist/editor/components/index.d.ts +5 -5
  80. package/dist/editor/editor.d.ts +2 -2
  81. package/dist/editor/index.d.ts +2 -2
  82. package/dist/editor/nodeViews/ImageComponent.d.ts +1 -1
  83. package/dist/editor/nodeViews/TaskItemComponent.d.ts +1 -1
  84. package/dist/editor/types.d.ts +1 -1
  85. package/dist/{export-oauWlxuu.js → export-BItpoIu0.js} +2 -2
  86. package/dist/{export-oauWlxuu.js.map → export-BItpoIu0.js.map} +1 -1
  87. package/dist/form/EntityForm.d.ts +1 -1
  88. package/dist/form/EntityFormActions.d.ts +1 -1
  89. package/dist/form/EntityFormBinding.d.ts +1 -1
  90. package/dist/form/PropertyFieldBinding.d.ts +1 -1
  91. package/dist/form/components/StorageItemPreview.d.ts +1 -1
  92. package/dist/form/components/index.d.ts +6 -6
  93. package/dist/form/field_bindings/ArrayCustomShapedFieldBinding.d.ts +1 -1
  94. package/dist/form/field_bindings/ArrayOfReferencesFieldBinding.d.ts +1 -1
  95. package/dist/form/field_bindings/BinaryFieldBinding.d.ts +1 -1
  96. package/dist/form/field_bindings/BlockFieldBinding.d.ts +1 -1
  97. package/dist/form/field_bindings/DateTimeFieldBinding.d.ts +1 -1
  98. package/dist/form/field_bindings/GeopointFieldBinding.d.ts +1 -1
  99. package/dist/form/field_bindings/KeyValueFieldBinding.d.ts +1 -1
  100. package/dist/form/field_bindings/MapFieldBinding.d.ts +1 -1
  101. package/dist/form/field_bindings/MarkdownEditorFieldBinding.d.ts +2 -2
  102. package/dist/form/field_bindings/MultiSelectFieldBinding.d.ts +1 -1
  103. package/dist/form/field_bindings/MultipleRelationFieldBinding.d.ts +1 -1
  104. package/dist/form/field_bindings/ReadOnlyFieldBinding.d.ts +1 -1
  105. package/dist/form/field_bindings/ReferenceFieldBinding.d.ts +1 -1
  106. package/dist/form/field_bindings/RelationFieldBinding.d.ts +1 -1
  107. package/dist/form/field_bindings/RepeatFieldBinding.d.ts +1 -1
  108. package/dist/form/field_bindings/SelectFieldBinding.d.ts +1 -1
  109. package/dist/form/field_bindings/StorageUploadFieldBinding.d.ts +1 -1
  110. package/dist/form/field_bindings/SwitchFieldBinding.d.ts +1 -1
  111. package/dist/form/field_bindings/TextFieldBinding.d.ts +1 -1
  112. package/dist/form/field_bindings/UserSelectFieldBinding.d.ts +1 -1
  113. package/dist/form/field_bindings/VectorFieldBinding.d.ts +1 -1
  114. package/dist/form/index.d.ts +24 -24
  115. package/dist/{history-C2cqc0bw.js → history-C7WF14VR.js} +2 -2
  116. package/dist/{history-C2cqc0bw.js.map → history-C7WF14VR.js.map} +1 -1
  117. package/dist/hooks/index.d.ts +16 -16
  118. package/dist/hooks/navigation/contexts/index.d.ts +3 -3
  119. package/dist/hooks/useBuildSideDialogsController.d.ts +1 -1
  120. package/dist/hooks/useSelectionDialog.d.ts +1 -1
  121. package/dist/{import-Bxv5JPm-.js → import-CZgNAlcd.js} +2 -2
  122. package/dist/{import-Bxv5JPm-.js.map → import-CZgNAlcd.js.map} +1 -1
  123. package/dist/index.d.ts +17 -15
  124. package/dist/index.js +238 -21
  125. package/dist/index.js.map +1 -1
  126. package/dist/preview/PropertyPreview.d.ts +1 -1
  127. package/dist/preview/components/ArrayEnumPreview.d.ts +1 -1
  128. package/dist/preview/components/BooleanPreview.d.ts +1 -1
  129. package/dist/preview/components/ImagePreview.d.ts +1 -1
  130. package/dist/preview/components/ReferencePreview.d.ts +1 -1
  131. package/dist/preview/components/RelationPreview.d.ts +1 -1
  132. package/dist/preview/components/StorageThumbnail.d.ts +1 -1
  133. package/dist/preview/components/UrlComponentPreview.d.ts +2 -2
  134. package/dist/preview/components/UserPreview.d.ts +1 -1
  135. package/dist/preview/index.d.ts +24 -24
  136. package/dist/preview/property_previews/ArrayOfMapsPreview.d.ts +1 -1
  137. package/dist/preview/property_previews/ArrayOfReferencesPreview.d.ts +1 -1
  138. package/dist/preview/property_previews/ArrayOfRelationsPreview.d.ts +1 -1
  139. package/dist/preview/property_previews/ArrayOfStorageComponentsPreview.d.ts +1 -1
  140. package/dist/preview/property_previews/ArrayOfStringsPreview.d.ts +1 -1
  141. package/dist/preview/property_previews/ArrayOneOfPreview.d.ts +1 -1
  142. package/dist/preview/property_previews/ArrayPropertyEnumPreview.d.ts +1 -1
  143. package/dist/preview/property_previews/ArrayPropertyPreview.d.ts +1 -1
  144. package/dist/preview/property_previews/MapPropertyPreview.d.ts +1 -1
  145. package/dist/preview/property_previews/NumberPropertyPreview.d.ts +1 -1
  146. package/dist/preview/property_previews/SkeletonPropertyComponent.d.ts +1 -1
  147. package/dist/preview/property_previews/StringPropertyPreview.d.ts +1 -1
  148. package/dist/preview/util.d.ts +1 -1
  149. package/dist/routes/index.d.ts +2 -2
  150. package/dist/types/components/EntityFormActionsProps.d.ts +2 -2
  151. package/dist/types/components/EntityFormProps.d.ts +3 -3
  152. package/dist/types/components/index.d.ts +3 -3
  153. package/dist/types/fields.d.ts +1 -1
  154. package/dist/types/index.d.ts +2 -2
  155. package/dist/util/index.d.ts +5 -5
  156. package/dist/{util-C4qsI1QV.js → util-wrBH4QSB.js} +130 -7
  157. package/dist/util-wrBH4QSB.js.map +1 -0
  158. package/package.json +10 -10
  159. package/src/collection_editor/ConfigControllerProvider.tsx +5 -0
  160. package/src/collection_editor/liveSchemaClient.ts +272 -0
  161. package/src/collection_editor/types/config_controller.tsx +11 -0
  162. package/src/collection_editor/ui/collection_editor/CollectionEditorDialog.tsx +10 -0
  163. package/src/collection_editor/ui/collection_editor/SchemaChangeDialog.tsx +368 -0
  164. package/src/collection_editor/useLiveSchemaEditing.tsx +285 -0
  165. package/src/collection_editor/useLocalCollectionsConfigController.tsx +109 -10
  166. package/src/index.ts +6 -0
  167. 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.g2d1aec8",
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/admin-types": "0.16.1-canary.g2d1aec8",
87
+ "@rebasepro/app": "0.16.1-canary.g2d1aec8",
88
+ "@rebasepro/common": "0.16.1-canary.g2d1aec8",
89
+ "@rebasepro/forms": "0.16.1-canary.g2d1aec8",
90
+ "@rebasepro/inference": "0.16.1-canary.g2d1aec8",
91
+ "@rebasepro/types": "0.16.1-canary.g2d1aec8",
92
+ "@rebasepro/ui": "0.16.1-canary.g2d1aec8",
93
+ "@rebasepro/utils": "0.16.1-canary.g2d1aec8"
94
94
  },
95
95
  "peerDependencies": {
96
96
  "react": ">=19.2.7",
@@ -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 ../../scripts/add-dts-extensions.mjs dist && node ../../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,272 @@
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
+
56
+ export interface LiveSchemaResult {
57
+ applied: boolean;
58
+ applyError?: string;
59
+ committed: { sha: string; branch: string; files: string[] };
60
+ statements: string[];
61
+ summary: string;
62
+ withheldConstraints: WithheldConstraint[];
63
+ }
64
+
65
+ /**
66
+ * Whether this backend can edit its schema, and what is missing when it cannot.
67
+ *
68
+ * `canPlan` is separate from `enabled` deliberately: previewing a change needs
69
+ * no repository, so a deployment running from a bundle can still show somebody
70
+ * exactly what their change would do. Greying out the preview as well would be
71
+ * a worse answer than the truth.
72
+ */
73
+ export interface LiveSchemaStatus {
74
+ enabled: boolean;
75
+ canPlan: boolean;
76
+ /**
77
+ * Whether *this caller* may apply, as opposed to whether the server can.
78
+ *
79
+ * Applying commits to the repository under an author, and a credential is
80
+ * not an author — so an API key or the service key may preview a change and
81
+ * not make one. Reported here so the panel can grey out the button and say
82
+ * why, rather than letting somebody read a plan, decide, press, and only
83
+ * then be refused.
84
+ */
85
+ canApply: boolean;
86
+ applyRefusedBecause?: string;
87
+ applyRefusedCode?: string;
88
+ repository?: string;
89
+ code?: string;
90
+ reason?: string;
91
+ }
92
+
93
+ /**
94
+ * Why live editing is not available here.
95
+ *
96
+ * Distinguished from an ordinary failure because both are expected states with
97
+ * their own explanation, and a UI should say which rather than showing a
98
+ * stack: a Mongo backend will never support this, and a bundle deployment has
99
+ * no source to edit.
100
+ */
101
+ export type LiveSchemaUnavailable =
102
+ | { reason: "unsupported"; message: string }
103
+ | { reason: "no-repository"; message: string };
104
+
105
+ export class LiveSchemaError extends Error {
106
+ constructor(
107
+ message: string,
108
+ readonly code: string | undefined,
109
+ readonly changes?: LiveSchemaPlan["changes"]
110
+ ) {
111
+ super(message);
112
+ this.name = "LiveSchemaError";
113
+ }
114
+ }
115
+
116
+ const UNAVAILABLE: Record<string, LiveSchemaUnavailable["reason"]> = {
117
+ SCHEMA_EDITING_UNSUPPORTED: "unsupported",
118
+ SCHEMA_EDITING_NO_REPOSITORY: "no-repository"
119
+ };
120
+
121
+ export interface LiveSchemaClientOptions {
122
+ /** Base for the routes, e.g. `https://api.example.com/api/schema`. */
123
+ baseUrl: string;
124
+ getAuthToken?: () => Promise<string | null> | string | null;
125
+ fetchImpl?: typeof fetch;
126
+ }
127
+
128
+ export interface ProposedCollectionChange {
129
+ collectionId: string;
130
+ /**
131
+ * The **whole** collection as it should end up — not a patch. See the
132
+ * module comment: a partial reads as a set of removals.
133
+ */
134
+ collection: Record<string, unknown>;
135
+ }
136
+
137
+ export function createLiveSchemaClient(options: LiveSchemaClientOptions) {
138
+ const doFetch = options.fetchImpl ?? fetch;
139
+ const base = options.baseUrl.replace(/\/$/, "");
140
+
141
+ const post = async <T>(path: string, body: ProposedCollectionChange): Promise<T> => {
142
+ const token = await options.getAuthToken?.();
143
+ const headers: Record<string, string> = { "Content-Type": "application/json" };
144
+ if (token) headers.Authorization = `Bearer ${token}`;
145
+
146
+ const response = await doFetch(`${base}${path}`, {
147
+ method: "POST",
148
+ headers,
149
+ body: JSON.stringify(body)
150
+ });
151
+
152
+ if (response.ok) return await response.json() as T;
153
+
154
+ // Parsed rather than assumed: the server answers a structured error, and
155
+ // the code is what tells an expected refusal from a broken request.
156
+ let payload: { error?: { code?: string; message?: string; details?: { changes?: LiveSchemaPlan["changes"] } } } = {};
157
+ try {
158
+ payload = await response.json() as typeof payload;
159
+ } catch {
160
+ payload = { error: { message: await response.text().catch(() => "") } };
161
+ }
162
+
163
+ throw new LiveSchemaError(
164
+ payload.error?.message || `Schema request failed with ${response.status}`,
165
+ payload.error?.code,
166
+ payload.error?.details?.changes
167
+ );
168
+ };
169
+
170
+ return {
171
+ /**
172
+ * Whether this backend can edit its schema.
173
+ *
174
+ * Never throws. Every way of failing to get an answer — a backend too
175
+ * old to have the endpoint, an unreachable one, a session that is not
176
+ * an admin — means the same thing to a caller deciding whether to offer
177
+ * the control, and turning that into an exception each caller has to
178
+ * catch would only produce four copies of this `catch`.
179
+ */
180
+ status: async (): Promise<LiveSchemaStatus> => {
181
+ try {
182
+ const token = await options.getAuthToken?.();
183
+ const response = await doFetch(`${base}/status`, {
184
+ headers: token ? { Authorization: `Bearer ${token}` } : {}
185
+ });
186
+ const body = await response.json().catch(() => ({})) as
187
+ Partial<LiveSchemaStatus> & { error?: { code?: string; message?: string } };
188
+
189
+ if (response.ok) {
190
+ return {
191
+ enabled: body.enabled === true,
192
+ canPlan: body.canPlan === true,
193
+ // Absent on a backend from before capabilities existed.
194
+ // Read as "yes" there, because on that server being
195
+ // through the admin gate *was* the whole permission —
196
+ // defaulting to no would take the feature away from
197
+ // every deployment that has not upgraded.
198
+ canApply: body.canApply ?? body.enabled === true,
199
+ applyRefusedBecause: body.applyRefusedBecause,
200
+ applyRefusedCode: body.applyRefusedCode,
201
+ repository: body.repository,
202
+ code: body.code,
203
+ reason: body.reason
204
+ };
205
+ }
206
+ return {
207
+ enabled: false,
208
+ canPlan: false,
209
+ canApply: false,
210
+ code: body.error?.code,
211
+ reason: body.error?.message
212
+ ?? `The backend refused to say whether its schema is editable (HTTP ${response.status}).`
213
+ };
214
+ } catch (err) {
215
+ return {
216
+ enabled: false,
217
+ canPlan: false,
218
+ canApply: false,
219
+ reason: err instanceof Error ? err.message : "The backend could not be reached."
220
+ };
221
+ }
222
+ },
223
+
224
+ /** What the change would do. No side effects. */
225
+ plan: (change: ProposedCollectionChange) => post<LiveSchemaPlan>("/plan", change),
226
+
227
+ /** Commit the change, then apply it. */
228
+ apply: (change: ProposedCollectionChange) => post<LiveSchemaResult>("/apply", change)
229
+ };
230
+ }
231
+
232
+ /** The client's shape, for the hook and the tests that fake it. */
233
+ export type LiveSchemaClient = ReturnType<typeof createLiveSchemaClient>;
234
+
235
+ /**
236
+ * Whether an error means live editing is unavailable here, rather than that the
237
+ * change was wrong.
238
+ *
239
+ * Returns the reason so a caller can say which, and `undefined` for anything
240
+ * else — a refused change is not an unavailable feature, and conflating them
241
+ * would hide the refusal that the user needs to read.
242
+ */
243
+ export function asUnavailable(err: unknown): LiveSchemaUnavailable | undefined {
244
+ if (!(err instanceof LiveSchemaError) || !err.code) return undefined;
245
+ const reason = UNAVAILABLE[err.code];
246
+ return reason ? { reason, message: err.message } : undefined;
247
+ }
248
+
249
+ /** The person closed the dialog without applying. Not an error; an answer. */
250
+ export class SchemaChangeCancelled extends Error {
251
+ constructor() {
252
+ super("The schema change was not applied.");
253
+ this.name = "SchemaChangeCancelled";
254
+ }
255
+ }
256
+
257
+ /**
258
+ * Was this rejection the person saying no?
259
+ *
260
+ * A save has to *reject* when it is cancelled — resolving would leave the form
261
+ * believing it saved. But a caller that treats every rejection as a failure
262
+ * then reports the user's own choice back to them as an error, which is what
263
+ * the collection editor did: a console error and a red snackbar reading "Error
264
+ * persisting collection: The schema change was not applied."
265
+ *
266
+ * Matched by name rather than `instanceof`, which is unreliable the moment two
267
+ * copies of this module end up in one bundle — the class is then a different
268
+ * identity and every check silently answers false.
269
+ */
270
+ export function isSchemaChangeCancelled(err: unknown): boolean {
271
+ return err instanceof Error && err.name === "SchemaChangeCancelled";
272
+ }
@@ -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({