@rebasepro/admin 0.9.1-canary.ff338b5 → 0.10.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 (92) hide show
  1. package/dist/{CollectionEditorDialog-DbPpDDwb.js → CollectionEditorDialog-BOlKGGg7.js} +123 -31
  2. package/dist/{CollectionEditorDialog-DbPpDDwb.js.map → CollectionEditorDialog-BOlKGGg7.js.map} +1 -1
  3. package/dist/{PropertyEditView-DfJbZ67t.js → PropertyEditView-i51tFOfM.js} +3 -3
  4. package/dist/PropertyEditView-i51tFOfM.js.map +1 -0
  5. package/dist/{RouterCollectionsStudioView-C6XQ7Il5.js → RouterCollectionsStudioView-Dh4W53PU.js} +4 -4
  6. package/dist/{RouterCollectionsStudioView-C6XQ7Il5.js.map → RouterCollectionsStudioView-Dh4W53PU.js.map} +1 -1
  7. package/dist/collection_editor_ui.js +4 -4
  8. package/dist/components/field_configs.d.ts +17 -0
  9. package/dist/contexts/BreacrumbsContext.d.ts +1 -1
  10. package/dist/editor.js.map +1 -1
  11. package/dist/export-BM6HPujv.js +518 -0
  12. package/dist/export-BM6HPujv.js.map +1 -0
  13. package/dist/{history-GSa4BgDt.js → history-CtIPJxvQ.js} +3 -3
  14. package/dist/history-CtIPJxvQ.js.map +1 -0
  15. package/dist/hooks/useSelectionDialog.d.ts +1 -1
  16. package/dist/import-DzQnT03v.js +328 -0
  17. package/dist/import-DzQnT03v.js.map +1 -0
  18. package/dist/index.js +3299 -1
  19. package/dist/index.js.map +1 -0
  20. package/dist/preview/components/UrlComponentPreview.d.ts +2 -1
  21. package/dist/preview/util.d.ts +1 -0
  22. package/dist/util/view_mode.d.ts +54 -0
  23. package/dist/{src-B8WuGSPZ.js → util-XFwQ9FEc.js} +17388 -21329
  24. package/dist/util-XFwQ9FEc.js.map +1 -0
  25. package/package.json +8 -8
  26. package/src/collection_editor/ui/collection_editor/CollectionRLSTab.tsx +104 -9
  27. package/src/collection_editor/ui/collection_editor/properties/CommonPropertyFields.tsx +1 -1
  28. package/src/collection_editor/ui/collection_editor/properties/EnumPropertyField.tsx +1 -1
  29. package/src/components/CollectionTableBinding/CollectionRowActions.tsx +1 -1
  30. package/src/components/CollectionTableBinding/fields/TableReferenceField.tsx +1 -1
  31. package/src/components/CollectionViewBinding/CollectionBoardViewBinding.tsx +1 -1
  32. package/src/components/CollectionViewBinding/CollectionViewActions.tsx +1 -1
  33. package/src/components/CollectionViewBinding/CollectionViewBinding.tsx +76 -126
  34. package/src/components/CollectionViewBinding/CollectionViewStartActions.tsx +4 -8
  35. package/src/components/CollectionViewBinding/SplitListView.tsx +114 -63
  36. package/src/components/DetailViewBinding.tsx +4 -2
  37. package/src/components/EditFormActions.tsx +2 -2
  38. package/src/components/EditViewBinding.tsx +3 -2
  39. package/src/components/EntityPreviewBinding.tsx +2 -1
  40. package/src/components/HomePage/ContentHomePage.tsx +2 -1
  41. package/src/components/HomePage/FavouritesView.tsx +1 -1
  42. package/src/components/RebaseRouteDefs.tsx +1 -1
  43. package/src/components/ReferenceTable/SelectionTableBinding.tsx +2 -1
  44. package/src/components/ReferenceWidget.tsx +1 -1
  45. package/src/components/SelectableTable/filters/ReferenceFilterField.tsx +1 -1
  46. package/src/components/SidePanelBinding.tsx +46 -8
  47. package/src/components/SidePanelProvider.tsx +14 -11
  48. package/src/components/field_configs.tsx +41 -24
  49. package/src/components/history/EntityHistoryView.tsx +1 -1
  50. package/src/contexts/BreacrumbsContext.tsx +1 -1
  51. package/src/data_import/components/ImportNewPropertyFieldPreview.tsx +2 -4
  52. package/src/data_import/import/ImportCollectionAction.tsx +6 -6
  53. package/src/editor/selectors/color-selector.tsx +1 -1
  54. package/src/editor/selectors/node-selector.tsx +1 -1
  55. package/src/editor/selectors/text-buttons.tsx +1 -1
  56. package/src/form/EntityForm.tsx +4 -1
  57. package/src/form/EntityFormBinding.tsx +3 -1
  58. package/src/form/field_bindings/ArrayCustomShapedFieldBinding.tsx +2 -1
  59. package/src/form/field_bindings/ArrayOfReferencesFieldBinding.tsx +3 -2
  60. package/src/form/field_bindings/BlockFieldBinding.tsx +2 -1
  61. package/src/form/field_bindings/DateTimeFieldBinding.tsx +2 -1
  62. package/src/form/field_bindings/KeyValueFieldBinding.tsx +2 -1
  63. package/src/form/field_bindings/MapFieldBinding.tsx +2 -1
  64. package/src/form/field_bindings/MarkdownEditorFieldBinding.tsx +2 -1
  65. package/src/form/field_bindings/MultiSelectFieldBinding.tsx +2 -1
  66. package/src/form/field_bindings/MultipleRelationFieldBinding.tsx +2 -1
  67. package/src/form/field_bindings/ReadOnlyFieldBinding.tsx +2 -1
  68. package/src/form/field_bindings/ReferenceFieldBinding.tsx +3 -2
  69. package/src/form/field_bindings/RelationFieldBinding.tsx +2 -1
  70. package/src/form/field_bindings/RepeatFieldBinding.tsx +2 -1
  71. package/src/form/field_bindings/SelectFieldBinding.tsx +2 -1
  72. package/src/form/field_bindings/StorageUploadFieldBinding.tsx +2 -1
  73. package/src/form/field_bindings/SwitchFieldBinding.tsx +2 -1
  74. package/src/form/field_bindings/TextFieldBinding.tsx +2 -1
  75. package/src/form/field_bindings/UserSelectFieldBinding.tsx +2 -1
  76. package/src/form/field_bindings/VectorFieldBinding.tsx +2 -1
  77. package/src/hooks/useBuildSidePanel.tsx +8 -0
  78. package/src/hooks/useSelectionDialog.tsx +3 -3
  79. package/src/preview/components/ImagePreview.tsx +1 -1
  80. package/src/preview/components/ReferencePreview.tsx +1 -1
  81. package/src/preview/components/RelationPreview.tsx +2 -2
  82. package/src/preview/components/UrlComponentPreview.tsx +2 -9
  83. package/src/preview/property_previews/ArrayOfStringsPreview.tsx +1 -1
  84. package/src/preview/property_previews/NumberPropertyPreview.tsx +1 -1
  85. package/src/preview/util.ts +9 -0
  86. package/src/routes/CustomViewRoute.tsx +2 -1
  87. package/src/routes/RebaseRoute.tsx +20 -25
  88. package/src/util/navigation_utils.ts +2 -5
  89. package/src/util/view_mode.ts +102 -0
  90. package/dist/PropertyEditView-DfJbZ67t.js.map +0 -1
  91. package/dist/history-GSa4BgDt.js.map +0 -1
  92. package/dist/src-B8WuGSPZ.js.map +0 -1
@@ -8,11 +8,12 @@ import {
8
8
  defaultBorderMixin,
9
9
  ResizablePanels
10
10
  } from "@rebasepro/ui";
11
- import { useLargeLayout } from "@rebasepro/app";
12
- import { useCollectionRegistryController } from "../../index";
11
+ import { useLargeLayout, UnsavedChangesDialog, useNavigationBlocker } from "@rebasepro/app";
12
+ import { useCollectionRegistryController } from "../../hooks/navigation/contexts/CollectionRegistryContext";
13
13
  import { useNavigate, useLocation } from "react-router-dom";
14
- import { useUrlController } from "../../index";
14
+ import { useUrlController } from "../../hooks/navigation/contexts/UrlContext";
15
15
  import { ErrorBoundary } from "@rebasepro/ui";
16
+ import { withViewMode } from "../../util/view_mode";
16
17
 
17
18
  export type SplitListViewProps<M extends Record<string, unknown> = Record<string, unknown>> = {
18
19
  collection: CollectionConfig<M>;
@@ -55,6 +56,27 @@ export type SplitListViewProps<M extends Record<string, unknown> = Record<string
55
56
 
56
57
  const PANEL_SIZE_KEY = "rebase_split_list_panel_size";
57
58
 
59
+ /**
60
+ * Whether the element accepts text, and therefore owns the keystroke.
61
+ * Covers native fields plus the ARIA roles custom editors expose.
62
+ */
63
+ function isTextEntryTarget(target: HTMLElement | null): boolean {
64
+ if (!target) return false;
65
+ const tag = target.tagName;
66
+ if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return true;
67
+ if (target.isContentEditable) return true;
68
+ return Boolean(target.closest('[contenteditable="true"], [role="textbox"], [role="combobox"], [role="searchbox"]'));
69
+ }
70
+
71
+ /**
72
+ * Whether a bare letter shortcut may act on this target. Only the list side of
73
+ * the split owns them — inside the detail panel a keystroke belongs to the form.
74
+ */
75
+ function isListShortcutTarget(target: HTMLElement | null, detailPanel: HTMLElement | null): boolean {
76
+ if (!target || target === document.body) return true;
77
+ return !detailPanel?.contains(target);
78
+ }
79
+
58
80
  function getSavedPanelSize(path: string): number {
59
81
  try {
60
82
  const saved = localStorage.getItem(`${PANEL_SIZE_KEY}_${path}`);
@@ -115,6 +137,37 @@ export function SplitListView<M extends Record<string, unknown> = Record<string,
115
137
 
116
138
  const showDetail = selectedEntityId !== undefined;
117
139
 
140
+ // ── Unsaved changes ──
141
+ // The detail form lives inside the split layout, so every way out of it
142
+ // (Escape, j/k, clicking another row, the browser back button) is a plain
143
+ // router navigation. A ref — not state — because the blocker predicate must
144
+ // observe the change synchronously, before the navigation it is guarding.
145
+ const dirty = useRef(false);
146
+ const detailPanelRef = useRef<HTMLDivElement>(null);
147
+ const setDirty = useCallback((modified: boolean) => {
148
+ dirty.current = modified;
149
+ }, []);
150
+
151
+ // The form remounts per entity, so its dirty state must not leak into the next one.
152
+ useEffect(() => {
153
+ dirty.current = false;
154
+ }, [selectedEntityId]);
155
+
156
+ const blocker = useNavigationBlocker(useCallback(({
157
+ currentLocation,
158
+ nextLocation
159
+ }) => {
160
+ if (!dirty.current) return false;
161
+ if (currentLocation.pathname === nextLocation.pathname) return false;
162
+ // Side panel overlays (e.g. following a relation) render above the form
163
+ // and leave it mounted — nothing is lost in either direction.
164
+ const nextHash = nextLocation.hash;
165
+ if (nextHash === "#side" || nextHash === "#new_side") return false;
166
+ const currentHash = currentLocation.hash;
167
+ if (currentHash === "#side" || currentHash === "#new_side") return false;
168
+ return true;
169
+ }, []));
170
+
118
171
  // Panel size state with persistence
119
172
  const [panelSize, setPanelSize] = useState(() => getSavedPanelSize(path));
120
173
 
@@ -170,16 +223,15 @@ export function SplitListView<M extends Record<string, unknown> = Record<string,
170
223
  };
171
224
  }, [selectedEntityId]); // Intentionally only depend on selectedEntityId
172
225
 
226
+ // Build a URL below this collection, preserving the active view mode.
227
+ const buildUrl = useCallback((subPath: string) => {
228
+ return withViewMode(urlController.buildUrlCollectionPath(subPath));
229
+ }, [urlController]);
230
+
173
231
  // Close the detail panel: navigate back to the collection path
174
232
  const handleCloseDetail = useCallback(() => {
175
- let collectionUrl = urlController.buildUrlCollectionPath(path);
176
- // Preserve the __view query param so the view mode is retained
177
- const currentViewParam = new URLSearchParams(window.location.search).get("__view");
178
- if (currentViewParam) {
179
- collectionUrl += `${collectionUrl.includes("?") ? "&" : "?"}__view=${currentViewParam}`;
180
- }
181
- navigate(collectionUrl);
182
- }, [navigate, urlController, path]);
233
+ navigate(buildUrl(path));
234
+ }, [navigate, buildUrl, path]);
183
235
 
184
236
  // ── Keyboard navigation ──
185
237
  const entityIds = useMemo(
@@ -189,23 +241,33 @@ export function SplitListView<M extends Record<string, unknown> = Record<string,
189
241
 
190
242
  useEffect(() => {
191
243
  const handleKeyDown = (e: KeyboardEvent) => {
192
- // Don't intercept when user is typing in an input/textarea
193
- const tag = (e.target as HTMLElement)?.tagName;
194
- if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT" || (e.target as HTMLElement)?.isContentEditable) {
195
- // Only intercept Escape when inside the detail panel
196
- if (e.key === "Escape" && selectedEntityId) {
197
- handleCloseDetail();
244
+ const target = e.target as HTMLElement | null;
245
+
246
+ // Radix overlays own the keyboard while they are open including Escape.
247
+ if (document.querySelector('[role="dialog"][data-state="open"]')) return;
248
+
249
+ if (e.key === "Escape") {
250
+ if (!selectedEntityId) return;
251
+ // First Escape leaves the field (which may also be dismissing its
252
+ // own dropdown), a second one closes the panel. Closing straight
253
+ // from a focused field would discard an in-progress edit.
254
+ if (isTextEntryTarget(target)) {
255
+ target?.blur();
198
256
  e.preventDefault();
257
+ return;
199
258
  }
200
- return;
201
- }
202
-
203
- if (e.key === "Escape" && selectedEntityId) {
204
259
  handleCloseDetail();
205
260
  e.preventDefault();
206
261
  return;
207
262
  }
208
263
 
264
+ // Record navigation only belongs to the list side. Inside the detail
265
+ // panel these keys are the form's — arrows move between fields, and a
266
+ // bare letter is input for widgets that aren't native fields (rich
267
+ // text, code editors, comboboxes).
268
+ if (!isListShortcutTarget(target, detailPanelRef.current)) return;
269
+ if (isTextEntryTarget(target)) return;
270
+
209
271
  // Arrow down / j → next entity
210
272
  if ((e.key === "ArrowDown" || e.key === "j") && !e.metaKey && !e.ctrlKey) {
211
273
  e.preventDefault();
@@ -282,6 +344,7 @@ export function SplitListView<M extends Record<string, unknown> = Record<string,
282
344
 
283
345
  const detailPanel = renderedEntityId !== undefined ? (
284
346
  <div
347
+ ref={detailPanelRef}
285
348
  className={cls(
286
349
  "flex-1 flex flex-col min-w-0 h-full transition-all ease-out w-full",
287
350
  animationPhase === "entered"
@@ -302,25 +365,15 @@ export function SplitListView<M extends Record<string, unknown> = Record<string,
302
365
  selectedTab={selectedTab}
303
366
  layout="split"
304
367
  onEditClick={() => {
305
- let entityUrl = urlController.buildUrlCollectionPath(`${path}/${renderedEntityId}/edit`);
306
- const currentViewParam = new URLSearchParams(window.location.search).get("__view");
307
- if (currentViewParam) {
308
- entityUrl += `${entityUrl.includes("?") ? "&" : "?"}__view=${currentViewParam}`;
309
- }
310
- navigate(entityUrl);
368
+ navigate(buildUrl(`${path}/${renderedEntityId}/edit`));
311
369
  }}
312
370
  onTabChange={(params) => {
313
371
  const newSelectedTab = params.selectedTab;
314
- let entityUrl = urlController.buildUrlCollectionPath(
372
+ navigate(buildUrl(
315
373
  newSelectedTab
316
374
  ? `${path}/${renderedEntityId}/${newSelectedTab}`
317
375
  : `${path}/${renderedEntityId}`
318
- );
319
- const currentViewParam = new URLSearchParams(window.location.search).get("__view");
320
- if (currentViewParam) {
321
- entityUrl += `${entityUrl.includes("?") ? "&" : "?"}__view=${currentViewParam}`;
322
- }
323
- navigate(entityUrl);
376
+ ));
324
377
  }}
325
378
  />
326
379
  : <EditViewBinding
@@ -332,34 +385,21 @@ export function SplitListView<M extends Record<string, unknown> = Record<string,
332
385
  parentEntityIds={usedParentEntityIds}
333
386
  selectedTab={selectedTab}
334
387
  layout="split"
335
- onSaved={(params) => {
336
- let entityUrl = urlController.buildUrlCollectionPath(`${path}/${renderedEntityId}`);
337
- const currentViewParam = new URLSearchParams(window.location.search).get("__view");
338
- if (currentViewParam) {
339
- entityUrl += `${entityUrl.includes("?") ? "&" : "?"}__view=${currentViewParam}`;
340
- }
341
- navigate(entityUrl, { replace: true });
388
+ onValuesModified={setDirty}
389
+ onSaved={() => {
390
+ setDirty(false);
391
+ navigate(buildUrl(`${path}/${renderedEntityId}`), { replace: true });
342
392
  }}
343
393
  navigateBack={() => {
344
- let entityUrl = urlController.buildUrlCollectionPath(`${path}/${renderedEntityId}`);
345
- const currentViewParam = new URLSearchParams(window.location.search).get("__view");
346
- if (currentViewParam) {
347
- entityUrl += `${entityUrl.includes("?") ? "&" : "?"}__view=${currentViewParam}`;
348
- }
349
- navigate(entityUrl);
394
+ navigate(buildUrl(`${path}/${renderedEntityId}`));
350
395
  }}
351
396
  onTabChange={(params) => {
352
397
  const newSelectedTab = params.selectedTab;
353
- let entityUrl = urlController.buildUrlCollectionPath(
398
+ navigate(buildUrl(
354
399
  newSelectedTab
355
400
  ? `${path}/${renderedEntityId}/${newSelectedTab}`
356
401
  : `${path}/${renderedEntityId}`
357
- );
358
- const currentViewParam = new URLSearchParams(window.location.search).get("__view");
359
- if (currentViewParam) {
360
- entityUrl += `${entityUrl.includes("?") ? "&" : "?"}__view=${currentViewParam}`;
361
- }
362
- navigate(entityUrl);
402
+ ));
363
403
  }}
364
404
  />
365
405
  }
@@ -368,14 +408,25 @@ export function SplitListView<M extends Record<string, unknown> = Record<string,
368
408
  ) : <></>;
369
409
 
370
410
  return (
371
- <ResizablePanels
372
- stacked={!largeLayout}
373
- firstPanel={listPanel}
374
- secondPanel={detailPanel}
375
- showSecondPanel={isDetailVisible}
376
- panelSizePercent={animationPhase === "entering" ? 100 : panelSize}
377
- onPanelSizeChange={setPanelSize}
378
- minPanelSizePx={240}
379
- />
411
+ <>
412
+ <ResizablePanels
413
+ stacked={!largeLayout}
414
+ firstPanel={listPanel}
415
+ secondPanel={detailPanel}
416
+ showSecondPanel={isDetailVisible}
417
+ panelSizePercent={animationPhase === "entering" ? 100 : panelSize}
418
+ onPanelSizeChange={setPanelSize}
419
+ minPanelSizePx={240}
420
+ />
421
+
422
+ <UnsavedChangesDialog
423
+ open={blocker?.state === "blocked"}
424
+ handleOk={() => {
425
+ dirty.current = false;
426
+ blocker?.proceed?.();
427
+ }}
428
+ handleCancel={() => blocker?.reset?.()}
429
+ body={<>You have unsaved changes in this <b>{collection.singularName ?? collection.name}</b>.</>}/>
430
+ </>
380
431
  );
381
432
  }
@@ -5,7 +5,8 @@ import React, { lazy, Suspense, useCallback, useEffect, useMemo, useRef, useStat
5
5
  import { Entity, EntityStatus, getCollectionDataPath, Property } from "@rebasepro/types";
6
6
  import { PluginProviderStack, resolveComponentRef, useComponentOverride, CollectionScopeProvider } from "@rebasepro/app";
7
7
 
8
- import { CollectionViewBinding, EntityViewBinding } from "../components";
8
+ import { CollectionViewBinding } from "./CollectionViewBinding/CollectionViewBinding";
9
+ import { EntityViewBinding } from "./EntityViewBinding";
9
10
  import { CircularProgressCenter, iconSize } from "@rebasepro/ui";
10
11
  import {
11
12
  Alert,
@@ -39,7 +40,8 @@ import {
39
40
  useLargeLayout,
40
41
  useSlot
41
42
  } from "@rebasepro/app";
42
- import { useUrlController, useCollectionRegistryController } from "../index";
43
+ import { useUrlController } from "../hooks/navigation/contexts/UrlContext";
44
+ import { useCollectionRegistryController } from "../hooks/navigation/contexts/CollectionRegistryContext";
43
45
  import { useNavigate } from "react-router-dom";
44
46
  import { getValueInPath } from "@rebasepro/utils";
45
47
  import { getEntityTitlePropertyKey, resolveTitleToString } from "../util/previews";
@@ -5,7 +5,7 @@ import type { EntityAction, EntityActionClickProps, SidePanelController } from "
5
5
  import React, { useMemo } from "react";
6
6
  import { Entity, getCollectionDataPath, RebaseContext } from "@rebasepro/types";
7
7
  import type { EntityFormActionsProps } from "../types/components/EntityFormActionsProps";
8
- import { copyEntityAction, deleteEntityAction } from "../components";
8
+ import { copyEntityAction, deleteEntityAction } from "./common/default_entity_actions";
9
9
  import { mergeEntityActions } from "../util/entity_actions";
10
10
  import { resolveEntityAction } from "../util/resolutions";
11
11
  import { Button, CircularProgress, cls, defaultBorderMixin, DialogActions, IconButton, LoadingButton, Tooltip, Typography } from "@rebasepro/ui";
@@ -19,7 +19,7 @@ import { SideDialogController, useSideDialogContext } from "./SideDialogs";
19
19
  import { FormexController } from "@rebasepro/forms";
20
20
  import { ErrorTooltip } from "@rebasepro/app";
21
21
  import { usePermissions } from "@rebasepro/app";
22
- import { useCMSContext } from "../index";
22
+ import { useCMSContext } from "../hooks/useCMSContext";
23
23
 
24
24
  export function EditFormActions({
25
25
  collection,
@@ -5,7 +5,8 @@ import React, { lazy, Suspense, useCallback, useEffect, useMemo, useRef, useStat
5
5
  import { Entity, EntityStatus, getCollectionDataPath } from "@rebasepro/types";
6
6
  import { PluginProviderStack, resolveComponentRef, useComponentOverride, CollectionScopeProvider } from "@rebasepro/app";
7
7
 
8
- import { CollectionViewBinding, EntityViewBinding } from "../components";
8
+ import { CollectionViewBinding } from "./CollectionViewBinding/CollectionViewBinding";
9
+ import { EntityViewBinding } from "./EntityViewBinding";
9
10
  import { CircularProgressCenter, iconSize } from "@rebasepro/ui";
10
11
  import {
11
12
  CenteredView,
@@ -48,7 +49,7 @@ import { JsonPreviewBinding } from "../components/JsonPreviewBinding";
48
49
  const EntityHistoryView = lazy(() => import("../components/history").then(m => ({ default: m.EntityHistoryView })));
49
50
  import { createFormexStub, getEntityFromCache } from "@rebasepro/app";
50
51
  import { usePermissions } from "@rebasepro/app";
51
- import { useUrlController } from "../index";
52
+ import { useUrlController } from "../hooks/navigation/contexts/UrlContext";
52
53
  import { useNavigate } from "react-router-dom";
53
54
 
54
55
  import { MAIN_TAB_VALUE, JSON_TAB_VALUE, HISTORY_TAB_VALUE } from "../util/view_constants";
@@ -27,7 +27,8 @@ import { IconForView } from "@rebasepro/app";
27
27
  import { getPropertyInPath } from "../util/property_utils";
28
28
  import { getEntityPreviewKeys, getEntityTitlePropertyKey } from "../util/previews";
29
29
  import { getValueInPath } from "@rebasepro/utils";
30
- import { useCollectionRegistryController, useSidePanel } from "../index";
30
+ import { useCollectionRegistryController } from "../hooks/navigation/contexts/CollectionRegistryContext";
31
+ import { useSidePanel } from "../hooks/useSidePanel";
31
32
 
32
33
  export type EntityPreviewBindingProps = {
33
34
  size?: PreviewSize,
@@ -23,7 +23,8 @@ import { useCollapsedGroups, buildCollapsedDefaults, useCustomizationController,
23
23
  import { useRestoreScroll } from "@rebasepro/app";
24
24
 
25
25
  import { SchemaDriftBanner } from "@rebasepro/app";
26
- import { useBreadcrumbsController, useCMSContext } from "../../index";
26
+ import { useBreadcrumbsController } from "../../hooks/useBreadcrumbsController";
27
+ import { useCMSContext } from "../../hooks/useCMSContext";
27
28
 
28
29
  export const DEFAULT_GROUP_NAME = "Views";
29
30
  export const ADMIN_GROUP_NAME = "Admin";
@@ -3,7 +3,7 @@ import { useNavigate } from "react-router-dom";
3
3
  ;
4
4
  import { Chip, Collapse, StarIcon } from "@rebasepro/ui";
5
5
  import { useUserConfigurationPersistence } from "@rebasepro/app";
6
- import { useNavigationStateController } from "../../index";
6
+ import { useNavigationStateController } from "../../hooks/navigation/contexts/NavigationStateContext";
7
7
 
8
8
  function NavigationChip({ entry }: { entry: NavigationEntry }) {
9
9
 
@@ -18,7 +18,7 @@ import { useNavigationStateController } from "../hooks/navigation/contexts/Navig
18
18
  import { CollectionEditorDialogs } from "./CollectionEditorDialogs";
19
19
  import { useEffect } from "react";
20
20
  import { useTranslation } from "@rebasepro/app";
21
- import { useBreadcrumbsController } from "../index";
21
+ import { useBreadcrumbsController } from "../hooks/useBreadcrumbsController";
22
22
 
23
23
  function SettingsView() {
24
24
  const { t } = useTranslation();
@@ -23,7 +23,8 @@ import { useSelectionController } from "../CollectionViewBinding/useSelectionCon
23
23
  import { useColumnIds } from "@rebasepro/app";
24
24
  import { useSideDialogContext } from "../SideDialogs";
25
25
  import { useAnalyticsController } from "@rebasepro/app";
26
- import { useUrlController, useSidePanel } from "../../index";
26
+ import { useUrlController } from "../../hooks/navigation/contexts/UrlContext";
27
+ import { useSidePanel } from "../../hooks/useSidePanel";
27
28
 
28
29
  /**
29
30
  * @group Components
@@ -7,7 +7,7 @@ import type { PreviewSize } from "../types/components/PropertyPreviewProps";
7
7
  import { getReferenceFrom } from "@rebasepro/common";
8
8
  import { ReferencePreview } from "../preview";
9
9
  import { Button, cls } from "@rebasepro/ui";
10
- import { useCollectionRegistryController } from "../index";
10
+ import { useCollectionRegistryController } from "../hooks/navigation/contexts/CollectionRegistryContext";
11
11
 
12
12
  export type ReferenceWidgetProps<M extends Record<string, unknown>> = {
13
13
  name?: string,
@@ -7,7 +7,7 @@ import { ReferencePreview } from "../../../preview";
7
7
  import { Button, Checkbox, Label, Select, SelectItem } from "@rebasepro/ui";
8
8
  import { getReferenceFrom } from "@rebasepro/common";
9
9
  import { useTranslation } from "@rebasepro/app";
10
- import { useCollectionRegistryController } from "../../../index";
10
+ import { useCollectionRegistryController } from "../../../hooks/navigation/contexts/CollectionRegistryContext";
11
11
 
12
12
  interface ReferenceFilterFieldProps {
13
13
  name: string,
@@ -10,8 +10,9 @@ import { DetailViewBinding } from "./DetailViewBinding";
10
10
  import { useSideDialogContext } from "./SideDialogs";
11
11
  import { useNavigate } from "react-router-dom";
12
12
  import { saveEntityToMemoryCache, useComponentOverride } from "@rebasepro/app";
13
- import { useCollectionRegistryController, useSidePanel } from "../index";
14
- import { useUrlController } from "../index";
13
+ import { useCollectionRegistryController } from "../hooks/navigation/contexts/CollectionRegistryContext";
14
+ import { useSidePanel } from "../hooks/useSidePanel";
15
+ import { useUrlController } from "../hooks/navigation/contexts/UrlContext";
15
16
  import { resolveDefaultSelectedView } from "@rebasepro/common";
16
17
 
17
18
  /**
@@ -61,8 +62,10 @@ export function SidePanelBinding(props: SidePanelBindingProps) {
61
62
  if (props.onUpdate) {
62
63
  props.onUpdate(params);
63
64
  }
64
- setShowEditInPanel(false);
65
+ setEditInDialog(false);
65
66
  if (params.status !== "existing") {
67
+ // A newly created entity replaces the panel with its own address,
68
+ // which already leaves the edit view behind.
66
69
  sidePanelController.replace({
67
70
  path: params.path,
68
71
  entityId: params.entityId,
@@ -70,6 +73,8 @@ export function SidePanelBinding(props: SidePanelBindingProps) {
70
73
  updateUrl: collection?.openEntityMode !== "dialog",
71
74
  collection: params.collection
72
75
  });
76
+ } else {
77
+ closeEditView();
73
78
  }
74
79
 
75
80
  if (sideDialogsController.pendingClose) {
@@ -89,13 +94,46 @@ export function SidePanelBinding(props: SidePanelBindingProps) {
89
94
 
90
95
  const collection = collectionRegistryController.getCollection(path) ?? props.collection;
91
96
 
92
- const [showEditInPanel, setShowEditInPanel] = useState(selectedTab === "edit");
97
+ // Dialog mode deliberately stays out of the URL, so it is the only case that
98
+ // needs local edit state. Everywhere else `edit` is a path segment, which
99
+ // keeps refresh and the back button working the same way they do in split.
100
+ const isDialogMode = collection?.openEntityMode === "dialog";
101
+ const [editInDialog, setEditInDialog] = useState(false);
102
+ const showEditInPanel = isDialogMode ? editInDialog : selectedTab === "edit";
93
103
  const isDetailMode = collection?.defaultEntityAction === "view" && !showEditInPanel && Boolean(entityId);
94
104
 
95
105
  // Reset edit mode when switching entities
96
106
  useEffect(() => {
97
- setShowEditInPanel(selectedTab === "edit");
98
- }, [entityId, selectedTab]);
107
+ setEditInDialog(false);
108
+ }, [entityId]);
109
+
110
+ const openEditView = useCallback(() => {
111
+ if (isDialogMode || !entityId) {
112
+ setEditInDialog(true);
113
+ return;
114
+ }
115
+ sidePanelController.replace({
116
+ path,
117
+ entityId,
118
+ selectedTab: "edit",
119
+ updateUrl: true,
120
+ collection
121
+ });
122
+ }, [isDialogMode, entityId, path, collection, sidePanelController]);
123
+
124
+ const closeEditView = useCallback(() => {
125
+ if (isDialogMode || !entityId) {
126
+ setEditInDialog(false);
127
+ return;
128
+ }
129
+ sidePanelController.replace({
130
+ path,
131
+ entityId,
132
+ selectedTab: undefined,
133
+ updateUrl: true,
134
+ collection
135
+ });
136
+ }, [isDialogMode, entityId, path, collection, sidePanelController]);
99
137
 
100
138
  // One-time correction: when the side panel opens without the correct
101
139
  // selectedTab but the resolved collection (from the registry) has a
@@ -154,7 +192,7 @@ entityId }
154
192
  entityId={entityId!}
155
193
  parentCollectionSlugs={parentCollectionSlugs}
156
194
  parentEntityIds={parentEntityIds}
157
- onEditClick={() => setShowEditInPanel(true)}
195
+ onEditClick={openEditView}
158
196
  barActions={({
159
197
  status,
160
198
  values
@@ -203,7 +241,7 @@ entityId }
203
241
  parentCollectionSlugs={parentCollectionSlugs} parentEntityIds={parentEntityIds}
204
242
  onValuesModified={onValuesModified}
205
243
  onSaved={onUpdate}
206
- navigateBack={() => setShowEditInPanel(false)}
244
+ navigateBack={closeEditView}
207
245
  barActions={({
208
246
  status,
209
247
  values
@@ -6,9 +6,10 @@ import { SidePanelControllerContext } from "../hooks/useSidePanel";
6
6
  import { SideDialogsControllerContext } from "../contexts/SideDialogsControllerContext";
7
7
  import { BreadcrumbsProvider } from "../contexts/BreacrumbsContext";
8
8
  import { useBreadcrumbsController } from "../hooks/useBreadcrumbsController";
9
- import { useCollectionRegistryController, useUrlController } from "../index";
10
- import { useNavigationStateController } from "../index";
11
- import { useAuthController, useBridgeRegistration } from "@rebasepro/app";
9
+ import { useCollectionRegistryController } from "../hooks/navigation/contexts/CollectionRegistryContext";
10
+ import { useUrlController } from "../hooks/navigation/contexts/UrlContext";
11
+ import { useNavigationStateController } from "../hooks/navigation/contexts/NavigationStateContext";
12
+ import { useAuthController, useBridgeRegistration, NavigationBlockerProvider } from "@rebasepro/app";
12
13
 
13
14
  /**
14
15
  * Provider that builds the SidePanelController and makes it available
@@ -54,14 +55,16 @@ export function SidePanelProvider({ children }: { children: React.ReactNode }) {
54
55
  );
55
56
 
56
57
  return (
57
- <BreadcrumbsProvider>
58
- <SideDialogsControllerContext.Provider value={sideDialogsController}>
59
- <SidePanelControllerContext.Provider value={sidePanelController}>
60
- <BridgeAutoRegistrar sidePanelController={sidePanelController}/>
61
- {children}
62
- </SidePanelControllerContext.Provider>
63
- </SideDialogsControllerContext.Provider>
64
- </BreadcrumbsProvider>
58
+ <NavigationBlockerProvider>
59
+ <BreadcrumbsProvider>
60
+ <SideDialogsControllerContext.Provider value={sideDialogsController}>
61
+ <SidePanelControllerContext.Provider value={sidePanelController}>
62
+ <BridgeAutoRegistrar sidePanelController={sidePanelController}/>
63
+ {children}
64
+ </SidePanelControllerContext.Provider>
65
+ </SideDialogsControllerContext.Provider>
66
+ </BreadcrumbsProvider>
67
+ </NavigationBlockerProvider>
65
68
  );
66
69
  }
67
70