@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.
- package/dist/{CollectionEditorDialog-DbPpDDwb.js → CollectionEditorDialog-BOlKGGg7.js} +123 -31
- package/dist/{CollectionEditorDialog-DbPpDDwb.js.map → CollectionEditorDialog-BOlKGGg7.js.map} +1 -1
- package/dist/{PropertyEditView-DfJbZ67t.js → PropertyEditView-i51tFOfM.js} +3 -3
- package/dist/PropertyEditView-i51tFOfM.js.map +1 -0
- package/dist/{RouterCollectionsStudioView-C6XQ7Il5.js → RouterCollectionsStudioView-Dh4W53PU.js} +4 -4
- package/dist/{RouterCollectionsStudioView-C6XQ7Il5.js.map → RouterCollectionsStudioView-Dh4W53PU.js.map} +1 -1
- package/dist/collection_editor_ui.js +4 -4
- package/dist/components/field_configs.d.ts +17 -0
- package/dist/contexts/BreacrumbsContext.d.ts +1 -1
- package/dist/editor.js.map +1 -1
- package/dist/export-BM6HPujv.js +518 -0
- package/dist/export-BM6HPujv.js.map +1 -0
- package/dist/{history-GSa4BgDt.js → history-CtIPJxvQ.js} +3 -3
- package/dist/history-CtIPJxvQ.js.map +1 -0
- package/dist/hooks/useSelectionDialog.d.ts +1 -1
- package/dist/import-DzQnT03v.js +328 -0
- package/dist/import-DzQnT03v.js.map +1 -0
- package/dist/index.js +3299 -1
- package/dist/index.js.map +1 -0
- package/dist/preview/components/UrlComponentPreview.d.ts +2 -1
- package/dist/preview/util.d.ts +1 -0
- package/dist/util/view_mode.d.ts +54 -0
- package/dist/{src-B8WuGSPZ.js → util-XFwQ9FEc.js} +17388 -21329
- package/dist/util-XFwQ9FEc.js.map +1 -0
- package/package.json +8 -8
- package/src/collection_editor/ui/collection_editor/CollectionRLSTab.tsx +104 -9
- package/src/collection_editor/ui/collection_editor/properties/CommonPropertyFields.tsx +1 -1
- package/src/collection_editor/ui/collection_editor/properties/EnumPropertyField.tsx +1 -1
- package/src/components/CollectionTableBinding/CollectionRowActions.tsx +1 -1
- package/src/components/CollectionTableBinding/fields/TableReferenceField.tsx +1 -1
- package/src/components/CollectionViewBinding/CollectionBoardViewBinding.tsx +1 -1
- package/src/components/CollectionViewBinding/CollectionViewActions.tsx +1 -1
- package/src/components/CollectionViewBinding/CollectionViewBinding.tsx +76 -126
- package/src/components/CollectionViewBinding/CollectionViewStartActions.tsx +4 -8
- package/src/components/CollectionViewBinding/SplitListView.tsx +114 -63
- package/src/components/DetailViewBinding.tsx +4 -2
- package/src/components/EditFormActions.tsx +2 -2
- package/src/components/EditViewBinding.tsx +3 -2
- package/src/components/EntityPreviewBinding.tsx +2 -1
- package/src/components/HomePage/ContentHomePage.tsx +2 -1
- package/src/components/HomePage/FavouritesView.tsx +1 -1
- package/src/components/RebaseRouteDefs.tsx +1 -1
- package/src/components/ReferenceTable/SelectionTableBinding.tsx +2 -1
- package/src/components/ReferenceWidget.tsx +1 -1
- package/src/components/SelectableTable/filters/ReferenceFilterField.tsx +1 -1
- package/src/components/SidePanelBinding.tsx +46 -8
- package/src/components/SidePanelProvider.tsx +14 -11
- package/src/components/field_configs.tsx +41 -24
- package/src/components/history/EntityHistoryView.tsx +1 -1
- package/src/contexts/BreacrumbsContext.tsx +1 -1
- package/src/data_import/components/ImportNewPropertyFieldPreview.tsx +2 -4
- package/src/data_import/import/ImportCollectionAction.tsx +6 -6
- package/src/editor/selectors/color-selector.tsx +1 -1
- package/src/editor/selectors/node-selector.tsx +1 -1
- package/src/editor/selectors/text-buttons.tsx +1 -1
- package/src/form/EntityForm.tsx +4 -1
- package/src/form/EntityFormBinding.tsx +3 -1
- package/src/form/field_bindings/ArrayCustomShapedFieldBinding.tsx +2 -1
- package/src/form/field_bindings/ArrayOfReferencesFieldBinding.tsx +3 -2
- package/src/form/field_bindings/BlockFieldBinding.tsx +2 -1
- package/src/form/field_bindings/DateTimeFieldBinding.tsx +2 -1
- package/src/form/field_bindings/KeyValueFieldBinding.tsx +2 -1
- package/src/form/field_bindings/MapFieldBinding.tsx +2 -1
- package/src/form/field_bindings/MarkdownEditorFieldBinding.tsx +2 -1
- package/src/form/field_bindings/MultiSelectFieldBinding.tsx +2 -1
- package/src/form/field_bindings/MultipleRelationFieldBinding.tsx +2 -1
- package/src/form/field_bindings/ReadOnlyFieldBinding.tsx +2 -1
- package/src/form/field_bindings/ReferenceFieldBinding.tsx +3 -2
- package/src/form/field_bindings/RelationFieldBinding.tsx +2 -1
- package/src/form/field_bindings/RepeatFieldBinding.tsx +2 -1
- package/src/form/field_bindings/SelectFieldBinding.tsx +2 -1
- package/src/form/field_bindings/StorageUploadFieldBinding.tsx +2 -1
- package/src/form/field_bindings/SwitchFieldBinding.tsx +2 -1
- package/src/form/field_bindings/TextFieldBinding.tsx +2 -1
- package/src/form/field_bindings/UserSelectFieldBinding.tsx +2 -1
- package/src/form/field_bindings/VectorFieldBinding.tsx +2 -1
- package/src/hooks/useBuildSidePanel.tsx +8 -0
- package/src/hooks/useSelectionDialog.tsx +3 -3
- package/src/preview/components/ImagePreview.tsx +1 -1
- package/src/preview/components/ReferencePreview.tsx +1 -1
- package/src/preview/components/RelationPreview.tsx +2 -2
- package/src/preview/components/UrlComponentPreview.tsx +2 -9
- package/src/preview/property_previews/ArrayOfStringsPreview.tsx +1 -1
- package/src/preview/property_previews/NumberPropertyPreview.tsx +1 -1
- package/src/preview/util.ts +9 -0
- package/src/routes/CustomViewRoute.tsx +2 -1
- package/src/routes/RebaseRoute.tsx +20 -25
- package/src/util/navigation_utils.ts +2 -5
- package/src/util/view_mode.ts +102 -0
- package/dist/PropertyEditView-DfJbZ67t.js.map +0 -1
- package/dist/history-GSa4BgDt.js.map +0 -1
- 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 "../../
|
|
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 "../../
|
|
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
|
-
|
|
176
|
-
|
|
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
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
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
|
|
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
|
|
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 "
|
|
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 "../
|
|
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
|
|
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 "../
|
|
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
|
|
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
|
|
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 "../../
|
|
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 "../
|
|
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
|
|
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 "../
|
|
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 "../../../
|
|
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
|
|
14
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
98
|
-
}, [entityId
|
|
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={
|
|
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={
|
|
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
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
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
|
-
<
|
|
58
|
-
<
|
|
59
|
-
<
|
|
60
|
-
<
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
|