@rebasepro/cms 0.22.0 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/dist/{CollectionEditorDialog-CUtfS_49.js → CollectionEditorDialog-p5-I-Apu.js} +181 -97
  2. package/dist/CollectionEditorDialog-p5-I-Apu.js.map +1 -0
  3. package/dist/{PropertyEditView-BpEbC0Q2.js → PropertyEditView-DDB_X-00.js} +55 -23
  4. package/dist/PropertyEditView-DDB_X-00.js.map +1 -0
  5. package/dist/{RouterCollectionsStudioView-EK8Knq5I.js → RouterCollectionsStudioView-SGBV4CMg.js} +34 -10
  6. package/dist/RouterCollectionsStudioView-SGBV4CMg.js.map +1 -0
  7. package/dist/{SchemaChangeDialog-BeP0A4MJ.js → SchemaChangeDialog-CcFSXvkK.js} +33 -12
  8. package/dist/SchemaChangeDialog-CcFSXvkK.js.map +1 -0
  9. package/dist/collection_editor/_cms_internals.d.ts +3 -1
  10. package/dist/collection_editor/liveSchemaClient.d.ts +38 -18
  11. package/dist/collection_editor/relation_kinds.d.ts +11 -0
  12. package/dist/collection_editor/serializable_types.d.ts +2 -0
  13. package/dist/collection_editor/types/config_controller.d.ts +11 -0
  14. package/dist/collection_editor/ui/collection_editor/CollectionStudioView.d.ts +8 -1
  15. package/dist/collection_editor/useLiveSchemaEditing.d.ts +5 -5
  16. package/dist/collection_editor/useLocalCollectionsConfigController.d.ts +7 -0
  17. package/dist/collection_editor_ui.js +5 -5
  18. package/dist/components/ClearFilterSortButton.d.ts +7 -1
  19. package/dist/components/CollectionTableBinding/fields/CompactEntityCellField.d.ts +12 -13
  20. package/dist/components/CollectionTableBinding/fields/TableMultipleRelationField.d.ts +6 -3
  21. package/dist/components/CollectionTableBinding/fields/TableReferenceField.d.ts +8 -1
  22. package/dist/components/CollectionTableBinding/fields/TableRelationField.d.ts +8 -1
  23. package/dist/components/CollectionTableBinding/fields/TableRelationSelectorField.d.ts +13 -3
  24. package/dist/components/CollectionTableBinding/fields/VirtualTableSelect.d.ts +10 -0
  25. package/dist/components/CollectionTableBinding/fields/VirtualTableUserSelect.d.ts +10 -0
  26. package/dist/components/CollectionTableBinding/internal/EntityTableCell.d.ts +21 -0
  27. package/dist/components/CollectionTableBinding/internal/TableCellOpener.d.ts +45 -0
  28. package/dist/components/CollectionTableBinding/table_bindings.d.ts +24 -0
  29. package/dist/components/CollectionViewBinding/CollectionViewActions.d.ts +6 -1
  30. package/dist/components/CollectionViewBinding/CollectionViewBinding.d.ts +12 -3
  31. package/dist/components/CollectionViewBinding/FiltersDialog.d.ts +5 -0
  32. package/dist/components/EditViewBinding.d.ts +7 -1
  33. package/dist/components/EntityIdentityBar.d.ts +29 -4
  34. package/dist/components/EntityPreviewBinding.d.ts +8 -1
  35. package/dist/components/EntityPreviewNesting.d.ts +15 -0
  36. package/dist/components/NavigationLoadingErrorView.d.ts +16 -0
  37. package/dist/components/PickerTrigger.d.ts +17 -0
  38. package/dist/components/ReferenceTable/SelectionTableBinding.d.ts +12 -0
  39. package/dist/components/RelationSelector.d.ts +24 -7
  40. package/dist/components/UserSelector.d.ts +8 -1
  41. package/dist/components/common/ActionMenuLabel.d.ts +15 -0
  42. package/dist/components/common/default_entity_actions.d.ts +37 -0
  43. package/dist/data_export/export/additional_fields.d.ts +13 -0
  44. package/dist/data_export/export/export.d.ts +6 -0
  45. package/dist/data_import/components/ImportConversionProblems.d.ts +11 -0
  46. package/dist/data_import/components/index.d.ts +1 -0
  47. package/dist/data_import/utils/csv.d.ts +3 -2
  48. package/dist/data_import/utils/data.d.ts +81 -4
  49. package/dist/data_import/utils/id_column.d.ts +16 -0
  50. package/dist/data_import/utils/import_columns.d.ts +48 -0
  51. package/dist/editor/editor.d.ts +7 -1
  52. package/dist/editor/extensions/Image.d.ts +22 -1
  53. package/dist/editor/hooks/useProseMirror.d.ts +4 -1
  54. package/dist/editor.js +69 -7
  55. package/dist/editor.js.map +1 -1
  56. package/dist/{export-LvIpE7sE.js → export-DCHuPD1a.js} +65 -211
  57. package/dist/export-DCHuPD1a.js.map +1 -0
  58. package/dist/form/form_utils.d.ts +37 -0
  59. package/dist/form/resolve_form_properties.d.ts +26 -0
  60. package/dist/form/validation.d.ts +13 -0
  61. package/dist/{history-DG6Ln7CV.js → history-CBd6SBRs.js} +3 -2
  62. package/dist/{history-DG6Ln7CV.js.map → history-CBd6SBRs.js.map} +1 -1
  63. package/dist/hooks/useEntityDisplay.d.ts +4 -5
  64. package/dist/hooks/useRecordActions.d.ts +28 -6
  65. package/dist/hooks/useReferencePaths.d.ts +21 -0
  66. package/dist/hooks/useStorageReferenceResolver.d.ts +23 -0
  67. package/dist/{import-Cxnmpyjl.js → import-DjQoRrIU.js} +35 -54
  68. package/dist/import-DjQoRrIU.js.map +1 -0
  69. package/dist/index.js +191 -117
  70. package/dist/index.js.map +1 -1
  71. package/dist/preview/components/StorageMarkdown.d.ts +10 -0
  72. package/dist/preview/util.d.ts +29 -0
  73. package/dist/util/caller_scopes.d.ts +15 -0
  74. package/dist/util/entity_actions.d.ts +37 -1
  75. package/dist/util/fit_inline_actions.d.ts +20 -0
  76. package/dist/util/navigation_utils.d.ts +14 -2
  77. package/dist/util/view_mode.d.ts +12 -18
  78. package/dist/{util-CyIcgnv2.js → util-BRvKFMLa.js} +5775 -3665
  79. package/dist/util-BRvKFMLa.js.map +1 -0
  80. package/dist/{util-DB31IoJE.js → util-D7ebttKT.js} +39 -2
  81. package/dist/util-D7ebttKT.js.map +1 -0
  82. package/package.json +9 -9
  83. package/dist/CollectionEditorDialog-CUtfS_49.js.map +0 -1
  84. package/dist/PropertyEditView-BpEbC0Q2.js.map +0 -1
  85. package/dist/RouterCollectionsStudioView-EK8Knq5I.js.map +0 -1
  86. package/dist/SchemaChangeDialog-BeP0A4MJ.js.map +0 -1
  87. package/dist/export-LvIpE7sE.js.map +0 -1
  88. package/dist/import-Cxnmpyjl.js.map +0 -1
  89. package/dist/util-CyIcgnv2.js.map +0 -1
  90. package/dist/util-DB31IoJE.js.map +0 -1
@@ -1,7 +1,7 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
- import { Sn as useUrlController } from "./util-CyIcgnv2.js";
3
- import { t as CollectionEditor } from "./CollectionEditorDialog-CUtfS_49.js";
4
- import { s as AIModifiedPathsProvider } from "./PropertyEditView-BpEbC0Q2.js";
2
+ import { Bn as useUrlController } from "./util-BRvKFMLa.js";
3
+ import { t as CollectionEditor } from "./CollectionEditorDialog-p5-I-Apu.js";
4
+ import { s as AIModifiedPathsProvider } from "./PropertyEditView-DDB_X-00.js";
5
5
  import { useEffect, useState } from "react";
6
6
  import { Button, IconButton, PlusIcon, ResizablePanels, Tooltip, Typography, cls, defaultBorderMixin, iconSize } from "@rebasepro/ui";
7
7
  import { IconForView, UnsavedChangesDialog, useUnsavedChangesDialog } from "@rebasepro/app";
@@ -9,8 +9,11 @@ import { jsx, jsxs } from "react/jsx-runtime";
9
9
  import { readStoredString, writeStoredString } from "@rebasepro/utils";
10
10
  import { useLocation, useNavigate } from "react-router";
11
11
  //#region src/collection_editor/ui/collection_editor/CollectionStudioView.tsx
12
- function CollectionStudioView({ collectionId, onSave, onCancel, ...props }) {
12
+ function CollectionStudioView({ collectionId, onSave, onCancel, onDirtyChange, ...props }) {
13
13
  const [formDirty, setFormDirty] = useState(false);
14
+ useEffect(() => {
15
+ onDirtyChange?.(formDirty);
16
+ }, [formDirty, onDirtyChange]);
14
17
  const [cancelRequested, setCancelRequested] = useState(false);
15
18
  const { dialogProps, triggerDialog } = useUnsavedChangesDialog(formDirty, () => setFormDirty(false));
16
19
  const activeCollectionId = collectionId;
@@ -73,6 +76,15 @@ function CollectionsStudioView({ configController, collections: collectionsProp,
73
76
  if (isControlled) onActiveCollectionChange?.(id);
74
77
  else setInternalActiveId(id);
75
78
  };
79
+ const [editorDirty, setEditorDirty] = useState(false);
80
+ const [pendingSelection, setPendingSelection] = useState(null);
81
+ const selectCollection = (id) => {
82
+ if (!isControlled && editorDirty && id !== activeCollectionId) {
83
+ setPendingSelection({ id });
84
+ return;
85
+ }
86
+ setActiveCollectionId(id);
87
+ };
76
88
  const collections = collectionsProp ?? configController.collections ?? [];
77
89
  const [sidebarSize, setSidebarSize] = useState(() => {
78
90
  const saved = readStoredString("rebase_collections_editor_sidebar_size");
@@ -82,9 +94,9 @@ function CollectionsStudioView({ configController, collections: collectionsProp,
82
94
  useEffect(() => {
83
95
  writeStoredString("rebase_collections_editor_sidebar_size", sidebarSize.toString());
84
96
  }, [sidebarSize]);
85
- return /* @__PURE__ */ jsx("div", {
97
+ return /* @__PURE__ */ jsxs("div", {
86
98
  className: "flex h-full w-full bg-surface-sheet overflow-hidden text-text-primary dark:text-text-primary-dark",
87
- children: /* @__PURE__ */ jsx(ResizablePanels, {
99
+ children: [/* @__PURE__ */ jsx(ResizablePanels, {
88
100
  orientation: "horizontal",
89
101
  panelSizePercent: sidebarSize,
90
102
  onPanelSizeChange: setSidebarSize,
@@ -101,8 +113,9 @@ function CollectionsStudioView({ configController, collections: collectionsProp,
101
113
  title: configController.readOnly ? configController.readOnlyReason || "Read only" : "Add collection",
102
114
  children: /* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsx(IconButton, {
103
115
  size: "small",
116
+ "aria-label": "Add collection",
104
117
  disabled: configController.readOnly,
105
- onClick: () => setActiveCollectionId("new"),
118
+ onClick: () => selectCollection("new"),
106
119
  className: activeCollectionId === "new" ? "text-primary dark:text-primary-dark" : "text-text-secondary dark:text-text-secondary-dark",
107
120
  children: /* @__PURE__ */ jsx(PlusIcon, { size: iconSize.smallest })
108
121
  }) })
@@ -120,7 +133,7 @@ function CollectionsStudioView({ configController, collections: collectionsProp,
120
133
  const collectionKey = collection.slug;
121
134
  const isSelected = activeCollectionId === collectionKey;
122
135
  return /* @__PURE__ */ jsxs("div", {
123
- onClick: () => setActiveCollectionId(collectionKey),
136
+ onClick: () => selectCollection(collectionKey),
124
137
  className: cls("flex items-center gap-2.5 px-3 h-[30px] cursor-pointer rounded-lg text-[13px] font-medium transition-colors", isSelected ? "bg-primary/8 text-primary dark:bg-primary/10 dark:text-primary-light font-semibold" : "hover:bg-surface-hover text-surface-700 dark:text-surface-300 hover:text-surface-900 dark:hover:text-white"),
125
138
  children: [/* @__PURE__ */ jsx(IconForView, {
126
139
  collectionOrView: collection,
@@ -142,6 +155,7 @@ function CollectionsStudioView({ configController, collections: collectionsProp,
142
155
  onSave: (savedCollection) => {
143
156
  if (activeCollectionId === "new" && savedCollection?.slug) setActiveCollectionId(savedCollection.slug);
144
157
  },
158
+ onDirtyChange: setEditorDirty,
145
159
  onCancel: () => setActiveCollectionId(void 0),
146
160
  propertyTypePresets,
147
161
  hiddenPropertyTypes,
@@ -161,7 +175,17 @@ function CollectionsStudioView({ configController, collections: collectionsProp,
161
175
  })]
162
176
  })
163
177
  })
164
- })
178
+ }), /* @__PURE__ */ jsx(UnsavedChangesDialog, {
179
+ open: pendingSelection !== null,
180
+ body: "There are unsaved changes in this collection",
181
+ handleOk: () => {
182
+ const next = pendingSelection;
183
+ setPendingSelection(null);
184
+ setEditorDirty(false);
185
+ if (next) setActiveCollectionId(next.id);
186
+ },
187
+ handleCancel: () => setPendingSelection(null)
188
+ })]
165
189
  });
166
190
  }
167
191
  //#endregion
@@ -185,4 +209,4 @@ function RouterCollectionsStudioView(props) {
185
209
  //#endregion
186
210
  export { CollectionStudioView as i, RouterCollectionsStudioView_exports as n, CollectionsStudioView as r, RouterCollectionsStudioView as t };
187
211
 
188
- //# sourceMappingURL=RouterCollectionsStudioView-EK8Knq5I.js.map
212
+ //# sourceMappingURL=RouterCollectionsStudioView-SGBV4CMg.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RouterCollectionsStudioView-SGBV4CMg.js","names":[],"sources":["../src/collection_editor/ui/collection_editor/CollectionStudioView.tsx","../src/collection_editor/ui/collection_editor/CollectionsStudioView.tsx","../src/collection_editor/ui/collection_editor/RouterCollectionsStudioView.tsx"],"sourcesContent":["\nimport { useUnsavedChangesDialog, UnsavedChangesDialog } from \"@rebasepro/app\";\nimport * as React from \"react\";\nimport { useEffect, useState } from \"react\";\nimport { Button, PlusIcon, Typography } from \"@rebasepro/ui\";\nimport { CollectionEditorDialogProps } from \"./CollectionEditorDialog\";\nimport { AIModifiedPathsProvider } from \"./AIModifiedPathsContext\";\nimport { CollectionEditor } from \"./CollectionEditorDialog\";\nimport type { AdminCollection } from \"@rebasepro/cms-types\";\n\nexport type CollectionStudioViewProps = Omit<CollectionEditorDialogProps, \"open\" | \"isNewCollection\" | \"editedCollectionId\" | \"handleClose\" | \"handleCancel\"> & {\n collectionId?: string | \"new\";\n\n /**\n * Called after a successful save.\n * Receives the saved collection (or undefined if dismissed without saving).\n * When not provided, save completes silently.\n */\n onSave?: (collection?: AdminCollection) => void;\n\n /**\n * Called when the collection on screen gains or loses unsaved edits.\n *\n * For whatever decides which collection is on screen: this view is\n * remounted when that changes, and its edits go with it.\n */\n onDirtyChange?: (dirty: boolean) => void;\n\n /**\n * Called when the user cancels editing.\n * When not provided, cancel is a no-op.\n */\n onCancel?: () => void;\n};\n\nexport function CollectionStudioView({ collectionId, onSave, onCancel, onDirtyChange, ...props }: CollectionStudioViewProps) {\n\n // Form state from the editor\n const [formDirty, setFormDirty] = useState<boolean>(false);\n\n useEffect(() => {\n onDirtyChange?.(formDirty);\n }, [formDirty, onDirtyChange]);\n const [cancelRequested, setCancelRequested] = useState<boolean>(false);\n\n const { dialogProps, triggerDialog } = useUnsavedChangesDialog(\n formDirty,\n () => setFormDirty(false)\n );\n\n const activeCollectionId = collectionId;\n\n const handleCancelClick = () => {\n if (!formDirty) {\n onCancel?.();\n } else {\n setCancelRequested(true);\n triggerDialog();\n }\n };\n\n return (\n <div className=\"flex-grow flex flex-col h-full w-full bg-surface-sheet\">\n <AIModifiedPathsProvider>\n {activeCollectionId ? (\n <CollectionEditor\n key={activeCollectionId}\n {...props}\n fullScreen={true}\n open={true}\n isNewCollection={activeCollectionId === \"new\"}\n editedCollectionId={activeCollectionId !== \"new\" ? activeCollectionId : undefined}\n handleCancel={handleCancelClick}\n handleClose={(savedCollection) => {\n setFormDirty(false);\n if (savedCollection) {\n setTimeout(() => {\n onSave?.(savedCollection);\n }, 0);\n }\n }}\n setFormDirty={setFormDirty}\n />\n ) : (\n <div className=\"flex-grow flex flex-col items-center justify-center h-full gap-4\">\n <Typography variant=\"label\">\n Select a collection or create a new one\n </Typography>\n <Button\n disabled={props.configController?.readOnly}\n onClick={() => onSave?.()}\n >\n <PlusIcon/>\n Add new collection\n </Button>\n </div>\n )}\n\n <UnsavedChangesDialog\n {...dialogProps}\n handleOk={() => {\n dialogProps.handleOk();\n if (cancelRequested) {\n onCancel?.();\n setCancelRequested(false);\n }\n }}\n handleCancel={() => {\n dialogProps.handleCancel();\n setCancelRequested(false);\n }}\n />\n </AIModifiedPathsProvider>\n </div>\n );\n}\n","\nimport { IconForView, UnsavedChangesDialog } from \"@rebasepro/app\";\nimport React, { useState, useEffect } from \"react\";\nimport {\n Button,\n cls,\n defaultBorderMixin,\n IconButton,\n iconSize,\n PlusIcon,\n ResizablePanels,\n Tooltip,\n Typography\n} from \"@rebasepro/ui\";\n\nimport { CollectionsConfigController } from \"../../types/config_controller\";\nimport { CollectionStudioView } from \"./CollectionStudioView\";\nimport type { CollectionEditorExtensionProps } from \"../../extensibility_types\";\nimport type { AdminCollection } from \"@rebasepro/cms-types\";\nimport { readStoredString, writeStoredString } from \"@rebasepro/utils\";\n\nexport interface CollectionsStudioViewProps extends CollectionEditorExtensionProps {\n configController: CollectionsConfigController;\n\n /**\n * Collections to show in the sidebar.\n * When provided, overrides the collections from `configController`.\n * Use this to control exactly which collections the editor displays.\n */\n collections?: AdminCollection[];\n\n /**\n * Controlled active collection ID.\n * When provided together with `onActiveCollectionChange`,\n * navigation is fully external — no internal state is used.\n */\n activeCollectionId?: string;\n\n /**\n * Called when the user clicks a collection, \"new\", or navigates.\n * When provided, the component is fully controlled (no internal state).\n * When not provided, the component manages selection via internal state.\n *\n * Pass `\"new\"` to create a new collection. Pass `undefined` to deselect.\n */\n onActiveCollectionChange?: (collectionId: string | undefined) => void;\n}\n\nexport function CollectionsStudioView({\n configController,\n collections: collectionsProp,\n activeCollectionId: controlledActiveId,\n onActiveCollectionChange,\n propertyTypePresets,\n hiddenPropertyTypes,\n renderExtraPropertyFields,\n renderExtraCollectionFields,\n visibleTabs,\n standalone,\n}: CollectionsStudioViewProps) {\n\n // ── Navigation state ────────────────────────────────────────────────\n // If onActiveCollectionChange is provided, the component is controlled.\n // Otherwise, use internal state.\n const [internalActiveId, setInternalActiveId] = useState<string | undefined>(undefined);\n const isControlled = onActiveCollectionChange !== undefined;\n const activeCollectionId = isControlled ? controlledActiveId : internalActiveId;\n\n const setActiveCollectionId = (id: string | undefined) => {\n if (isControlled) {\n onActiveCollectionChange?.(id);\n } else {\n setInternalActiveId(id);\n }\n };\n\n // ── Unsaved edits ───────────────────────────────────────────────────\n // The editor is keyed by the active id, so selecting another collection\n // remounts it and drops whatever was typed into the one on screen.\n // Controlled, the selection is the caller's navigation and the caller\n // guards it — `RouterCollectionsStudioView` is a route change, which the\n // editor's own navigation blocker asks about. Uncontrolled, it is state\n // this component owns, and nothing else would ask.\n const [editorDirty, setEditorDirty] = useState(false);\n const [pendingSelection, setPendingSelection] = useState<{ id: string | undefined } | null>(null);\n\n const selectCollection = (id: string | undefined) => {\n if (!isControlled && editorDirty && id !== activeCollectionId) {\n setPendingSelection({ id });\n return;\n }\n setActiveCollectionId(id);\n };\n\n // ── Collections list ────────────────────────────────────────────────\n const collections = collectionsProp ?? configController.collections ?? [];\n\n // ── Sidebar sizing ──────────────────────────────────────────────────\n // Checked, not just parsed: `parseFloat` answers NaN for anything it cannot\n // read, and a NaN pane size lays the sidebar out to nothing — with the bad\n // value still in storage on reload. `SplitListView.getSavedPanelSize` is the\n // same read, done this way.\n const [sidebarSize, setSidebarSize] = useState(() => {\n const saved = readStoredString(\"rebase_collections_editor_sidebar_size\");\n const parsed = saved === null ? NaN : parseFloat(saved);\n return Number.isFinite(parsed) && parsed > 0 && parsed < 100 ? parsed : 25;\n });\n\n useEffect(() => {\n writeStoredString(\"rebase_collections_editor_sidebar_size\", sidebarSize.toString());\n }, [sidebarSize]);\n\n return (\n <div className=\"flex h-full w-full bg-surface-sheet overflow-hidden text-text-primary dark:text-text-primary-dark\">\n <ResizablePanels\n orientation=\"horizontal\"\n panelSizePercent={sidebarSize}\n onPanelSizeChange={setSidebarSize}\n minPanelSizePx={220}\n firstPanel={\n <div className={cls(\"flex flex-col h-full w-full bg-surface-sheet border-r\", defaultBorderMixin)}>\n <div className={cls(\"flex items-center justify-between px-3 py-2 border-b min-h-[48px]\", defaultBorderMixin)}>\n <Typography variant=\"caption\" className=\"font-semibold text-[11px] uppercase tracking-wider text-surface-400 dark:text-surface-400\">\n Collections\n </Typography>\n <Tooltip title={configController.readOnly ? configController.readOnlyReason || \"Read only\" : \"Add collection\"}>\n <div>\n <IconButton\n size=\"small\"\n aria-label={\"Add collection\"}\n disabled={configController.readOnly}\n onClick={() => selectCollection(\"new\")}\n className={activeCollectionId === \"new\" ? \"text-primary dark:text-primary-dark\" : \"text-text-secondary dark:text-text-secondary-dark\"}\n >\n <PlusIcon size={iconSize.smallest}/>\n </IconButton>\n </div>\n </Tooltip>\n </div>\n\n <div className=\"flex-grow overflow-y-auto w-full no-scrollbar p-2 space-y-0.5\">\n {collections.length === 0 && (\n <div className=\"p-4 text-center\">\n <Typography variant=\"caption\" className=\"text-text-disabled dark:text-text-disabled-dark italic\">\n No collections yet.\n </Typography>\n </div>\n )}\n {collections.map((collection) => {\n const collectionKey = collection.slug;\n const isSelected = activeCollectionId === collectionKey;\n return (\n <div\n key={collectionKey}\n onClick={() => selectCollection(collectionKey)}\n className={cls(\n \"flex items-center gap-2.5 px-3 h-[30px] cursor-pointer rounded-lg text-[13px] font-medium transition-colors\",\n isSelected\n ? \"bg-primary/8 text-primary dark:bg-primary/10 dark:text-primary-light font-semibold\"\n : \"hover:bg-surface-hover text-surface-700 dark:text-surface-300 hover:text-surface-900 dark:hover:text-white\"\n )}\n >\n <IconForView collectionOrView={collection} size={\"smallest\"} className={cls(\n isSelected\n ? \"text-primary dark:text-primary-light\"\n : \"text-surface-500 dark:text-text-secondary-dark\"\n )}/>\n <span className=\"truncate flex-1\">\n {collection.name || collection.slug}\n </span>\n </div>\n );\n })}\n </div>\n </div>\n }\n secondPanel={\n <div className=\"flex-grow flex flex-col min-w-0 h-full w-full\">\n {/* We use key to force unmount when switching collections, preventing stale state */}\n {activeCollectionId ? (\n <CollectionStudioView\n key={activeCollectionId}\n configController={configController}\n collectionId={activeCollectionId}\n onSave={(savedCollection) => {\n // After creating a new collection, switch to it\n if (activeCollectionId === \"new\" && savedCollection?.slug) {\n setActiveCollectionId(savedCollection.slug);\n }\n }}\n onDirtyChange={setEditorDirty}\n // Not through the guard: the editor has already\n // asked about its own edits before cancelling.\n onCancel={() => setActiveCollectionId(undefined)}\n propertyTypePresets={propertyTypePresets}\n hiddenPropertyTypes={hiddenPropertyTypes}\n renderExtraPropertyFields={renderExtraPropertyFields}\n renderExtraCollectionFields={renderExtraCollectionFields}\n visibleTabs={visibleTabs}\n standalone={standalone}\n />\n ) : (\n <div className=\"flex flex-col items-center justify-center h-full gap-4\">\n <Typography variant=\"label\">\n Select a collection or create a new one to start editing\n </Typography>\n <Button\n disabled={configController.readOnly}\n onClick={() => setActiveCollectionId(\"new\")}\n >\n <PlusIcon/>\n Add new collection\n </Button>\n </div>\n )}\n </div>\n }\n />\n <UnsavedChangesDialog\n open={pendingSelection !== null}\n body={\"There are unsaved changes in this collection\"}\n handleOk={() => {\n const next = pendingSelection;\n setPendingSelection(null);\n setEditorDirty(false);\n if (next) setActiveCollectionId(next.id);\n }}\n handleCancel={() => setPendingSelection(null)}\n />\n </div>\n );\n}\n","/**\n * Router-aware wrapper around CollectionsStudioView.\n *\n * This component provides the react-router-based navigation behavior\n * that Rebase Studio expects: the active collection is derived from the\n * URL, and clicking a collection navigates via react-router.\n *\n * External consumers should use `CollectionsStudioView` directly\n * (which has no react-router dependency).\n */\nimport React from \"react\";\nimport { useLocation, useNavigate } from \"react-router\";\nimport { useUrlController } from \"../../_cms_internals\";\nimport { CollectionsStudioView, CollectionsStudioViewProps } from \"./CollectionsStudioView\";\n\nexport type RouterCollectionsStudioViewProps = Omit<CollectionsStudioViewProps, \"activeCollectionId\" | \"onActiveCollectionChange\">;\n\nexport function RouterCollectionsStudioView(props: RouterCollectionsStudioViewProps) {\n const navigate = useNavigate();\n const urlController = useUrlController();\n const location = useLocation();\n\n // Determine the active collection from the URL segment after \"schema/\"\n const basePath = urlController.buildAppUrlPath(\"schema\");\n const relativePath = location.pathname.replace(basePath, \"\").replace(/^\\//, \"\");\n const activeCollectionId = relativePath.split(\"/\")[0] || undefined;\n\n return (\n <CollectionsStudioView\n {...props}\n activeCollectionId={activeCollectionId}\n onActiveCollectionChange={(id) => {\n if (id) {\n navigate(urlController.buildAppUrlPath(`schema/${id}`));\n } else {\n navigate(urlController.buildAppUrlPath(\"schema\"));\n }\n }}\n />\n );\n}\n"],"mappings":"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"}
@@ -161,25 +161,37 @@ function WithheldConstraints({ constraints }) {
161
161
  })]
162
162
  });
163
163
  }
164
+ /**
165
+ * How far a failed apply got, in a heading.
166
+ *
167
+ * The statements run one at a time with no transaction around them, so only
168
+ * "none ran" means the database is as it was.
169
+ */
170
+ function partialApplyHeading(result) {
171
+ const ran = result.appliedStatements;
172
+ if (ran === 0) return "The database was not changed";
173
+ if (ran === void 0) return "The database may have been changed in part";
174
+ return `${ran} of ${result.statements.length} statements ran, then one failed`;
175
+ }
164
176
  /** The receipt. Shown in place of the plan once the change has been applied. */
165
177
  function Applied({ result }) {
166
178
  return /* @__PURE__ */ jsxs("div", {
167
179
  className: "flex flex-col gap-4",
168
180
  children: [
169
181
  /* @__PURE__ */ jsx(Alert, {
170
- color: result.applied ? "success" : "warning",
182
+ color: result.applied || result.sourceOnly ? "success" : "warning",
171
183
  children: /* @__PURE__ */ jsx(Typography, {
172
184
  variant: "body2",
173
185
  children: result.summary
174
186
  })
175
187
  }),
176
- !result.applied && result.applyError && /* @__PURE__ */ jsxs(Alert, {
188
+ !result.applied && !result.sourceOnly && result.applyError && /* @__PURE__ */ jsxs(Alert, {
177
189
  color: "warning",
178
190
  children: [
179
191
  /* @__PURE__ */ jsx(Typography, {
180
192
  variant: "body2",
181
193
  className: "font-medium mb-1",
182
- children: "The database was not changed"
194
+ children: partialApplyHeading(result)
183
195
  }),
184
196
  /* @__PURE__ */ jsx(Typography, {
185
197
  variant: "body2",
@@ -191,7 +203,7 @@ function Applied({ result }) {
191
203
  children: [
192
204
  "The commit is on ",
193
205
  /* @__PURE__ */ jsx("code", { children: result.committed.branch }),
194
- " either way, so the change will be applied on the next boot."
206
+ " either way, so whatever did not run will be applied on the next boot."
195
207
  ]
196
208
  })
197
209
  ]
@@ -221,7 +233,7 @@ function SchemaChangeDialog({ open, collectionId, plan, planError, result, apply
221
233
  const commitOnly = Boolean(plan?.applicable) && statements === 0;
222
234
  const copy = verdict === "safe" && commitOnly ? {
223
235
  title: "Ready to commit",
224
- body: "This changes what your project declares. Nothing runs against the database — there is no schema change to make.",
236
+ body: plan && plan.changes.length > 0 ? "Nothing runs against the database now. Each change below says what it does and when it takes effect." : "This changes what your project declares. Nothing runs against the database — there is no schema change to make.",
225
237
  color: "info"
226
238
  } : verdict ? VERDICT_COPY[verdict] : void 0;
227
239
  return /* @__PURE__ */ jsxs(Dialog, {
@@ -233,7 +245,7 @@ function SchemaChangeDialog({ open, collectionId, plan, planError, result, apply
233
245
  children: [
234
246
  /* @__PURE__ */ jsxs(DialogTitle, {
235
247
  className: "flex items-center gap-2",
236
- children: [result ? "Schema change applied" : "Review schema change", /* @__PURE__ */ jsx("code", {
248
+ children: [result ? result.sourceOnly ? "Schema change committed" : "Schema change applied" : "Review schema change", /* @__PURE__ */ jsx("code", {
237
249
  className: "text-sm font-normal text-text-secondary dark:text-text-secondary-dark",
238
250
  children: collectionId
239
251
  })]
@@ -283,12 +295,21 @@ function SchemaChangeDialog({ open, collectionId, plan, planError, result, apply
283
295
  plan.changes.length === 0 && /* @__PURE__ */ jsx(Typography, {
284
296
  variant: "body2",
285
297
  color: "secondary",
286
- children: "Nothing here changes the shape of your data. The collection source is still rewritten and committed."
298
+ children: "Nothing here changes the shape of your data. Only the collection source changes, and it is committed."
287
299
  }),
288
- !plan.applicable && onSourceOnly && /* @__PURE__ */ jsx(Typography, {
289
- variant: "body2",
290
- color: "secondary",
291
- children: "You can still write the change to your collection source and leave the database as it is — the editor’s behaviour before this preview existed. The column stays, holding whatever is in it, and nothing serves it."
300
+ !plan.applicable && onSourceOnly && /* @__PURE__ */ jsxs(Alert, {
301
+ color: "warning",
302
+ children: [/* @__PURE__ */ jsx(Typography, {
303
+ variant: "body2",
304
+ className: "font-medium mb-1",
305
+ children: "Edit source only: commit the collection, leave the database"
306
+ }), /* @__PURE__ */ jsx("ul", {
307
+ className: "space-y-1",
308
+ children: plan.changes.filter((change) => change.verdict !== "safe" && change.sourceOnly).map((change, index) => /* @__PURE__ */ jsx("li", {
309
+ className: "text-sm",
310
+ children: change.sourceOnly
311
+ }, `${change.property ?? change.collection}.${index}`))
312
+ })]
292
313
  }),
293
314
  plan.changes.length > 0 && /* @__PURE__ */ jsx("ul", {
294
315
  className: "flex flex-col gap-3",
@@ -393,4 +414,4 @@ function SchemaChangeDialog({ open, collectionId, plan, planError, result, apply
393
414
  //#endregion
394
415
  export { SchemaChangeDialog_exports as n, SchemaChangeDialog as t };
395
416
 
396
- //# sourceMappingURL=SchemaChangeDialog-BeP0A4MJ.js.map
417
+ //# sourceMappingURL=SchemaChangeDialog-CcFSXvkK.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SchemaChangeDialog-CcFSXvkK.js","names":[],"sources":["../src/collection_editor/ui/collection_editor/SchemaChangeDialog.tsx"],"sourcesContent":["/**\n * What a schema change would do, before it does it.\n *\n * The dialog exists because a schema change has three possible verdicts and two\n * of them are refusals. Without a preview, the only way to discover that a\n * change is refused is to press a button on a live database and read what comes\n * back — which is a fine way to learn and a poor way to work.\n *\n * ## What it shows, in the order somebody reads it\n *\n * The verdict first, in a sentence, because it decides whether the rest is\n * worth reading. Then the changes, each with what it does and — when it is\n * refused — what to do instead. Then the things that are easy to skip past and\n * expensive to have skipped: constraints the change asks for that the database\n * will not end up enforcing. The SQL and the file list come last, collapsed,\n * for the reader who wants them; they are the evidence, not the summary.\n *\n * ## Why the button says \"Commit and apply\"\n *\n * Because that is the order, and the order is the feature. The commit lands\n * first, so a failed apply leaves the repository ahead of the database — the\n * ordinary state of every project between an edit and a deploy, which the next\n * boot reconciles. A button labelled \"Save\" would be describing a different,\n * worse thing.\n */\nimport React, { useState } from \"react\";\nimport {\n Alert,\n Button,\n Chip,\n CircularProgress,\n cls,\n Dialog,\n DialogActions,\n DialogContent,\n DialogTitle,\n Typography\n} from \"@rebasepro/ui\";\n\nimport type { LiveSchemaPlan, LiveSchemaResult, WithheldConstraint } from \"../../liveSchemaClient\";\n\nexport interface SchemaChangeDialogProps {\n open: boolean;\n /** The collection being changed, for the title. */\n collectionId: string;\n /** `undefined` while the plan is still being fetched. */\n plan?: LiveSchemaPlan;\n /** Set when planning itself failed — a network error, a refusal, a 500. */\n planError?: string;\n /** Set once the change has been applied, which turns this into a receipt. */\n result?: LiveSchemaResult;\n applying: boolean;\n /** Set when the apply threw rather than returning a result. */\n applyError?: string;\n onConfirm: () => void;\n /**\n * Ask again, when planning failed.\n *\n * A dropped connection should cost a click, not the whole wizard. The form\n * is already filled in, and making somebody redo it for a transient failure\n * teaches them to distrust the preview rather than the network.\n */\n onRetry?: () => void;\n /**\n * Write the collection source and leave the database alone.\n *\n * Offered only when the change is refused, and it is what keeps the editor\n * usable while it is. Removing a property is the ordinary case: the ensure\n * path has no `DROP COLUMN`, so a removal can never be applied — and\n * refusing the whole save over it would mean a dev could no longer delete a\n * field from a collection they are still designing.\n *\n * This is exactly what the editor did before live editing existed, so it is\n * not a new hazard; the difference is that it now says what it leaves\n * behind instead of reporting success.\n */\n onSourceOnly?: () => void;\n onClose: () => void;\n /**\n * Why this caller may preview but not apply, when that is the case.\n *\n * A person signed in with an API key, most often. Shown next to a disabled\n * button so the refusal arrives while they are deciding rather than after\n * they have decided.\n */\n applyRefusedBecause?: string;\n /**\n * Where a confirmation would land — `owner/repo` and a branch.\n *\n * On screen before the button, not in the receipt after it. A commit is\n * going somewhere, and which branch it is going to is the one fact somebody\n * cannot recover from being wrong about.\n */\n target?: { repository?: string; branch?: string };\n}\n\ntype Verdict = LiveSchemaPlan[\"verdict\"];\n\n/** What the verdict means, in the words the reader needs rather than the enum's. */\n/**\n * What the verdict means, in the words the reader needs rather than the enum's.\n *\n * `safe` is written twice because the two cases are genuinely different, and\n * saying \"can be made against the running database\" when nothing will be run is\n * the kind of small lie that teaches people to stop reading. A document\n * database has no table to alter, and a change that needs no DDL on a\n * relational one is in the same position: the commit *is* the change.\n */\nconst VERDICT_COPY: Record<Verdict, { title: string; body: string; color: \"info\" | \"warning\" | \"error\" }> = {\n safe: {\n title: \"Ready to apply\",\n body: \"Every change here can be made against the running database.\",\n color: \"info\"\n },\n diverges: {\n title: \"This would leave the database out of step\",\n body: \"The change can be written, but the database will not end up matching it. \" +\n \"The details below say which part, and what would make it applicable.\",\n color: \"warning\"\n },\n \"needs-migration\": {\n title: \"This needs a migration\",\n body: \"Some of this cannot be done to a running database at all — it drops or rewrites \" +\n \"something, and that belongs in a migration somebody has read.\",\n color: \"error\"\n }\n};\n\nconst VERDICT_CHIP: Record<Verdict, { label: string; scheme: \"greenLighter\" | \"orangeLighter\" | \"redLighter\" }> = {\n safe: { label: \"safe\", scheme: \"greenLighter\" },\n diverges: { label: \"diverges\", scheme: \"orangeLighter\" },\n \"needs-migration\": { label: \"needs migration\", scheme: \"redLighter\" }\n};\n\nconst monospace = \"font-mono text-xs leading-relaxed\";\nconst codeBlock = cls(\n monospace,\n \"bg-surface-raised rounded-md p-3 overflow-x-auto whitespace-pre\"\n);\n\n/** A disclosure that starts closed. The evidence, for the reader who wants it. */\nfunction Details({ summary, count, children }: {\n summary: string;\n count: number;\n children: React.ReactNode;\n}) {\n const [open, setOpen] = useState(false);\n if (count === 0) return null;\n return (\n <div>\n <button\n type=\"button\"\n onClick={() => setOpen(o => !o)}\n className=\"flex items-center gap-2 text-sm text-text-secondary dark:text-text-secondary-dark hover:underline\"\n >\n <span className={cls(\"transition-transform\", open && \"rotate-90\")}>›</span>\n {summary} ({count})\n </button>\n {open && <div className=\"mt-2\">{children}</div>}\n </div>\n );\n}\n\n/**\n * What still has to happen after this lands.\n *\n * Above the evidence and below the changes, because it is not a refusal and not\n * a detail: the change will apply, and something outside this dialog still has\n * to be done or a later environment will differ from this one.\n */\nfunction FollowUp({ items }: { items: string[] }) {\n if (items.length === 0) return null;\n return (\n <Alert color=\"info\">\n <Typography variant=\"body2\" className=\"font-medium mb-1\">\n {items.length === 1 ? \"One thing to do afterwards\" : \"To do afterwards\"}\n </Typography>\n <ul className=\"space-y-2\">\n {items.map(item => (\n <li key={item} className=\"text-sm\">{item}</li>\n ))}\n </ul>\n </Alert>\n );\n}\n\n/**\n * The destination, short enough to read at a glance.\n *\n * A remote repository already reports itself as `owner/repo`, which is exactly\n * what somebody recognises. A local one reports an absolute path, and the\n * leading directories are the part nobody needs: what matters is which\n * checkout, and its name says that.\n */\nfunction repositoryLabel(repository: string): string {\n if (!repository.startsWith(\"/\")) return repository;\n return repository.split(\"/\").filter(Boolean).pop() ?? repository;\n}\n\nfunction WithheldConstraints({ constraints }: { constraints: WithheldConstraint[] }) {\n if (constraints.length === 0) return null;\n return (\n <Alert color=\"warning\">\n <Typography variant=\"body2\" className=\"font-medium mb-1\">\n {constraints.length === 1\n ? \"One constraint will not be enforced\"\n : `${constraints.length} constraints will not be enforced`}\n </Typography>\n <ul className=\"space-y-2\">\n {constraints.map(constraint => (\n <li key={constraint.target} className=\"text-sm\">\n <code className=\"bg-surface-raised px-1 rounded\">\n {constraint.target}\n </code>\n <div className=\"mt-1\">{constraint.reason}</div>\n <div className=\"mt-1 text-text-secondary dark:text-text-secondary-dark\">\n {constraint.remedy}\n </div>\n </li>\n ))}\n </ul>\n </Alert>\n );\n}\n\n/**\n * How far a failed apply got, in a heading.\n *\n * The statements run one at a time with no transaction around them, so only\n * \"none ran\" means the database is as it was.\n */\nfunction partialApplyHeading(result: LiveSchemaResult): string {\n const ran = result.appliedStatements;\n if (ran === 0) return \"The database was not changed\";\n if (ran === undefined) return \"The database may have been changed in part\";\n return `${ran} of ${result.statements.length} statements ran, then one failed`;\n}\n\n/** The receipt. Shown in place of the plan once the change has been applied. */\nfunction Applied({ result }: { result: LiveSchemaResult }) {\n return (\n <div className=\"flex flex-col gap-4\">\n <Alert color={result.applied || result.sourceOnly ? \"success\" : \"warning\"}>\n <Typography variant=\"body2\">{result.summary}</Typography>\n </Alert>\n\n {!result.applied && !result.sourceOnly && result.applyError && (\n // Deliberately not an error state. The commit landed, which is\n // the durable half; the database is now behind the repository,\n // which is where every project sits between an edit and a\n // deploy, and boot reconciles it.\n <Alert color=\"warning\">\n <Typography variant=\"body2\" className=\"font-medium mb-1\">\n {partialApplyHeading(result)}\n </Typography>\n <Typography variant=\"body2\">{result.applyError}</Typography>\n <Typography variant=\"body2\" className=\"mt-2\">\n The commit is on <code>{result.committed.branch}</code> either way, so\n whatever did not run will be applied on the next boot.\n </Typography>\n </Alert>\n )}\n\n <WithheldConstraints constraints={result.withheldConstraints ?? []}/>\n <FollowUp items={result.followUp ?? []}/>\n\n <div>\n <Typography variant=\"label\" color=\"secondary\">Committed</Typography>\n <div className={cls(codeBlock, \"mt-1\")}>\n {result.committed.sha.slice(0, 9)} on {result.committed.branch}\n {\"\\n\"}\n {result.committed.files.map(file => ` ${file}`).join(\"\\n\")}\n </div>\n </div>\n </div>\n );\n}\n\nexport function SchemaChangeDialog({\n open,\n collectionId,\n plan,\n planError,\n result,\n applying,\n applyError,\n onConfirm,\n onRetry,\n onSourceOnly,\n onClose,\n applyRefusedBecause,\n target\n}: SchemaChangeDialogProps) {\n\n const verdict = plan?.verdict;\n const statements = plan?.statements.length ?? 0;\n\n // \"Commit\" only when we positively know there is nothing to apply: a plan\n // that is going ahead and has no statements. A *refused* plan also has none\n // — because it will not run — and labelling that button \"Commit\" would be\n // describing the change it is refusing to make. Unknown reads as the fuller\n // action, which is the safer way to be wrong about a label.\n const commitOnly = Boolean(plan?.applicable) && statements === 0;\n\n // Described from the plan rather than from an assumption about the engine.\n // A document database runs nothing; so does a relational change that only\n // moves source. Both are a commit, and calling either one \"apply\" would be\n // describing something that does not happen.\n // A plan with changes and no statements still changes something — a\n // security rule takes effect when the server next starts — so \"there is\n // no schema change to make\" is said only when there is none.\n const copy = verdict === \"safe\" && commitOnly\n ? {\n title: \"Ready to commit\",\n body: plan && plan.changes.length > 0\n ? \"Nothing runs against the database now. Each change below says what it does and \" +\n \"when it takes effect.\"\n : \"This changes what your project declares. Nothing runs against the database — \" +\n \"there is no schema change to make.\",\n color: \"info\" as const\n }\n : verdict\n ? VERDICT_COPY[verdict]\n : undefined;\n\n return (\n <Dialog open={open} onOpenChange={value => { if (!value) onClose(); }} maxWidth=\"3xl\">\n <DialogTitle className=\"flex items-center gap-2\">\n {result ? (result.sourceOnly ? \"Schema change committed\" : \"Schema change applied\") : \"Review schema change\"}\n <code className=\"text-sm font-normal text-text-secondary dark:text-text-secondary-dark\">\n {collectionId}\n </code>\n </DialogTitle>\n\n <DialogContent className=\"flex flex-col gap-4\">\n {planError && (\n <Alert color=\"error\">\n <Typography variant=\"body2\" className=\"font-medium mb-1\">\n The change could not be planned\n </Typography>\n <Typography variant=\"body2\">{planError}</Typography>\n </Alert>\n )}\n\n {!plan && !planError && !result && (\n <div className=\"flex items-center gap-3 py-6\">\n <CircularProgress size=\"small\"/>\n <Typography variant=\"body2\" color=\"secondary\">\n Working out what this change would do…\n </Typography>\n </div>\n )}\n\n {result && <Applied result={result}/>}\n\n {plan && !result && copy && (\n <>\n <Alert color={copy.color}>\n <Typography variant=\"body2\" className=\"font-medium mb-1\">\n {copy.title}\n </Typography>\n <Typography variant=\"body2\">{copy.body}</Typography>\n </Alert>\n\n {applyError && (\n <Alert color=\"error\">\n <Typography variant=\"body2\">{applyError}</Typography>\n </Alert>\n )}\n\n {plan.changes.length === 0 && (\n <Typography variant=\"body2\" color=\"secondary\">\n Nothing here changes the shape of your data. Only the collection\n source changes, and it is committed.\n </Typography>\n )}\n\n {!plan.applicable && onSourceOnly && (\n // Said per change: what a removal leaves behind is\n // not what a type change or a new required field\n // does, and the one that breaks every insert has to\n // be on screen before the button is pressed.\n <Alert color=\"warning\">\n <Typography variant=\"body2\" className=\"font-medium mb-1\">\n Edit source only: commit the collection, leave the database\n </Typography>\n <ul className=\"space-y-1\">\n {plan.changes.filter(change => change.verdict !== \"safe\" && change.sourceOnly).map((change, index) => (\n <li key={`${change.property ?? change.collection}.${index}`} className=\"text-sm\">\n {change.sourceOnly}\n </li>\n ))}\n </ul>\n </Alert>\n )}\n\n {plan.changes.length > 0 && (\n <ul className=\"flex flex-col gap-3\">\n {plan.changes.map((change, index) => (\n <li\n key={`${change.collection}.${change.property ?? \"\"}.${index}`}\n className=\"flex gap-3\"\n >\n <Chip\n size=\"smallest\"\n colorScheme={VERDICT_CHIP[change.verdict].scheme}\n className=\"shrink-0 mt-0.5\"\n >\n {VERDICT_CHIP[change.verdict].label}\n </Chip>\n <div className=\"min-w-0\">\n <Typography variant=\"body2\">{change.detail}</Typography>\n {change.remedy && (\n <Typography\n variant=\"body2\"\n color=\"secondary\"\n className=\"mt-1\"\n >\n {change.remedy}\n </Typography>\n )}\n </div>\n </li>\n ))}\n </ul>\n )}\n\n <WithheldConstraints constraints={plan.withheldConstraints ?? []}/>\n <FollowUp items={plan.followUp ?? []}/>\n\n {applyRefusedBecause && (\n <Alert color=\"info\">\n <Typography variant=\"body2\" className=\"font-medium mb-1\">\n You can preview this change, but not apply it\n </Typography>\n <Typography variant=\"body2\">{applyRefusedBecause}</Typography>\n </Alert>\n )}\n\n <Details summary=\"Statements that will run\" count={plan.statements.length}>\n <div className={codeBlock}>{plan.statements.join(\"\\n\")}</div>\n </Details>\n\n <Details summary=\"Files that will be committed\" count={plan.files.length}>\n <div className={codeBlock}>{plan.files.join(\"\\n\")}</div>\n </Details>\n\n <Details summary=\"Commit message\" count={plan.message ? 1 : 0}>\n <div className={codeBlock}>{plan.message}</div>\n </Details>\n </>\n )}\n </DialogContent>\n\n <DialogActions>\n {!result && target?.repository && (\n <span className={cls(\n monospace,\n \"mr-auto text-text-secondary dark:text-text-secondary-dark truncate\"\n )}>\n → {repositoryLabel(target.repository)}{target.branch ? ` · ${target.branch}` : \"\"}\n </span>\n )}\n <Button variant=\"text\" onClick={onClose} disabled={applying}>\n {result ? \"Close\" : \"Cancel\"}\n </Button>\n {!result && planError && onRetry && (\n <Button variant=\"outlined\" onClick={onRetry}>\n Try again\n </Button>\n )}\n {!result && plan && !plan.applicable && onSourceOnly && (\n <Button variant=\"outlined\" onClick={onSourceOnly} disabled={applying}>\n Edit source only\n </Button>\n )}\n {!result && (\n <Button\n variant=\"filled\"\n color=\"primary\"\n onClick={onConfirm}\n // `applicable` is the server's word, not a\n // re-derivation from the verdicts on screen:\n // `applySchemaChange` re-checks it before it writes\n // anything, so a button that disagreed would only\n // produce the same refusal one click later. The same\n // goes for the permission — `/apply` checks it too, and\n // this only saves somebody the round trip.\n disabled={!plan?.applicable || applying || Boolean(applyRefusedBecause)}\n >\n {applying\n ? (commitOnly ? \"Committing…\" : \"Applying…\")\n : (commitOnly ? \"Commit\" : \"Commit and apply\")}\n </Button>\n )}\n </DialogActions>\n </Dialog>\n );\n}\n"],"mappings":"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"}
@@ -20,7 +20,9 @@ export { ImportFileUpload } from "../data_import/components/ImportFileUpload.js"
20
20
  export { ImportSaveInProgress } from "../data_import/components/ImportSaveInProgress.js";
21
21
  export { useImportConfig } from "../data_import/hooks/useImportConfig.js";
22
22
  export { getInferenceType } from "../data_import/utils/get_import_inference_type.js";
23
- export { convertDataToEntity } from "../data_import/utils/data.js";
23
+ export { convertDataToEntity, convertImportData } from "../data_import/utils/data.js";
24
+ export { ImportConversionProblems } from "../data_import/components/ImportConversionProblems.js";
25
+ export { guessIdColumn } from "../data_import/utils/id_column.js";
24
26
  export type { ImportConfig } from "../data_import/types/index.js";
25
27
  export { DataNewPropertiesMapping } from "../data_import/components/DataNewPropertiesMapping.js";
26
28
  export { ImportNewPropertyFieldPreview } from "../data_import/components/ImportNewPropertyFieldPreview.js";
@@ -7,18 +7,17 @@
7
7
  * try-and-see would be asking somebody to discover that their change is refused
8
8
  * by pressing a button on a live database.
9
9
  *
10
- * ## Partial payloads are the caller's problem, not this module's
10
+ * ## A change to an existing collection is a patch
11
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.
12
+ * What the person changed — the difference between the collection the editor
13
+ * loaded and the one it is saving, from `diffCollections` in
14
+ * `@rebasepro/types` — not the collection. The server applies it to the
15
+ * collection as it is to plan the change, and writes only those keys into the
16
+ * file. Posting the whole collection had the server write back the panel's
17
+ * JSON, which had lost every handler, shared property and imported value.
18
+ * A new collection is still sent whole.
21
19
  */
20
+ import type { CollectionPatch } from "@rebasepro/types";
22
21
  /** A constraint the change asks for that the statements will not carry. */
23
22
  export interface WithheldConstraint {
24
23
  target: string;
@@ -37,6 +36,12 @@ export interface LiveSchemaPlan {
37
36
  property?: string;
38
37
  detail: string;
39
38
  remedy?: string;
39
+ /**
40
+ * What "Edit source only" leaves behind for this change. Absent on a
41
+ * change that may not be written to the source alone.
42
+ */
43
+ sourceOnly?: string;
44
+ kept?: string;
40
45
  }>;
41
46
  statements: string[];
42
47
  files: string[];
@@ -54,14 +59,23 @@ export interface LiveSchemaPlan {
54
59
  *
55
60
  * Empty almost always. The case it exists for: a project that replays
56
61
  * versioned migrations, where applying here changes *this* database and
57
- * commits `drizzle/schema.sql`, but writes no migration — so the next
62
+ * commits the collection, but writes no migration — so the next
58
63
  * environment built from migrations would not have the change.
59
64
  */
60
65
  followUp?: string[];
61
66
  }
62
67
  export interface LiveSchemaResult {
63
68
  applied: boolean;
69
+ /** Committed as "Edit source only": nothing was meant to run. */
70
+ sourceOnly?: boolean;
64
71
  applyError?: string;
72
+ /**
73
+ * How many statements ran before the one that failed. The statements run
74
+ * one at a time with no transaction around them, so a failure part-way
75
+ * leaves the earlier ones applied. `undefined` when the server could not
76
+ * say.
77
+ */
78
+ appliedStatements?: number;
65
79
  committed: {
66
80
  sha: string;
67
81
  branch: string;
@@ -129,11 +143,12 @@ export interface LiveSchemaClientOptions {
129
143
  }
130
144
  export interface ProposedCollectionChange {
131
145
  collectionId: string;
132
- /**
133
- * The **whole** collection as it should end up — not a patch. See the
134
- * module comment: a partial reads as a set of removals.
135
- */
136
- collection: Record<string, unknown>;
146
+ /** A new collection, whole. */
147
+ collection?: Record<string, unknown>;
148
+ /** What changed about an existing one. See the module comment. */
149
+ patch?: CollectionPatch;
150
+ /** Delete the collection's source — its file and its `index.ts` entry. The table stays. */
151
+ remove?: true;
137
152
  }
138
153
  export declare function createLiveSchemaClient(options: LiveSchemaClientOptions): {
139
154
  /**
@@ -148,8 +163,13 @@ export declare function createLiveSchemaClient(options: LiveSchemaClientOptions)
148
163
  status: () => Promise<LiveSchemaStatus>;
149
164
  /** What the change would do. No side effects. */
150
165
  plan: (change: ProposedCollectionChange) => Promise<LiveSchemaPlan>;
151
- /** Commit the change, then apply it. */
152
- apply: (change: ProposedCollectionChange) => Promise<LiveSchemaResult>;
166
+ /**
167
+ * Commit the change, then apply it — or, with `sourceOnly`, commit it
168
+ * and run nothing ("Edit source only").
169
+ */
170
+ apply: (change: ProposedCollectionChange & {
171
+ sourceOnly?: boolean;
172
+ }) => Promise<LiveSchemaResult>;
153
173
  };
154
174
  /** The client's shape, for the hook and the tests that fake it. */
155
175
  export type LiveSchemaClient = ReturnType<typeof createLiveSchemaClient>;
@@ -21,3 +21,14 @@ export declare const kindUsesLocalKey: (kind: RelationKind) => kind is "belongsT
21
21
  export declare const kindUsesForeignKeyOnTarget: (kind: RelationKind) => kind is "hasMany" | "hasOne";
22
22
  export declare const kindUsesThrough: (kind: RelationKind) => kind is "manyToMany";
23
23
  export declare const kindUsesJoinPath: (kind: RelationKind) => kind is "via";
24
+ /**
25
+ * `relation` switched to `kind`, without the link fields its old kind owned.
26
+ *
27
+ * Switching kind in the property form used to set `kind` and nothing else, so
28
+ * a junction table filled in before switching to "Belongs to" stayed on the
29
+ * relation — `through` on a `belongsTo` — and a `localKey` rode along into a
30
+ * `hasMany`. The boot validator refuses both, and every collection in the
31
+ * directory stops loading. The name, the target and the referential actions
32
+ * belong to every kind and are kept.
33
+ */
34
+ export declare function relationWithKind(relation: Record<string, unknown>, kind: RelationKind): Record<string, unknown>;
@@ -21,6 +21,8 @@ export interface SerializableAdminBaseOptions {
21
21
  hideFromCollection?: boolean;
22
22
  readOnly?: boolean;
23
23
  disabled?: boolean | SerializablePropertyDisabledConfig;
24
+ /** The server fills this on create, so the form does not require it then. */
25
+ filledByServer?: boolean;
24
26
  span?: 1 | 2 | 3 | 4;
25
27
  /** Opaque props handed to a custom `Field` / `Preview`. Round-tripped as-is. */
26
28
  customProps?: unknown;
@@ -67,6 +67,17 @@ export type UpdateCollectionParams<M extends Record<string, unknown> = Record<st
67
67
  export type SaveCollectionParams<M extends Record<string, unknown> = Record<string, unknown>> = {
68
68
  id: string;
69
69
  collectionData: AdminCollection<M>;
70
+ /**
71
+ * The collection as the editor loaded it, before the person changed
72
+ * anything.
73
+ *
74
+ * A controller that writes source saves only the difference between this
75
+ * and `collectionData` — what the person changed. The whole collection is
76
+ * JSON by the time it is sent, and writing it back whole deleted every
77
+ * handler, shared property and imported value JSON could not carry.
78
+ * Absent, the controller's own copy of the collection stands in for it.
79
+ */
80
+ baseline?: AdminCollection<M>;
70
81
  previousId?: string;
71
82
  parentCollectionSlugs?: string[];
72
83
  parentEntityIds?: string[];
@@ -9,10 +9,17 @@ export type CollectionStudioViewProps = Omit<CollectionEditorDialogProps, "open"
9
9
  * When not provided, save completes silently.
10
10
  */
11
11
  onSave?: (collection?: AdminCollection) => void;
12
+ /**
13
+ * Called when the collection on screen gains or loses unsaved edits.
14
+ *
15
+ * For whatever decides which collection is on screen: this view is
16
+ * remounted when that changes, and its edits go with it.
17
+ */
18
+ onDirtyChange?: (dirty: boolean) => void;
12
19
  /**
13
20
  * Called when the user cancels editing.
14
21
  * When not provided, cancel is a no-op.
15
22
  */
16
23
  onCancel?: () => void;
17
24
  };
18
- export declare function CollectionStudioView({ collectionId, onSave, onCancel, ...props }: CollectionStudioViewProps): React.JSX.Element;
25
+ export declare function CollectionStudioView({ collectionId, onSave, onCancel, onDirtyChange, ...props }: CollectionStudioViewProps): React.JSX.Element;
@@ -30,12 +30,12 @@ export interface UseLiveSchemaEditingOptions {
30
30
  */
31
31
  authKey?: string | null;
32
32
  /**
33
- * Write the collection source and leave the database alone — what the
34
- * editor did before this existed. Offered in the dialog when a change is
35
- * refused, so a dev can still delete a field from a collection they are
36
- * designing. Omit it and the fallback is not offered.
33
+ * Whether to ask the backend at all. `false` when the caller cannot be told
34
+ * anything (`/status` needs `schema:read`) or the editor is read-only: the
35
+ * answer is then "not available" without a request that could only be
36
+ * refused. Defaults to `true`.
37
37
  */
38
- writeSourceOnly?: (change: ProposedCollectionChange) => Promise<void>;
38
+ enabled?: boolean;
39
39
  /** Swappable for the tests, which have no server. */
40
40
  client?: LiveSchemaClient;
41
41
  }
@@ -11,4 +11,11 @@ export declare function useLocalCollectionsConfigController(clientOrUrl: any, ba
11
11
  * anonymous probe does not survive a sign-in.
12
12
  */
13
13
  authKey?: string | null;
14
+ /**
15
+ * Whether the signed-in user holds `schema:read`, which both
16
+ * status routes require. `false` asks neither and leaves the editor
17
+ * read-only, because a 403 is the only answer such a user can get.
18
+ * Absent means ask.
19
+ */
20
+ mayReadSchema?: boolean;
14
21
  }): CollectionsConfigController;
@@ -1,8 +1,8 @@
1
- import { Sn as useUrlController } from "./util-CyIcgnv2.js";
2
- import { n as CollectionEditorDialog } from "./CollectionEditorDialog-CUtfS_49.js";
3
- import { n as PropertyForm, r as PropertyFormDialog } from "./PropertyEditView-BpEbC0Q2.js";
4
- import { i as CollectionStudioView, r as CollectionsStudioView, t as RouterCollectionsStudioView } from "./RouterCollectionsStudioView-EK8Knq5I.js";
5
- import { t as SchemaChangeDialog } from "./SchemaChangeDialog-BeP0A4MJ.js";
1
+ import { Bn as useUrlController } from "./util-BRvKFMLa.js";
2
+ import { n as CollectionEditorDialog } from "./CollectionEditorDialog-p5-I-Apu.js";
3
+ import { n as PropertyForm, r as PropertyFormDialog } from "./PropertyEditView-DDB_X-00.js";
4
+ import { i as CollectionStudioView, r as CollectionsStudioView, t as RouterCollectionsStudioView } from "./RouterCollectionsStudioView-SGBV4CMg.js";
5
+ import { t as SchemaChangeDialog } from "./SchemaChangeDialog-CcFSXvkK.js";
6
6
  import "react";
7
7
  import { useSnackbarController } from "@rebasepro/app";
8
8
  import { jsx } from "react/jsx-runtime";
@@ -1,7 +1,13 @@
1
+ import type { FilterValues } from "@rebasepro/types";
1
2
  import { EntityTableController } from "@rebasepro/cms-types";
2
- export declare function ClearFilterSortButton({ tableController, enabled, compact }: {
3
+ export declare function ClearFilterSortButton({ tableController, enabled, fixedFilter, compact }: {
3
4
  enabled: boolean;
4
5
  tableController: EntityTableController;
6
+ /**
7
+ * The collection's fixed filter. Its fields are not the user's to clear,
8
+ * so they do not count as a filter that is set.
9
+ */
10
+ fixedFilter?: FilterValues<string>;
5
11
  /**
6
12
  * Toolbar showing icon-only controls (split view, or a layout too narrow
7
13
  * for the filters button's label). Only decides the icon size, which
@@ -1,23 +1,22 @@
1
1
  import React from "react";
2
2
  /**
3
- * The one-line editor a reference or relation cell shows while selected, at
4
- * the row sizes that carry small previews (`xs`, `s`, `m`).
3
+ * A reference or relation cell, at the row sizes that carry small previews
4
+ * (`xs`, `s`, `m`), for the pickers that open a selection dialog.
5
5
  *
6
- * The flow it replaces: at 48px the selected cell swapped its resting inline
7
- * line for the card editor built for 140px rows, which was clipped to its top
8
- * third and whose only click opened the picker — there was no way to look at
9
- * the record. Here the line stays (its title opens the record in the side
10
- * panel, exactly as it does at rest), a pencil opens the picker and a cross
11
- * clears the value. Click the name to see it, click the pencil to change it.
6
+ * The value is one line of inline previews, the same at rest and selected:
7
+ * each title opens its record in the side panel. The cell's opener — the
8
+ * chevron it reveals on hover — opens the dialog, and a cross beside it clears
9
+ * the value. The cross follows the opener: hidden at rest, shown on hover and
10
+ * while the cell is selected. Unlike the opener it saves at once, so hidden it
11
+ * cannot be hit either: a tap where nothing is hovered selects the cell.
12
12
  */
13
- export declare function CompactEntityCellField({ children, empty, disabled, onEdit, onClear, emptyLabel }: {
13
+ export declare function CompactEntityCellField({ children, empty, disabled, selected, onClear }: {
14
14
  /** The inline previews of the current value or values. */
15
15
  children?: React.ReactNode;
16
- /** No value: the line becomes the select prompt. */
16
+ /** No value: the line shows the empty marker every other empty cell shows. */
17
17
  empty: boolean;
18
18
  disabled: boolean;
19
- onEdit: () => void;
19
+ /** The cell is selected: the cross is shown whether or not it is hovered. */
20
+ selected?: boolean;
20
21
  onClear: () => void;
21
- /** Shown in place of the previews when empty. Defaults to "Select reference". */
22
- emptyLabel?: string;
23
22
  }): React.JSX.Element;