@rebasepro/cms 0.21.2-canary.g1ea48be → 0.23.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 (58) hide show
  1. package/dist/{CollectionEditorDialog-B1mG1cfx.js → CollectionEditorDialog-DLHCka00.js} +103 -58
  2. package/dist/CollectionEditorDialog-DLHCka00.js.map +1 -0
  3. package/dist/{PropertyEditView-gKNUHxLY.js → PropertyEditView-_pu5hzZn.js} +53 -21
  4. package/dist/PropertyEditView-_pu5hzZn.js.map +1 -0
  5. package/dist/{RouterCollectionsStudioView-qgDWC6E2.js → RouterCollectionsStudioView-ChPNl_uU.js} +34 -10
  6. package/dist/RouterCollectionsStudioView-ChPNl_uU.js.map +1 -0
  7. package/dist/collection_editor/_cms_internals.d.ts +1 -0
  8. package/dist/collection_editor/relation_kinds.d.ts +11 -0
  9. package/dist/collection_editor/ui/collection_editor/CollectionStudioView.d.ts +8 -1
  10. package/dist/collection_editor_ui.js +4 -4
  11. package/dist/components/CollectionTableBinding/fields/CompactEntityCellField.d.ts +12 -13
  12. package/dist/components/CollectionTableBinding/fields/TableMultipleRelationField.d.ts +6 -3
  13. package/dist/components/CollectionTableBinding/fields/TableReferenceField.d.ts +8 -1
  14. package/dist/components/CollectionTableBinding/fields/TableRelationField.d.ts +8 -1
  15. package/dist/components/CollectionTableBinding/fields/TableRelationSelectorField.d.ts +13 -3
  16. package/dist/components/CollectionTableBinding/fields/VirtualTableSelect.d.ts +10 -0
  17. package/dist/components/CollectionTableBinding/fields/VirtualTableUserSelect.d.ts +10 -0
  18. package/dist/components/CollectionTableBinding/internal/EntityTableCell.d.ts +21 -0
  19. package/dist/components/CollectionTableBinding/internal/TableCellOpener.d.ts +45 -0
  20. package/dist/components/CollectionTableBinding/table_bindings.d.ts +24 -0
  21. package/dist/components/CollectionViewBinding/CollectionViewBinding.d.ts +7 -1
  22. package/dist/components/EntityPreviewBinding.d.ts +8 -1
  23. package/dist/components/EntityPreviewNesting.d.ts +15 -0
  24. package/dist/components/PickerTrigger.d.ts +17 -0
  25. package/dist/components/ReferenceTable/SelectionTableBinding.d.ts +12 -0
  26. package/dist/components/RelationSelector.d.ts +24 -7
  27. package/dist/components/UserSelector.d.ts +8 -1
  28. package/dist/data_export/export/additional_fields.d.ts +13 -0
  29. package/dist/data_export/export/export.d.ts +6 -0
  30. package/dist/data_import/utils/csv.d.ts +3 -2
  31. package/dist/data_import/utils/id_column.d.ts +16 -0
  32. package/dist/editor.js +1 -1
  33. package/dist/{export-DIyk1720.js → export-4bJWdlDK.js} +63 -209
  34. package/dist/export-4bJWdlDK.js.map +1 -0
  35. package/dist/form/form_utils.d.ts +13 -0
  36. package/dist/form/validation.d.ts +13 -0
  37. package/dist/{history-CrZEzQ5b.js → history-BElrZ5Rw.js} +2 -2
  38. package/dist/{history-CrZEzQ5b.js.map → history-BElrZ5Rw.js.map} +1 -1
  39. package/dist/hooks/useEntityDisplay.d.ts +4 -5
  40. package/dist/{import-ByaJMZuT.js → import-TCgkTVlS.js} +3 -4
  41. package/dist/import-TCgkTVlS.js.map +1 -0
  42. package/dist/index.js +16 -10
  43. package/dist/index.js.map +1 -1
  44. package/dist/preview/util.d.ts +25 -0
  45. package/dist/util/navigation_utils.d.ts +14 -2
  46. package/dist/util/view_mode.d.ts +12 -18
  47. package/dist/{util-DB31IoJE.js → util-CC0ozHoD.js} +35 -2
  48. package/dist/util-CC0ozHoD.js.map +1 -0
  49. package/dist/{util-qfCPxJzB.js → util-DybQBkug.js} +1482 -717
  50. package/dist/util-DybQBkug.js.map +1 -0
  51. package/package.json +9 -9
  52. package/dist/CollectionEditorDialog-B1mG1cfx.js.map +0 -1
  53. package/dist/PropertyEditView-gKNUHxLY.js.map +0 -1
  54. package/dist/RouterCollectionsStudioView-qgDWC6E2.js.map +0 -1
  55. package/dist/export-DIyk1720.js.map +0 -1
  56. package/dist/import-ByaJMZuT.js.map +0 -1
  57. package/dist/util-DB31IoJE.js.map +0 -1
  58. package/dist/util-qfCPxJzB.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-qfCPxJzB.js";
3
- import { t as CollectionEditor } from "./CollectionEditorDialog-B1mG1cfx.js";
4
- import { s as AIModifiedPathsProvider } from "./PropertyEditView-gKNUHxLY.js";
2
+ import { Mn as useUrlController } from "./util-DybQBkug.js";
3
+ import { t as CollectionEditor } from "./CollectionEditorDialog-DLHCka00.js";
4
+ import { s as AIModifiedPathsProvider } from "./PropertyEditView-_pu5hzZn.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-qgDWC6E2.js.map
212
+ //# sourceMappingURL=RouterCollectionsStudioView-ChPNl_uU.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RouterCollectionsStudioView-ChPNl_uU.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":";;;;;;;;;;;AAmCA,SAAgB,qBAAqB,EAAE,cAAc,QAAQ,UAAU,eAAe,GAAG,SAAoC;CAGzH,MAAM,CAAC,WAAW,gBAAgB,SAAkB,KAAK;CAEzD,gBAAgB;EACZ,gBAAgB,SAAS;CAC7B,GAAG,CAAC,WAAW,aAAa,CAAC;CAC7B,MAAM,CAAC,iBAAiB,sBAAsB,SAAkB,KAAK;CAErE,MAAM,EAAE,aAAa,kBAAkB,wBACnC,iBACM,aAAa,KAAK,CAC5B;CAEA,MAAM,qBAAqB;CAE3B,MAAM,0BAA0B;EAC5B,IAAI,CAAC,WACD,WAAW;OACR;GACH,mBAAmB,IAAI;GACvB,cAAc;EAClB;CACJ;CAEA,OACI,oBAAC,OAAD;EAAK,WAAU;YACX,qBAAC,yBAAD,EAAA,UAAA,CACK,qBACG,oBAAC,kBAAD;GAEI,GAAI;GACJ,YAAY;GACZ,MAAM;GACN,iBAAiB,uBAAuB;GACxC,oBAAoB,uBAAuB,QAAQ,qBAAqB,KAAA;GACxE,cAAc;GACd,cAAc,oBAAoB;IAC9B,aAAa,KAAK;IAClB,IAAI,iBACA,iBAAiB;KACb,SAAS,eAAe;IAC5B,GAAG,CAAC;GAEZ;GACc;EACjB,GAhBQ,kBAgBR,IAED,qBAAC,OAAD;GAAK,WAAU;aAAf,CACI,oBAAC,YAAD;IAAY,SAAQ;cAAQ;GAEhB,CAAA,GACZ,qBAAC,QAAD;IACI,UAAU,MAAM,kBAAkB;IAClC,eAAe,SAAS;cAF5B,CAII,oBAAC,UAAD,CAAU,CAAA,GAAC,oBAEP;KACP;MAGT,oBAAC,sBAAD;GACI,GAAI;GACJ,gBAAgB;IACZ,YAAY,SAAS;IACrB,IAAI,iBAAiB;KACjB,WAAW;KACX,mBAAmB,KAAK;IAC5B;GACJ;GACA,oBAAoB;IAChB,YAAY,aAAa;IACzB,mBAAmB,KAAK;GAC5B;EACH,CAAA,CACoB,EAAA,CAAA;CACxB,CAAA;AAEb;;;ACnEA,SAAgB,sBAAsB,EAClC,kBACA,aAAa,iBACb,oBAAoB,oBACpB,0BACA,qBACA,qBACA,2BACA,6BACA,aACA,cAC2B;CAK3B,MAAM,CAAC,kBAAkB,uBAAuB,SAA6B,KAAA,CAAS;CACtF,MAAM,eAAe,6BAA6B,KAAA;CAClD,MAAM,qBAAqB,eAAe,qBAAqB;CAE/D,MAAM,yBAAyB,OAA2B;EACtD,IAAI,cACA,2BAA2B,EAAE;OAE7B,oBAAoB,EAAE;CAE9B;CASA,MAAM,CAAC,aAAa,kBAAkB,SAAS,KAAK;CACpD,MAAM,CAAC,kBAAkB,uBAAuB,SAA4C,IAAI;CAEhG,MAAM,oBAAoB,OAA2B;EACjD,IAAI,CAAC,gBAAgB,eAAe,OAAO,oBAAoB;GAC3D,oBAAoB,EAAE,GAAG,CAAC;GAC1B;EACJ;EACA,sBAAsB,EAAE;CAC5B;CAGA,MAAM,cAAc,mBAAmB,iBAAiB,eAAe,CAAC;CAOxE,MAAM,CAAC,aAAa,kBAAkB,eAAe;EACjD,MAAM,QAAQ,iBAAiB,wCAAwC;EACvE,MAAM,SAAS,UAAU,OAAO,MAAM,WAAW,KAAK;EACtD,OAAO,OAAO,SAAS,MAAM,KAAK,SAAS,KAAK,SAAS,MAAM,SAAS;CAC5E,CAAC;CAED,gBAAgB;EACZ,kBAAkB,0CAA0C,YAAY,SAAS,CAAC;CACtF,GAAG,CAAC,WAAW,CAAC;CAEhB,OACI,qBAAC,OAAD;EAAK,WAAU;YAAf,CACI,oBAAC,iBAAD;GACI,aAAY;GACZ,kBAAkB;GAClB,mBAAmB;GACnB,gBAAgB;GAChB,YACI,qBAAC,OAAD;IAAK,WAAW,IAAI,yDAAyD,kBAAkB;cAA/F,CACI,qBAAC,OAAD;KAAK,WAAW,IAAI,qEAAqE,kBAAkB;eAA3G,CACI,oBAAC,YAAD;MAAY,SAAQ;MAAU,WAAU;gBAA4F;KAExH,CAAA,GACZ,oBAAC,SAAD;MAAS,OAAO,iBAAiB,WAAW,iBAAiB,kBAAkB,cAAc;gBACzF,oBAAC,OAAD,EAAA,UACI,oBAAC,YAAD;OACI,MAAK;OACL,cAAY;OACZ,UAAU,iBAAiB;OAC3B,eAAe,iBAAiB,KAAK;OACrC,WAAW,uBAAuB,QAAQ,wCAAwC;iBAElF,oBAAC,UAAD,EAAU,MAAM,SAAS,SAAU,CAAA;MAC3B,CAAA,EACX,CAAA;KACA,CAAA,CACR;QAEL,qBAAC,OAAD;KAAK,WAAU;eAAf,CACK,YAAY,WAAW,KACpB,oBAAC,OAAD;MAAK,WAAU;gBACX,oBAAC,YAAD;OAAY,SAAQ;OAAU,WAAU;iBAAyD;MAErF,CAAA;KACX,CAAA,GAER,YAAY,KAAK,eAAe;MAC7B,MAAM,gBAAgB,WAAW;MACjC,MAAM,aAAa,uBAAuB;MAC1C,OACI,qBAAC,OAAD;OAEI,eAAe,iBAAiB,aAAa;OAC7C,WAAW,IACP,+GACA,aACM,uFACA,4GACV;iBARJ,CAUI,oBAAC,aAAD;QAAa,kBAAkB;QAAY,MAAM;QAAY,WAAW,IACpE,aACM,yCACA,gDACV;OAAG,CAAA,GACH,oBAAC,QAAD;QAAM,WAAU;kBACX,WAAW,QAAQ,WAAW;OAC7B,CAAA,CACL;SAjBI,aAiBJ;KAEb,CAAC,CACA;MACJ;;GAET,aACI,oBAAC,OAAD;IAAK,WAAU;cAEV,qBACG,oBAAC,sBAAD;KAEsB;KAClB,cAAc;KACd,SAAS,oBAAoB;MAEzB,IAAI,uBAAuB,SAAS,iBAAiB,MACjD,sBAAsB,gBAAgB,IAAI;KAElD;KACA,eAAe;KAGf,gBAAgB,sBAAsB,KAAA,CAAS;KAC1B;KACA;KACM;KACE;KAChB;KACD;IACf,GAnBQ,kBAmBR,IAED,qBAAC,OAAD;KAAK,WAAU;eAAf,CACI,oBAAC,YAAD;MAAY,SAAQ;gBAAQ;KAEhB,CAAA,GACZ,qBAAC,QAAD;MACI,UAAU,iBAAiB;MAC3B,eAAe,sBAAsB,KAAK;gBAF9C,CAII,oBAAC,UAAD,CAAU,CAAA,GAAC,oBAEP;OACP;;GAER,CAAA;EAEZ,CAAA,GACD,oBAAC,sBAAD;GACI,MAAM,qBAAqB;GAC3B,MAAM;GACN,gBAAgB;IACZ,MAAM,OAAO;IACb,oBAAoB,IAAI;IACxB,eAAe,KAAK;IACpB,IAAI,MAAM,sBAAsB,KAAK,EAAE;GAC3C;GACA,oBAAoB,oBAAoB,IAAI;EAC/C,CAAA,CACA;;AAEb;;;;ACtNA,SAAgB,4BAA4B,OAAyC;CACjF,MAAM,WAAW,YAAY;CAC7B,MAAM,gBAAgB,iBAAiB;CACvC,MAAM,WAAW,YAAY;CAG7B,MAAM,WAAW,cAAc,gBAAgB,QAAQ;CAEvD,MAAM,qBADe,SAAS,SAAS,QAAQ,UAAU,EAAE,CAAC,CAAC,QAAQ,OAAO,EACjD,CAAA,CAAa,MAAM,GAAG,CAAC,CAAC,MAAM,KAAA;CAEzD,OACI,oBAAC,uBAAD;EACI,GAAI;EACgB;EACpB,2BAA2B,OAAO;GAC9B,IAAI,IACA,SAAS,cAAc,gBAAgB,UAAU,IAAI,CAAC;QAEtD,SAAS,cAAc,gBAAgB,QAAQ,CAAC;EAExD;CACH,CAAA;AAET"}
@@ -21,6 +21,7 @@ export { ImportSaveInProgress } from "../data_import/components/ImportSaveInProg
21
21
  export { useImportConfig } from "../data_import/hooks/useImportConfig.js";
22
22
  export { getInferenceType } from "../data_import/utils/get_import_inference_type.js";
23
23
  export { convertDataToEntity } from "../data_import/utils/data.js";
24
+ export { guessIdColumn } from "../data_import/utils/id_column.js";
24
25
  export type { ImportConfig } from "../data_import/types/index.js";
25
26
  export { DataNewPropertiesMapping } from "../data_import/components/DataNewPropertiesMapping.js";
26
27
  export { ImportNewPropertyFieldPreview } from "../data_import/components/ImportNewPropertyFieldPreview.js";
@@ -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>;
@@ -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;
@@ -1,7 +1,7 @@
1
- import { Sn as useUrlController } from "./util-qfCPxJzB.js";
2
- import { n as CollectionEditorDialog } from "./CollectionEditorDialog-B1mG1cfx.js";
3
- import { n as PropertyForm, r as PropertyFormDialog } from "./PropertyEditView-gKNUHxLY.js";
4
- import { i as CollectionStudioView, r as CollectionsStudioView, t as RouterCollectionsStudioView } from "./RouterCollectionsStudioView-qgDWC6E2.js";
1
+ import { Mn as useUrlController } from "./util-DybQBkug.js";
2
+ import { n as CollectionEditorDialog } from "./CollectionEditorDialog-DLHCka00.js";
3
+ import { n as PropertyForm, r as PropertyFormDialog } from "./PropertyEditView-_pu5hzZn.js";
4
+ import { i as CollectionStudioView, r as CollectionsStudioView, t as RouterCollectionsStudioView } from "./RouterCollectionsStudioView-ChPNl_uU.js";
5
5
  import { t as SchemaChangeDialog } from "./SchemaChangeDialog-BeP0A4MJ.js";
6
6
  import "react";
7
7
  import { useSnackbarController } from "@rebasepro/app";
@@ -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;
@@ -1,18 +1,21 @@
1
1
  import React from "react";
2
2
  import { EntityRelation, FilterValues, Relation } from "@rebasepro/types";
3
- import { CollectionSize, AdminCollection } from "@rebasepro/cms-types";
3
+ import { AdminCollection } from "@rebasepro/cms-types";
4
4
  type TableMultipleRelationFieldProps = {
5
5
  name: string;
6
6
  disabled: boolean;
7
7
  internalValue: EntityRelation[] | undefined | null;
8
8
  updateValue: (newValue: EntityRelation[] | null) => void;
9
- size: CollectionSize;
10
9
  previewProperties?: string[];
11
- title?: string;
12
10
  relation: Relation;
13
11
  fixedFilter?: FilterValues<string>;
14
12
  includeId?: boolean;
15
13
  includeEntityLink?: boolean;
14
+ /** The cell is selected. */
15
+ selected?: boolean;
16
+ /** See `TableRelationField`. */
17
+ open?: boolean;
18
+ onOpenChange?: (open: boolean) => void;
16
19
  };
17
20
  export declare function TableMultipleRelationField(props: TableMultipleRelationFieldProps): React.JSX.Element;
18
21
  export declare const TableMultipleRelationFieldInternal: React.MemoExoticComponent<(props: TableMultipleRelationFieldProps & {
@@ -9,11 +9,18 @@ type TableReferenceFieldProps = {
9
9
  size: CollectionSize;
10
10
  multiselect: boolean;
11
11
  previewProperties?: string[];
12
- title?: string;
13
12
  path: string;
14
13
  fixedFilter?: FilterValues<string>;
15
14
  includeId?: boolean;
16
15
  includeEntityLink?: boolean;
16
+ /** The cell is selected. */
17
+ selected?: boolean;
18
+ /**
19
+ * Set by the cell's opener: opens the selection dialog, and is handed
20
+ * back as closed at once — the dialog keeps its own state from there.
21
+ */
22
+ open?: boolean;
23
+ onOpenChange?: (open: boolean) => void;
17
24
  };
18
25
  export declare function TableReferenceField(props: TableReferenceFieldProps): React.JSX.Element;
19
26
  export declare const TableReferenceFieldInternal: React.MemoExoticComponent<(props: TableReferenceFieldProps & {
@@ -9,11 +9,18 @@ type TableRelationFieldProps = {
9
9
  size: CollectionSize;
10
10
  multiselect: boolean;
11
11
  previewProperties?: string[];
12
- title?: string;
13
12
  relation: Relation;
14
13
  fixedFilter?: FilterValues<string>;
15
14
  includeId?: boolean;
16
15
  includeEntityLink?: boolean;
16
+ /** The cell is selected. */
17
+ selected?: boolean;
18
+ /**
19
+ * Set by the cell's opener: opens the selection dialog, and is handed
20
+ * back as closed at once — the dialog keeps its own state from there.
21
+ */
22
+ open?: boolean;
23
+ onOpenChange?: (open: boolean) => void;
17
24
  };
18
25
  export declare function TableRelationField(props: TableRelationFieldProps): React.JSX.Element;
19
26
  export declare const TableRelationFieldInternal: React.MemoExoticComponent<(props: TableRelationFieldProps & {
@@ -13,9 +13,19 @@ interface RelationSelectorFieldProps {
13
13
  relation: Relation;
14
14
  /** Force filter to be applied to the relation search */
15
15
  fixedFilter?: FilterValues<string>;
16
- /** Collection size for display */
17
- size?: "compact" | "small" | "medium";
16
+ /** The cell is selected: the trigger takes the focus, so Enter opens the list. */
17
+ focused?: boolean;
18
+ /** Whether the list is open. Set by the cell's opener as well as the trigger. */
19
+ open?: boolean;
20
+ onOpenChange?: (open: boolean) => void;
21
+ /**
22
+ * What the cell shows at rest. The trigger shows the same node, so a
23
+ * selected cell looks like an unselected one.
24
+ */
25
+ preview?: React.ReactNode;
26
+ /** The cell: the list opens against it rather than against the trigger. */
27
+ anchorRef?: React.RefObject<HTMLElement | null>;
18
28
  }
19
29
  /** Thin wrapper around RelationSelector for table cells */
20
- export declare function TableRelationSelectorField({ disabled, internalValue, updateValue, relation, fixedFilter, size }: RelationSelectorFieldProps): React.JSX.Element;
30
+ export declare function TableRelationSelectorField({ disabled, internalValue, updateValue, relation, fixedFilter, focused, open, onOpenChange, preview, anchorRef }: RelationSelectorFieldProps): React.JSX.Element;
21
31
  export {};
@@ -11,5 +11,15 @@ export declare function VirtualTableSelect(props: {
11
11
  valueType: "string" | "number";
12
12
  updateValue: (newValue: (string | number | string[] | number[] | null)) => void;
13
13
  focused: boolean;
14
+ /** Whether the list is open. Set by the cell's opener as well as the trigger. */
15
+ open?: boolean;
16
+ onOpenChange?: (open: boolean) => void;
17
+ /**
18
+ * What the cell shows at rest. The trigger shows the same node, so a
19
+ * selected cell looks like an unselected one; the chevron is the cell's.
20
+ */
21
+ preview?: React.ReactNode;
22
+ /** The cell: the list opens against it rather than against the trigger. */
23
+ anchorRef?: React.RefObject<HTMLElement | null>;
14
24
  onBlur?: React.FocusEventHandler<HTMLInputElement | HTMLTextAreaElement>;
15
25
  }): React.JSX.Element;
@@ -8,5 +8,15 @@ export declare function VirtualTableUserSelect(props: {
8
8
  internalValue: string | string[] | undefined;
9
9
  updateValue: (newValue: (string | string[] | null)) => void;
10
10
  focused: boolean;
11
+ /** Whether the list is open. Set by the cell's opener as well as the trigger. */
12
+ open?: boolean;
13
+ onOpenChange?: (open: boolean) => void;
14
+ /**
15
+ * What the cell shows at rest. The trigger shows the same node, so a
16
+ * selected cell looks like an unselected one.
17
+ */
18
+ preview?: React.ReactNode;
19
+ /** The cell: the list opens against it rather than against the trigger. */
20
+ anchorRef?: React.RefObject<HTMLElement | null>;
11
21
  onBlur?: React.FocusEventHandler<HTMLInputElement | HTMLTextAreaElement>;
12
22
  }): React.JSX.Element;
@@ -1,5 +1,18 @@
1
1
  import React from "react";
2
2
  import { TableSize } from "@rebasepro/app";
3
+ import { TableCellOpenerKind } from "./TableCellOpener.js";
4
+ export interface EntityTableCellOpener {
5
+ kind: TableCellOpenerKind;
6
+ open: boolean;
7
+ /** See {@link TableCellOpener}'s `focusOnSelect`. */
8
+ focusOnSelect: boolean;
9
+ label: string;
10
+ /**
11
+ * Whether the editor should now be open, and the cell's rect for editors
12
+ * positioned against it. The cell is selected before this is called.
13
+ */
14
+ onToggle: (open: boolean, cellRect: DOMRect | undefined) => void;
15
+ }
3
16
  interface EntityTableCellProps {
4
17
  children: React.ReactNode;
5
18
  actions?: React.ReactNode;
@@ -20,6 +33,14 @@ interface EntityTableCellProps {
20
33
  fullHeight?: boolean;
21
34
  selected?: boolean;
22
35
  hideOverflow?: boolean;
36
+ /**
37
+ * The control that opens the cell's editor, revealed on hover and kept
38
+ * shown while the cell is selected. The cell keeps a slot for it at its
39
+ * trailing edge either way, so it never moves the value.
40
+ */
41
+ opener?: EntityTableCellOpener;
42
+ /** The cell's element, for editors that float and are positioned against it. */
43
+ cellRef?: React.RefObject<HTMLDivElement | null>;
23
44
  onSelect?: (cellRect: DOMRect | undefined) => void;
24
45
  sortableNodeRef?: (node: HTMLElement | null) => void;
25
46
  sortableStyle?: React.CSSProperties;
@@ -0,0 +1,45 @@
1
+ import React from "react";
2
+ /**
3
+ * What a cell's opener opens.
4
+ *
5
+ * - `dropdown`: a list anchored to the cell — an enum, a user, an inline
6
+ * relation picker.
7
+ * - `dialog`: the selection dialog of a reference, or of a relation drawn with
8
+ * the dialog widget. Same chevron: to the person editing, both change which
9
+ * record the cell points at.
10
+ * - `calendar`: the date picker.
11
+ * - `expand`: the popup form, for values with no editor of their own in a
12
+ * cell (maps, arrays, custom fields).
13
+ */
14
+ export type TableCellOpenerKind = "dropdown" | "dialog" | "calendar" | "expand";
15
+ export interface TableCellOpenerProps {
16
+ kind: TableCellOpenerKind;
17
+ /** The editor it opens is open: the chevron turns over and stays shown. */
18
+ open: boolean;
19
+ /** The cell is selected: the opener is shown whether or not it is hovered. */
20
+ selected: boolean;
21
+ /**
22
+ * Take the focus when the cell is selected, so Enter opens the editor.
23
+ * Off when the editor's own trigger takes it instead.
24
+ */
25
+ focusOnSelect: boolean;
26
+ label: string;
27
+ /**
28
+ * Called on a click, with whether the editor should now be open. A click
29
+ * while it is open closes it rather than opening it a second time.
30
+ */
31
+ onToggle: (open: boolean) => void;
32
+ }
33
+ /**
34
+ * The one control a cell reveals to open its editor.
35
+ *
36
+ * A cell looks the same at rest, hovered and selected: the value, in the same
37
+ * place. This is the only thing that appears — on hover, and for as long as
38
+ * the cell is selected — in a slot the cell keeps for it whether it is shown
39
+ * or not, so revealing it moves nothing.
40
+ *
41
+ * One click on it opens the editor, from rest: it selects the cell on the way.
42
+ * It never takes the focus on a mouse press, because the cell selects itself
43
+ * on focus and doing so first would re-render the cell under the pointer.
44
+ */
45
+ export declare function TableCellOpener({ kind, open, selected, focusOnSelect, label, onToggle }: TableCellOpenerProps): React.JSX.Element;
@@ -1,6 +1,7 @@
1
1
  import React from "react";
2
2
  import { Entity, Property } from "@rebasepro/types";
3
3
  import { TableSize } from "@rebasepro/app";
4
+ import { TableCellOpenerKind } from "./internal/TableCellOpener.js";
4
5
  export interface TableFieldBindingProps<T = unknown> {
5
6
  propertyKey: string;
6
7
  property: Property;
@@ -15,9 +16,32 @@ export interface TableFieldBindingProps<T = unknown> {
15
16
  entity: Entity<Record<string, unknown>>;
16
17
  path: string;
17
18
  openPopup?: (cellRect: DOMRect | undefined) => void;
19
+ /**
20
+ * Whether the cell's floating editor is open — its dropdown, its selection
21
+ * dialog, its date picker. The cell's opener sets it as well as the
22
+ * editor's own trigger, and it can be set before the editor has mounted.
23
+ */
24
+ open?: boolean;
25
+ onOpenChange?: (open: boolean) => void;
26
+ /**
27
+ * What the cell shows at rest. An editor with a trigger shows this same
28
+ * node in it, so selecting the cell changes its frame and nothing else.
29
+ */
30
+ preview?: React.ReactNode;
31
+ /**
32
+ * The cell's element. A floating editor opens against it rather than
33
+ * against its own trigger, which can be taller than the cell clipping it.
34
+ */
35
+ anchorRef?: React.RefObject<HTMLElement | null>;
18
36
  }
19
37
  export interface TableFieldConfig {
20
38
  Component: React.ComponentType<TableFieldBindingProps>;
39
+ /**
40
+ * The opener the cell reveals on hover, and keeps shown while selected,
41
+ * to open this editor in one click. None for editors that are the value
42
+ * itself (a text input, a switch) or that draw their own (storage).
43
+ */
44
+ opener?: TableCellOpenerKind;
21
45
  fullHeight?: boolean;
22
46
  allowScroll?: boolean;
23
47
  removePadding?: boolean;
@@ -41,7 +41,7 @@ export type CollectionViewBindingProps<M extends Record<string, unknown>> = {
41
41
  selectedTab?: string;
42
42
  } & AdminCollection<M>;
43
43
  export declare const CollectionViewBinding: React.FunctionComponent<CollectionViewBindingProps<any>>;
44
- export declare function EntitiesCount({ path, collection, filter, sortBy, searchString, onCountChange }: {
44
+ export declare function EntitiesCount({ path, collection, filter, sortBy, searchString, onCountChange, refreshKey }: {
45
45
  path: string;
46
46
  collection: AdminCollection;
47
47
  filter?: FilterValues<any>;
@@ -55,4 +55,10 @@ export declare function EntitiesCount({ path, collection, filter, sortBy, search
55
55
  */
56
56
  searchString: string | undefined;
57
57
  onCountChange?: (count: number | null | undefined) => void;
58
+ /**
59
+ * Counts again whenever this changes. The count is one read, not a
60
+ * subscription: after a delete the toolbar kept the old total, and "select
61
+ * all matching" offered it over the rows that were left.
62
+ */
63
+ refreshKey?: number;
58
64
  }): null;
@@ -1,6 +1,7 @@
1
1
  import * as React from "react";
2
2
  import { Entity } from "@rebasepro/types";
3
3
  import type { PreviewSize } from "../types/components/PropertyPreviewProps.js";
4
+ import { EntityPreviewLayout } from "./EntityPreviewNesting.js";
4
5
  import type { AdminCollection } from "@rebasepro/cms-types";
5
6
  export type EntityPreviewBindingProps = {
6
7
  size?: PreviewSize;
@@ -28,12 +29,18 @@ export type EntityPreviewBindingDataProps = {
28
29
  includeTitle?: boolean;
29
30
  includeEntityLink?: boolean;
30
31
  includeImage?: boolean;
32
+ /**
33
+ * `row` draws the record as a line of a table row instead of a card: see
34
+ * {@link EntityPreviewLayout}. No id, no open button — the title opens the
35
+ * record — and only as many supporting lines as the row has room for.
36
+ */
37
+ layout?: EntityPreviewLayout;
31
38
  };
32
39
  /**
33
40
  * This component contains the main logic and content for displaying a entity preview,
34
41
  * without any container wrapper. Used internally by EntityPreviewBinding.
35
42
  */
36
- export declare function EntityPreviewBindingData({ actions, collection: collectionProp, previewKeys, size, includeId, onSidePanelClick, includeTitle, includeEntityLink, includeImage, entity }: EntityPreviewBindingDataProps): React.JSX.Element;
43
+ export declare function EntityPreviewBindingData({ actions, collection: collectionProp, previewKeys, size, includeId, onSidePanelClick, includeTitle, includeEntityLink, includeImage, layout, entity }: EntityPreviewBindingDataProps): React.JSX.Element;
37
44
  export declare function EntityPreviewBindingWithId({ entityId, path, ...props }: Omit<EntityPreviewBindingProps, "entity"> & {
38
45
  entityId: string | number;
39
46
  path: string;
@@ -23,3 +23,18 @@ export declare function NestedEntityPreviewBoundary({ children }: {
23
23
  export declare function RootEntityPreviewBoundary({ children }: {
24
24
  children: React.ReactNode;
25
25
  }): React.JSX.Element;
26
+ /**
27
+ * How a top-level reference or relation preview is laid out.
28
+ *
29
+ * `card` is the form's: a bordered box with the record's id, title, preview
30
+ * properties and a button to open it. `row` is a table cell's: the same
31
+ * record as a line of the row it sits in — image, a title that opens it, one
32
+ * supporting line — with no box of its own, because the cell is the box and
33
+ * a card is taller than a row.
34
+ */
35
+ export type EntityPreviewLayout = "card" | "row";
36
+ export declare function useEntityPreviewLayout(): EntityPreviewLayout;
37
+ /** Lays out the previews below it as rows. See {@link EntityPreviewLayout}. */
38
+ export declare function RowEntityPreviewLayout({ children }: {
39
+ children: React.ReactNode;
40
+ }): React.JSX.Element;
@@ -0,0 +1,17 @@
1
+ import * as React from "react";
2
+ /**
3
+ * The trigger of a picker's popover, or — given an anchor — a plain child
4
+ * beside a `Popover.Anchor` on that element.
5
+ *
6
+ * Both at once does not work: Radix's `Trigger` registers itself as the
7
+ * anchor through a ref callback, and a custom anchor through an effect that
8
+ * only reports a change. Under StrictMode the ref callback runs again after
9
+ * the effect, the effect sees nothing new, and the popover stays anchored to
10
+ * a trigger that the custom anchor then unmounts — it opened at the window's
11
+ * top-left corner. The pickers open and close themselves, so they lose
12
+ * nothing by leaving the `Trigger` out.
13
+ */
14
+ export declare function PickerTrigger({ anchorRef, children }: {
15
+ anchorRef?: React.RefObject<HTMLElement | null>;
16
+ children: React.ReactElement;
17
+ }): React.JSX.Element;
@@ -38,6 +38,18 @@ export interface SelectionProps<M extends Record<string, unknown>> {
38
38
  * @callback
39
39
  */
40
40
  onMultipleEntitiesSelected?(entities: Entity<any>[]): void;
41
+ /**
42
+ * Called once, with the final selection, when the user presses Done.
43
+ *
44
+ * `onMultipleEntitiesSelected` reports every tick as it happens, which is
45
+ * right when the selection *is* the value (a reference field). Use this
46
+ * one instead when acting on the selection writes something — linking
47
+ * rows, say — so a row ticked and then unticked, or a selection cleared,
48
+ * is never acted on.
49
+ * @param entities
50
+ * @callback
51
+ */
52
+ onSelectionDone?(entities: Entity<M>[]): void;
41
53
  /**
42
54
  * Allow selection of entities that pass the given filter only.
43
55
  */