@rebasepro/admin 0.9.1-canary.fd3754b → 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-DKGlYDPc.js → CollectionEditorDialog-BOlKGGg7.js} +149 -35
- package/dist/{CollectionEditorDialog-DKGlYDPc.js.map → CollectionEditorDialog-BOlKGGg7.js.map} +1 -1
- package/dist/{PropertyEditView-h0N-AMCY.js → PropertyEditView-i51tFOfM.js} +3 -3
- package/dist/PropertyEditView-i51tFOfM.js.map +1 -0
- package/dist/{RouterCollectionsStudioView-DdEStpFk.js → RouterCollectionsStudioView-Dh4W53PU.js} +4 -4
- package/dist/{RouterCollectionsStudioView-DdEStpFk.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-C0FdqGNz.js → history-CtIPJxvQ.js} +5 -5
- 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-DLAFyeZI.js → util-XFwQ9FEc.js} +17584 -21521
- package/dist/util-XFwQ9FEc.js.map +1 -0
- package/package.json +12 -17
- package/src/collection_editor/ui/collection_editor/CollectionRLSTab.tsx +139 -13
- 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/FiltersDialog.tsx +18 -0
- package/src/components/CollectionViewBinding/SplitListView.tsx +114 -63
- package/src/components/CollectionViewBinding/useBoardDataController.tsx +9 -3
- 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/RebaseNavigation.tsx +16 -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 +42 -25
- package/src/components/history/EntityHistoryView.tsx +3 -3
- 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/useResolvedNavigationFrom.tsx +4 -2
- 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-h0N-AMCY.js.map +0 -1
- package/dist/history-C0FdqGNz.js.map +0 -1
- package/dist/src-DLAFyeZI.js.map +0 -1
|
@@ -10,7 +10,9 @@ import { FiltersDialog } from "./FiltersDialog";
|
|
|
10
10
|
import { FilterPresetsButton } from "./FilterPresetsButton";
|
|
11
11
|
import { toArray } from "@rebasepro/utils";
|
|
12
12
|
import { useNavigate } from "react-router-dom";
|
|
13
|
-
import { useUrlController
|
|
13
|
+
import { useUrlController } from "../../hooks/navigation/contexts/UrlContext";
|
|
14
|
+
import { useCMSContext } from "../../hooks/useCMSContext";
|
|
15
|
+
import { withViewMode } from "../../util/view_mode";
|
|
14
16
|
|
|
15
17
|
export type CollectionViewStartActionsProps<M extends Record<string, unknown>> = {
|
|
16
18
|
collection: CollectionConfig<M>;
|
|
@@ -72,13 +74,7 @@ parentEntityIds,
|
|
|
72
74
|
};
|
|
73
75
|
|
|
74
76
|
const handleBackClick = useCallback(() => {
|
|
75
|
-
|
|
76
|
-
// Preserve the __view query param so the view mode is retained
|
|
77
|
-
const currentViewParam = new URLSearchParams(window.location.search).get("__view");
|
|
78
|
-
if (currentViewParam) {
|
|
79
|
-
collectionUrl += `${collectionUrl.includes("?") ? "&" : "?"}__view=${currentViewParam}`;
|
|
80
|
-
}
|
|
81
|
-
navigate(collectionUrl);
|
|
77
|
+
navigate(withViewMode(urlController.buildUrlCollectionPath(path)));
|
|
82
78
|
}, [navigate, urlController, path]);
|
|
83
79
|
|
|
84
80
|
const backButton = compact && (
|
|
@@ -8,6 +8,24 @@ import { resolveFilterOperators } from "@rebasepro/common";
|
|
|
8
8
|
import { useCollectionScope, useTranslation } from "@rebasepro/app";
|
|
9
9
|
import { FilterFieldBinding } from "../SelectableTable/filters/FilterFieldBinding";
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* `WhereFilterOp` is declared twice: once in `@rebasepro/types` as the canonical
|
|
13
|
+
* operator set every driver translates, and once in `@rebasepro/ui` for the
|
|
14
|
+
* table's own props. That is deliberate — `ui` is a standalone design system and
|
|
15
|
+
* depends on no Rebase package, so it cannot import the canonical one (and a
|
|
16
|
+
* type-only import would not help: `ui` emits declarations with `tsc`, which
|
|
17
|
+
* references external types rather than inlining them, so its published `.d.ts`
|
|
18
|
+
* would demand `@rebasepro/types` from every consumer).
|
|
19
|
+
*
|
|
20
|
+
* This file is where the two meet and get cast into each other, so it is where
|
|
21
|
+
* their agreement is checked. Adding an operator to one and not the other stops
|
|
22
|
+
* this line compiling instead of silently producing a filter the table can build
|
|
23
|
+
* and no driver can run — or one a driver supports and the table cannot offer.
|
|
24
|
+
*/
|
|
25
|
+
type MutuallyAssignable<A, B> = [A] extends [B] ? ([B] extends [A] ? true : never) : never;
|
|
26
|
+
const _whereFilterOpDefinitionsAgree: MutuallyAssignable<WhereFilterOp, VirtualTableWhereFilterOp> = true;
|
|
27
|
+
void _whereFilterOpDefinitionsAgree;
|
|
28
|
+
|
|
11
29
|
export interface FiltersDialogProps {
|
|
12
30
|
open: boolean;
|
|
13
31
|
onOpenChange: (open: boolean) => void;
|
|
@@ -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
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { CollectionConfig } from "@rebasepro/types";
|
|
2
2
|
import { Entity, FilterValues } from "@rebasepro/types";
|
|
3
3
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
4
|
-
import { useData, useRebaseContext } from "@rebasepro/app";
|
|
4
|
+
import { getRelationIncludeParams, useData, useRebaseContext } from "@rebasepro/app";
|
|
5
5
|
|
|
6
6
|
const DEFAULT_PAGE_SIZE = 20;
|
|
7
7
|
|
|
@@ -410,18 +410,24 @@ values: { ...e.values,
|
|
|
410
410
|
|
|
411
411
|
// Set up listener or fetch
|
|
412
412
|
const accessor = currentDataClient.collection(currentResolvedPath);
|
|
413
|
+
|
|
414
|
+
// Eagerly include relations to avoid N+1 fetches.
|
|
415
|
+
const includeParams = getRelationIncludeParams(currentCollection);
|
|
416
|
+
|
|
413
417
|
if (accessor.listen) {
|
|
414
418
|
const unsubscribe = accessor.listen({
|
|
415
419
|
where: whereFilter,
|
|
416
420
|
limit: itemCount,
|
|
417
|
-
orderBy: orderByParam
|
|
421
|
+
orderBy: orderByParam,
|
|
422
|
+
include: includeParams
|
|
418
423
|
}, res => onUpdate(res.data as Entity<M>[]), onError);
|
|
419
424
|
unsubscribersRef.current[column] = unsubscribe;
|
|
420
425
|
} else {
|
|
421
426
|
accessor.find({
|
|
422
427
|
where: whereFilter,
|
|
423
428
|
limit: itemCount,
|
|
424
|
-
orderBy: orderByParam
|
|
429
|
+
orderBy: orderByParam,
|
|
430
|
+
include: includeParams
|
|
425
431
|
})
|
|
426
432
|
.then(res => onUpdate(res.data as Entity<M>[]))
|
|
427
433
|
.catch(onError);
|
|
@@ -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
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useMemo, useRef, useEffect, lazy, Suspense } from "react";
|
|
1
|
+
import React, { useMemo, useRef, useEffect, useCallback, useContext, lazy, Suspense } from "react";
|
|
2
2
|
import {
|
|
3
3
|
useRebaseRegistry,
|
|
4
4
|
useRebaseContext,
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
useRebaseClient,
|
|
10
10
|
useData,
|
|
11
11
|
useDataSources,
|
|
12
|
+
CollectionResolverRegistrationContext,
|
|
12
13
|
StudioBridgeRegistryProvider,
|
|
13
14
|
useBridgeRegistration,
|
|
14
15
|
CustomizationControllerContext,
|
|
@@ -103,6 +104,20 @@ export function RebaseNavigation({ children }: RebaseNavigationProps) {
|
|
|
103
104
|
const defaultData = useData();
|
|
104
105
|
const getCollectionRef = useRef(collectionRegistryController.getCollection);
|
|
105
106
|
getCollectionRef.current = collectionRegistryController.getCollection;
|
|
107
|
+
|
|
108
|
+
// Hand the data layer a way to read this registry. It was built by <Rebase>,
|
|
109
|
+
// above us, and needs a collection's primary keys to give its rows an
|
|
110
|
+
// address — rows themselves carry only columns.
|
|
111
|
+
//
|
|
112
|
+
// Registered during render, not in an effect: the views that fetch rows are
|
|
113
|
+
// below us, and their effects run before ours, so an effect here would bind
|
|
114
|
+
// after the first page had already been converted. The call only assigns a
|
|
115
|
+
// ref, so repeating it per render is free.
|
|
116
|
+
const registerCollectionResolver = useContext(CollectionResolverRegistrationContext);
|
|
117
|
+
const resolveCollection = useCallback(
|
|
118
|
+
(slug: string) => getCollectionRef.current(slug),
|
|
119
|
+
[]);
|
|
120
|
+
registerCollectionResolver(resolveCollection);
|
|
106
121
|
const routedData = useMemo(() => buildRoutedRebaseData({
|
|
107
122
|
defaultData,
|
|
108
123
|
sources: dataSources.sources,
|
|
@@ -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
|