@rebasepro/admin 0.12.0 → 0.12.1-canary.g009ed95
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/README.md +1 -1
- package/dist/{CollectionEditorDialog-Dpc75wIZ.js → CollectionEditorDialog-B-iVE9y3.js} +75 -62
- package/dist/{CollectionEditorDialog-Dpc75wIZ.js.map → CollectionEditorDialog-B-iVE9y3.js.map} +1 -1
- package/dist/{PropertyEditView-DH3XZC2x.js → PropertyEditView-DoLEH_bQ.js} +36 -16
- package/dist/{PropertyEditView-DH3XZC2x.js.map → PropertyEditView-DoLEH_bQ.js.map} +1 -1
- package/dist/{RouterCollectionsStudioView-BZLq4MGp.js → RouterCollectionsStudioView-C3669Ezm.js} +5 -5
- package/dist/{RouterCollectionsStudioView-BZLq4MGp.js.map → RouterCollectionsStudioView-C3669Ezm.js.map} +1 -1
- package/dist/collection_editor/serializable_types.d.ts +4 -1
- package/dist/collection_editor/ui/collection_editor/CollectionRelationsTab.d.ts +1 -0
- package/dist/collection_editor/useLocalCollectionsConfigController.d.ts +8 -0
- package/dist/collection_editor_ui.js +5 -5
- package/dist/collection_editor_ui.js.map +1 -1
- package/dist/components/CollectionViewBinding/SplitListCloseButton.d.ts +10 -0
- package/dist/components/CollectionViewBinding/SplitViewContext.d.ts +23 -0
- package/dist/components/CollectionViewBinding/hooks/useKanbanDragAndDrop.d.ts +26 -0
- package/dist/components/DetailViewBinding.d.ts +5 -0
- package/dist/components/EditFormActions.d.ts +12 -1
- package/dist/components/EditViewBinding.d.ts +6 -1
- package/dist/components/EntityIdentityBar.d.ts +53 -0
- package/dist/components/EntityInspector.d.ts +29 -0
- package/dist/components/EntityPreviewNesting.d.ts +25 -0
- package/dist/components/EntityViewBinding.d.ts +9 -0
- package/dist/components/InlineEntityPreview.d.ts +47 -0
- package/dist/components/index.d.ts +2 -0
- package/dist/editor.js +18 -10
- package/dist/editor.js.map +1 -1
- package/dist/{export-BzIpUdSK.js → export-B8DIn-u8.js} +2 -2
- package/dist/{export-BzIpUdSK.js.map → export-B8DIn-u8.js.map} +1 -1
- package/dist/form/EntityForm.d.ts +1 -1
- package/dist/form/EntityFormActions.d.ts +11 -1
- package/dist/form/PropertyFieldBinding.d.ts +1 -1
- package/dist/form/components/FieldBlock.d.ts +30 -0
- package/dist/form/components/FormRail.d.ts +18 -0
- package/dist/form/components/FormSections.d.ts +16 -0
- package/dist/form/components/index.d.ts +3 -2
- package/dist/form/components/useRailVisible.d.ts +24 -0
- package/dist/form/field_bindings/BinaryFieldBinding.d.ts +1 -1
- package/dist/form/field_bindings/DateTimeFieldBinding.d.ts +1 -1
- package/dist/form/field_bindings/GeopointFieldBinding.d.ts +1 -1
- package/dist/form/field_bindings/MarkdownEditorFieldBinding.d.ts +1 -1
- package/dist/form/field_bindings/MultiSelectFieldBinding.d.ts +1 -1
- package/dist/form/field_bindings/ReadOnlyFieldBinding.d.ts +1 -1
- package/dist/form/field_bindings/SelectFieldBinding.d.ts +1 -1
- package/dist/form/field_bindings/StorageUploadFieldBinding.d.ts +1 -1
- package/dist/form/field_bindings/SwitchFieldBinding.d.ts +1 -1
- package/dist/form/field_bindings/TextFieldBinding.d.ts +1 -1
- package/dist/form/field_bindings/UserSelectFieldBinding.d.ts +1 -1
- package/dist/form/field_bindings/VectorFieldBinding.d.ts +1 -1
- package/dist/{history-g9688IaK.js → history-ClwDEtqN.js} +32 -31
- package/dist/history-ClwDEtqN.js.map +1 -0
- package/dist/hooks/navigation/utils.d.ts +16 -0
- package/dist/hooks/useBuildSidePanel.d.ts +14 -0
- package/dist/hooks/useEntityDisplayTitle.d.ts +18 -0
- package/dist/hooks/useRecordActions.d.ts +17 -0
- package/dist/{import-BjUG-cIY.js → import-Cwzf4RW5.js} +2 -2
- package/dist/{import-BjUG-cIY.js.map → import-Cwzf4RW5.js.map} +1 -1
- package/dist/index.js +106 -35
- package/dist/index.js.map +1 -1
- package/dist/markdown-CklalUUk.js.map +1 -1
- package/dist/preview/components/InlineEntityListPreview.d.ts +15 -0
- package/dist/preview/index.d.ts +1 -0
- package/dist/preview/property_previews/ArrayOfReferencesPreview.d.ts +1 -1
- package/dist/preview/property_previews/ArrayOfRelationsPreview.d.ts +1 -1
- package/dist/types/components/EntityFormActionsProps.d.ts +0 -1
- package/dist/types/components/EntityFormProps.d.ts +0 -5
- package/dist/types/fields.d.ts +24 -0
- package/dist/{util-B5fJm1FA.js → util-DSh3HRzK.js} +1974 -1130
- package/dist/util-DSh3HRzK.js.map +1 -0
- package/package.json +32 -34
- package/src/collection_editor/ConfigControllerProvider.tsx +1 -1
- package/src/collection_editor/serializable_types.ts +4 -3
- package/src/collection_editor/serializable_utils.ts +51 -17
- package/src/collection_editor/ui/collection_editor/CollectionEditorDialog.tsx +65 -22
- package/src/collection_editor/ui/collection_editor/CollectionRLSTab.tsx +38 -30
- package/src/collection_editor/ui/collection_editor/CollectionRelationsTab.tsx +3 -1
- package/src/collection_editor/ui/collection_editor/RouterCollectionStudioView.tsx +1 -1
- package/src/collection_editor/ui/collection_editor/RouterCollectionsStudioView.tsx +1 -1
- package/src/collection_editor/ui/collection_editor/properties/RelationPropertyField.tsx +16 -0
- package/src/collection_editor/useLocalCollectionsConfigController.tsx +127 -15
- package/src/collection_editor/validateCollectionJson.ts +3 -3
- package/src/components/AdminModeSyncer.tsx +1 -1
- package/src/components/CollectionTableBinding/PropertyTableCell.tsx +10 -2
- package/src/components/CollectionTableBinding/column_utils.tsx +26 -2
- package/src/components/CollectionTableBinding/table_bindings.tsx +2 -1
- package/src/components/CollectionViewBinding/CollectionBoardViewBinding.tsx +23 -16
- package/src/components/CollectionViewBinding/CollectionViewActions.tsx +7 -1
- package/src/components/CollectionViewBinding/CollectionViewBinding.tsx +8 -2
- package/src/components/CollectionViewBinding/CollectionViewStartActions.tsx +22 -11
- package/src/components/CollectionViewBinding/SplitListCloseButton.tsx +33 -0
- package/src/components/CollectionViewBinding/SplitListView.tsx +18 -3
- package/src/components/CollectionViewBinding/SplitViewContext.tsx +34 -0
- package/src/components/CollectionViewBinding/hooks/useKanbanDragAndDrop.ts +48 -9
- package/src/components/CollectionViewBinding/useBoardDataController.tsx +80 -25
- package/src/components/CollectionViewBinding/usePreviewSlots.ts +18 -6
- package/src/components/DefaultAppBar.tsx +1 -1
- package/src/components/DefaultDrawer.tsx +1 -1
- package/src/components/DetailViewBinding.tsx +100 -143
- package/src/components/DrawerNavigationItem.tsx +1 -1
- package/src/components/EditFormActions.tsx +54 -129
- package/src/components/EditViewBinding.tsx +182 -93
- package/src/components/EntityIdentityBar.tsx +288 -0
- package/src/components/EntityInspector.tsx +132 -0
- package/src/components/EntityPreviewBinding.tsx +6 -2
- package/src/components/EntityPreviewNesting.tsx +50 -0
- package/src/components/EntityViewBinding.tsx +35 -32
- package/src/components/HomePage/FavouritesView.tsx +1 -1
- package/src/components/HomePage/NavigationCardBinding.tsx +1 -1
- package/src/components/HomePage/SmallNavigationCard.tsx +1 -1
- package/src/components/InlineEntityPreview.tsx +149 -0
- package/src/components/RebaseLayout.tsx +1 -1
- package/src/components/RebaseNavigation.tsx +45 -5
- package/src/components/RebaseRouteDefs.tsx +1 -1
- package/src/components/SelectableTable/SelectableTable.tsx +2 -2
- package/src/components/SidePanelBinding.tsx +19 -13
- package/src/components/UserSelector.tsx +12 -6
- package/src/components/common/default_entity_actions.tsx +4 -3
- package/src/components/history/EntityHistoryEntry.tsx +28 -7
- package/src/components/history/EntityHistoryView.tsx +20 -20
- package/src/components/history/LastEditedByIndicator.tsx +4 -3
- package/src/components/index.ts +2 -0
- package/src/data_import/utils/get_import_inference_type.ts +6 -0
- package/src/form/EntityForm.tsx +267 -179
- package/src/form/EntityFormActions.tsx +50 -99
- package/src/form/EntityFormBinding.tsx +4 -2
- package/src/form/PropertyFieldBinding.tsx +4 -0
- package/src/form/components/FieldBlock.tsx +119 -0
- package/src/form/components/FormRail.tsx +125 -0
- package/src/form/components/FormSections.tsx +118 -0
- package/src/form/components/index.tsx +3 -2
- package/src/form/components/useRailVisible.tsx +46 -0
- package/src/form/field_bindings/BinaryFieldBinding.tsx +3 -2
- package/src/form/field_bindings/DateTimeFieldBinding.tsx +3 -2
- package/src/form/field_bindings/GeopointFieldBinding.tsx +3 -2
- package/src/form/field_bindings/MapFieldBinding.tsx +1 -2
- package/src/form/field_bindings/MarkdownEditorFieldBinding.tsx +3 -2
- package/src/form/field_bindings/MultiSelectFieldBinding.tsx +2 -1
- package/src/form/field_bindings/ReadOnlyFieldBinding.tsx +2 -1
- package/src/form/field_bindings/ReferenceFieldBinding.tsx +2 -1
- package/src/form/field_bindings/RelationFieldBinding.tsx +6 -4
- package/src/form/field_bindings/RepeatFieldBinding.tsx +4 -1
- package/src/form/field_bindings/SelectFieldBinding.tsx +3 -2
- package/src/form/field_bindings/StorageUploadFieldBinding.tsx +31 -8
- package/src/form/field_bindings/SwitchFieldBinding.tsx +7 -3
- package/src/form/field_bindings/TextFieldBinding.tsx +18 -16
- package/src/form/field_bindings/UserSelectFieldBinding.tsx +3 -2
- package/src/form/field_bindings/VectorFieldBinding.tsx +2 -1
- package/src/hooks/navigation/useBuildUrlController.tsx +1 -1
- package/src/hooks/navigation/useTopLevelNavigation.ts +18 -7
- package/src/hooks/navigation/utils.ts +20 -0
- package/src/hooks/useBuildSideDialogsController.tsx +2 -2
- package/src/hooks/useBuildSidePanel.tsx +22 -61
- package/src/hooks/useEntityDisplayTitle.tsx +60 -0
- package/src/hooks/useHistory.ts +6 -5
- package/src/hooks/useRecordActions.tsx +69 -0
- package/src/hooks/useResolvedUsers.ts +10 -5
- package/src/preview/PropertyPreview.tsx +48 -22
- package/src/preview/components/InlineEntityListPreview.tsx +42 -0
- package/src/preview/components/ReferencePreview.tsx +34 -14
- package/src/preview/components/RelationPreview.tsx +34 -14
- package/src/preview/index.ts +1 -0
- package/src/preview/property_previews/ArrayOfReferencesPreview.tsx +24 -1
- package/src/preview/property_previews/ArrayOfRelationsPreview.tsx +25 -1
- package/src/routes/RebaseRoute.tsx +2 -2
- package/src/types/components/EntityFormActionsProps.tsx +0 -1
- package/src/types/components/EntityFormProps.tsx +0 -6
- package/src/types/fields.tsx +27 -0
- package/dist/form/components/FormEntry.d.ts +0 -6
- package/dist/form/components/FormLayout.d.ts +0 -5
- package/dist/history-g9688IaK.js.map +0 -1
- package/dist/util-B5fJm1FA.js.map +0 -1
- package/src/form/components/FormEntry.tsx +0 -22
- package/src/form/components/FormLayout.tsx +0 -16
|
@@ -7,6 +7,42 @@ import { BoardItem } from "@rebasepro/ui";
|
|
|
7
7
|
import { BoardDataController } from "../useBoardDataController";
|
|
8
8
|
import { generateKeyBetween } from "fractional-indexing";
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* The digits a board order key is built from: base36, lower case only.
|
|
12
|
+
*
|
|
13
|
+
* The library's default alphabet is base62, and its keys only sort correctly
|
|
14
|
+
* under byte ordering. The column is sorted by the *database*, and Postgres'
|
|
15
|
+
* default collation is not byte ordering — under `en_US.UTF-8`, `"aa"` sorts
|
|
16
|
+
* before `"aC"`, so as soon as a board had been dragged around enough to reach
|
|
17
|
+
* the upper-case digits its order stopped matching the keys. One case has no
|
|
18
|
+
* such ambiguity: digits before letters is true of every collation in use.
|
|
19
|
+
*
|
|
20
|
+
* A key written with the old alphabet no longer validates, which is what makes
|
|
21
|
+
* the board offer to initialise the column — one click, and the order is
|
|
22
|
+
* rewritten in a form the database can sort.
|
|
23
|
+
*/
|
|
24
|
+
export const ORDER_KEY_DIGITS = "0123456789abcdefghijklmnopqrstuvwxyz";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Whether a stored order value is a key `fractional-indexing` can interpolate
|
|
28
|
+
* against.
|
|
29
|
+
*
|
|
30
|
+
* Anything else — a plain `"12"` from a seed or a migration, an empty string —
|
|
31
|
+
* makes `generateKeyBetween` throw, and the fallback below then hands out the
|
|
32
|
+
* same first key to every card, so each drag lands at the bottom of the column
|
|
33
|
+
* on top of the last one that did. Asking the library itself is the only
|
|
34
|
+
* definition of "valid" worth trusting.
|
|
35
|
+
*/
|
|
36
|
+
export function isValidOrderKey(value: unknown): value is string {
|
|
37
|
+
if (typeof value !== "string" || value.length === 0) return false;
|
|
38
|
+
try {
|
|
39
|
+
generateKeyBetween(value, null, ORDER_KEY_DIGITS);
|
|
40
|
+
return true;
|
|
41
|
+
} catch {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
10
46
|
export interface UseKanbanDragAndDropParams<M extends Record<string, unknown>> {
|
|
11
47
|
collection: AdminCollection<M>;
|
|
12
48
|
fullPath: string;
|
|
@@ -63,13 +99,16 @@ export function useKanbanDragAndDrop<M extends Record<string, unknown>>({
|
|
|
63
99
|
|
|
64
100
|
const movedIndex = targetColumnItems.findIndex(item => item.id === moveInfo.itemId);
|
|
65
101
|
|
|
66
|
-
//
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
: null;
|
|
102
|
+
// The order keys of the neighbours. A neighbour whose key is not a
|
|
103
|
+
// valid one counts as no neighbour at all: interpolating against it
|
|
104
|
+
// throws, and the whole move then falls back to a key that ignores
|
|
105
|
+
// where the card was actually dropped.
|
|
106
|
+
const keyAt = (index: number): string | null => {
|
|
107
|
+
const value = targetColumnItems[index]?.data.values?.[orderProperty];
|
|
108
|
+
return isValidOrderKey(value) ? value : null;
|
|
109
|
+
};
|
|
110
|
+
const prevKey = movedIndex > 0 ? keyAt(movedIndex - 1) : null;
|
|
111
|
+
const nextKey = movedIndex < targetColumnItems.length - 1 ? keyAt(movedIndex + 1) : null;
|
|
73
112
|
|
|
74
113
|
try {
|
|
75
114
|
const a = prevKey;
|
|
@@ -78,12 +117,12 @@ export function useKanbanDragAndDrop<M extends Record<string, unknown>>({
|
|
|
78
117
|
// Handle duplicate or out-of-order keys to prevent fractional-indexing crash
|
|
79
118
|
b = null;
|
|
80
119
|
}
|
|
81
|
-
const newKey = generateKeyBetween(a, b);
|
|
120
|
+
const newKey = generateKeyBetween(a, b, ORDER_KEY_DIGITS);
|
|
82
121
|
updatedValues = setIn(updatedValues, orderProperty, newKey) as Record<string, unknown>;
|
|
83
122
|
} catch (e) {
|
|
84
123
|
// Fallback: if keys are somehow invalid, generate from scratch
|
|
85
124
|
console.warn("fractional-indexing error, falling back:", e);
|
|
86
|
-
const newKey = generateKeyBetween(null, null);
|
|
125
|
+
const newKey = generateKeyBetween(null, null, ORDER_KEY_DIGITS);
|
|
87
126
|
updatedValues = setIn(updatedValues, orderProperty, newKey) as Record<string, unknown>;
|
|
88
127
|
}
|
|
89
128
|
}
|
|
@@ -6,6 +6,13 @@ import type { AdminCollection } from "@rebasepro/admin-types";
|
|
|
6
6
|
|
|
7
7
|
const DEFAULT_PAGE_SIZE = 20;
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* How long a column waits for its subscription's first payload before reading
|
|
11
|
+
* once over HTTP instead. Long enough that a healthy socket always wins the
|
|
12
|
+
* race, short enough that a dead one does not hold a blank board.
|
|
13
|
+
*/
|
|
14
|
+
const LIVE_FIRST_PAINT_TIMEOUT_MS = 2500;
|
|
15
|
+
|
|
9
16
|
/**
|
|
10
17
|
* Shallow equality for entity value records.
|
|
11
18
|
* Handles primitives and reference equality for nested values.
|
|
@@ -392,46 +399,94 @@ values: { ...e.values,
|
|
|
392
399
|
});
|
|
393
400
|
};
|
|
394
401
|
|
|
402
|
+
const accessor = currentDataClient.collection(currentResolvedPath);
|
|
403
|
+
|
|
404
|
+
// Eagerly include relations to avoid N+1 fetches.
|
|
405
|
+
const includeParams = getRelationIncludeParams(currentCollection);
|
|
406
|
+
|
|
407
|
+
const fetchOnce = () => accessor.find({
|
|
408
|
+
where: whereFilter,
|
|
409
|
+
limit: itemCount,
|
|
410
|
+
orderBy: orderByParam,
|
|
411
|
+
include: includeParams
|
|
412
|
+
});
|
|
413
|
+
|
|
395
414
|
const onError = (error: Error) => {
|
|
396
415
|
// Skip error handling if we're cleaning up
|
|
397
416
|
if (isCleaningUpRef.current) return;
|
|
398
417
|
|
|
399
418
|
console.error(`Error loading column ${column}:`, error);
|
|
400
|
-
setColumnData(prev => ({
|
|
401
|
-
...prev,
|
|
402
|
-
[column]: {
|
|
403
|
-
...prev[column],
|
|
404
|
-
entities: [],
|
|
405
|
-
loading: false,
|
|
406
|
-
hasMore: false,
|
|
407
|
-
error
|
|
408
|
-
}
|
|
409
|
-
}));
|
|
410
|
-
};
|
|
411
419
|
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
420
|
+
// A live subscription that fails is not the same thing as a column
|
|
421
|
+
// with nothing in it, but that is exactly what this used to render:
|
|
422
|
+
// entities cleared, no error surfaced, and a column reading "No
|
|
423
|
+
// items" above a header still counting eleven of them. Read once
|
|
424
|
+
// over HTTP instead — the board loses its live updates, not its
|
|
425
|
+
// contents — and only report the failure if that fails too.
|
|
426
|
+
fetchOnce()
|
|
427
|
+
.then(res => onUpdate(res.data as Entity<M>[]))
|
|
428
|
+
.catch(() => {
|
|
429
|
+
if (isCleaningUpRef.current) return;
|
|
430
|
+
setColumnData(prev => ({
|
|
431
|
+
...prev,
|
|
432
|
+
[column]: {
|
|
433
|
+
...prev[column],
|
|
434
|
+
entities: prev[column]?.entities ?? [],
|
|
435
|
+
loading: false,
|
|
436
|
+
hasMore: false,
|
|
437
|
+
error
|
|
438
|
+
}
|
|
439
|
+
}));
|
|
440
|
+
});
|
|
441
|
+
};
|
|
417
442
|
|
|
418
443
|
if (accessor.listen) {
|
|
444
|
+
let liveDataReceived = false;
|
|
419
445
|
const unsubscribe = accessor.listen({
|
|
420
446
|
where: whereFilter,
|
|
421
447
|
limit: itemCount,
|
|
422
448
|
orderBy: orderByParam,
|
|
423
449
|
include: includeParams
|
|
424
|
-
}, res =>
|
|
425
|
-
|
|
450
|
+
}, res => {
|
|
451
|
+
liveDataReceived = true;
|
|
452
|
+
onUpdate(res.data as Entity<M>[]);
|
|
453
|
+
}, onError);
|
|
454
|
+
|
|
455
|
+
// A subscription that is never going to answer takes the client's
|
|
456
|
+
// full 30s watchdog to say so, and the board spins the whole time.
|
|
457
|
+
// If the socket has not delivered shortly after mount, read once
|
|
458
|
+
// over HTTP and paint that; the live data still wins whenever it
|
|
459
|
+
// arrives. Costs nothing when realtime is healthy.
|
|
460
|
+
const firstPaintFallback = setTimeout(() => {
|
|
461
|
+
if (liveDataReceived || isCleaningUpRef.current) return;
|
|
462
|
+
fetchOnce()
|
|
463
|
+
.then(res => {
|
|
464
|
+
if (liveDataReceived || isCleaningUpRef.current) return;
|
|
465
|
+
onUpdate(res.data as Entity<M>[]);
|
|
466
|
+
})
|
|
467
|
+
.catch(() => undefined);
|
|
468
|
+
}, LIVE_FIRST_PAINT_TIMEOUT_MS);
|
|
469
|
+
|
|
470
|
+
unsubscribersRef.current[column] = () => {
|
|
471
|
+
clearTimeout(firstPaintFallback);
|
|
472
|
+
unsubscribe?.();
|
|
473
|
+
};
|
|
426
474
|
} else {
|
|
427
|
-
|
|
428
|
-
where: whereFilter,
|
|
429
|
-
limit: itemCount,
|
|
430
|
-
orderBy: orderByParam,
|
|
431
|
-
include: includeParams
|
|
432
|
-
})
|
|
475
|
+
fetchOnce()
|
|
433
476
|
.then(res => onUpdate(res.data as Entity<M>[]))
|
|
434
|
-
.catch(
|
|
477
|
+
.catch((error: Error) => {
|
|
478
|
+
if (isCleaningUpRef.current) return;
|
|
479
|
+
setColumnData(prev => ({
|
|
480
|
+
...prev,
|
|
481
|
+
[column]: {
|
|
482
|
+
...prev[column],
|
|
483
|
+
entities: prev[column]?.entities ?? [],
|
|
484
|
+
loading: false,
|
|
485
|
+
hasMore: false,
|
|
486
|
+
error
|
|
487
|
+
}
|
|
488
|
+
}));
|
|
489
|
+
});
|
|
435
490
|
}
|
|
436
491
|
}, []); // No dependencies - uses refs for all values
|
|
437
492
|
|
|
@@ -3,12 +3,12 @@ import type { Property, RelationProperty } from "@rebasepro/types";
|
|
|
3
3
|
import type { Entity, EntityRelation } from "@rebasepro/types";
|
|
4
4
|
import type { PropertyConfig, AdminCollection } from "@rebasepro/admin-types";
|
|
5
5
|
import { getEntityImagePreviewPropertyKey } from "@rebasepro/app";
|
|
6
|
-
import { getTitlePropertyCandidates, looksLikeIdentifierValue } from "@rebasepro/app";
|
|
6
|
+
import { getLeadingRelationTitleKey, getTitlePropertyCandidates, looksLikeIdentifierValue } from "@rebasepro/app";
|
|
7
7
|
import { getEntityFromCache } from "@rebasepro/app";
|
|
8
8
|
import { getEntityPreviewKeys } from "../../util/previews";
|
|
9
9
|
import { getValueInPath } from "@rebasepro/utils";
|
|
10
10
|
import type { AuthController } from "@rebasepro/admin-types";
|
|
11
|
-
import { ChipColorScheme, CHIP_COLORS } from "@rebasepro/ui";
|
|
11
|
+
import { ChipColorScheme, CHIP_COLORS, CHIP_SEED_KEYS } from "@rebasepro/ui";
|
|
12
12
|
|
|
13
13
|
/** Whether an authored relation yields many rows. Derived from its kind. */
|
|
14
14
|
function relationCardinality(relation: { kind?: string; cardinality?: string } | undefined): "one" | "many" | undefined {
|
|
@@ -119,7 +119,15 @@ export function resolveCollectionSlotKeys(
|
|
|
119
119
|
authController: AuthController,
|
|
120
120
|
propertyConfigs: Record<string, PropertyConfig>
|
|
121
121
|
): CollectionSlotKeys {
|
|
122
|
-
|
|
122
|
+
// One row per entity: when the collection leads with a relation and holds no
|
|
123
|
+
// name of its own, that relation *is* what the row is called. See
|
|
124
|
+
// `getLeadingRelationTitleKey` — the general ranking keeps relations last,
|
|
125
|
+
// which is right for a detail header but leaves a junction row titleless.
|
|
126
|
+
const rankedCandidates = getTitlePropertyCandidates(collection);
|
|
127
|
+
const leadingRelationKey = getLeadingRelationTitleKey(collection);
|
|
128
|
+
const titleKeyCandidates = leadingRelationKey
|
|
129
|
+
? [leadingRelationKey, ...rankedCandidates.filter(key => key !== leadingRelationKey)]
|
|
130
|
+
: rankedCandidates;
|
|
123
131
|
const titleKey = titleKeyCandidates[0];
|
|
124
132
|
const imageKey = getEntityImagePreviewPropertyKey(collection);
|
|
125
133
|
|
|
@@ -188,7 +196,9 @@ export function resolveCollectionSlotKeys(
|
|
|
188
196
|
if (!hasExplicitOrder) {
|
|
189
197
|
for (const [key, prop] of Object.entries(collection.properties)) {
|
|
190
198
|
const p = prop as Property;
|
|
191
|
-
|
|
199
|
+
// The relation filling the title slot is already the row's headline;
|
|
200
|
+
// repeating it as a chip below says the same thing twice.
|
|
201
|
+
if (p.type === "relation" && key !== titleKey) {
|
|
192
202
|
relationKeys.push(key);
|
|
193
203
|
}
|
|
194
204
|
}
|
|
@@ -417,8 +427,10 @@ id: relation.id });
|
|
|
417
427
|
|
|
418
428
|
if (items.length > 0) {
|
|
419
429
|
const relIndex = relationKeys.indexOf(relKey);
|
|
420
|
-
|
|
421
|
-
|
|
430
|
+
// Walking the seed keys rather than `Object.values`: the table also
|
|
431
|
+
// holds a bare-hue alias per hue, so raw values would hand the first
|
|
432
|
+
// two relations the same scheme.
|
|
433
|
+
const colorScheme = CHIP_COLORS[CHIP_SEED_KEYS[relIndex % CHIP_SEED_KEYS.length]];
|
|
422
434
|
relations.push({
|
|
423
435
|
property: prop,
|
|
424
436
|
propertyKey: relKey,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
|
|
2
2
|
import React from "react";
|
|
3
3
|
|
|
4
|
-
import { Link, useNavigate } from "react-router
|
|
4
|
+
import { Link, useNavigate } from "react-router";
|
|
5
5
|
import { RebaseLogo, LanguageToggle } from "@rebasepro/app";
|
|
6
6
|
import { ErrorBoundary, iconSize } from "@rebasepro/ui";
|
|
7
7
|
import {
|
|
@@ -6,7 +6,7 @@ import { useUrlController } from "../hooks/navigation/contexts/UrlContext";
|
|
|
6
6
|
import { useNavigationStateController } from "../hooks/navigation/contexts/NavigationStateContext";
|
|
7
7
|
|
|
8
8
|
|
|
9
|
-
import { Link, useNavigate } from "react-router
|
|
9
|
+
import { Link, useNavigate } from "react-router";
|
|
10
10
|
import { AnalyticsEvent } from "@rebasepro/admin-types";
|
|
11
11
|
import {
|
|
12
12
|
Avatar,
|
|
@@ -8,6 +8,9 @@ import { PluginProviderStack, resolveComponentRef, useComponentOverride, Collect
|
|
|
8
8
|
|
|
9
9
|
import { CollectionViewBinding } from "./CollectionViewBinding/CollectionViewBinding";
|
|
10
10
|
import { EntityViewBinding } from "./EntityViewBinding";
|
|
11
|
+
import { EntityIdentityBar } from "./EntityIdentityBar";
|
|
12
|
+
import { SplitListCloseButton } from "./CollectionViewBinding/SplitListCloseButton";
|
|
13
|
+
import { EntityInspector, InspectorTab } from "./EntityInspector";
|
|
11
14
|
import { CircularProgressCenter, iconSize } from "@rebasepro/ui";
|
|
12
15
|
import {
|
|
13
16
|
Alert,
|
|
@@ -41,13 +44,11 @@ import {
|
|
|
41
44
|
} from "@rebasepro/app";
|
|
42
45
|
import { useUrlController } from "../hooks/navigation/contexts/UrlContext";
|
|
43
46
|
import { useCollectionRegistryController } from "../hooks/navigation/contexts/CollectionRegistryContext";
|
|
44
|
-
import { useNavigate } from "react-router
|
|
47
|
+
import { useNavigate } from "react-router";
|
|
45
48
|
import { getValueInPath } from "@rebasepro/utils";
|
|
46
49
|
import { getEntityTitlePropertyKeyForEntity, isUserSelectProperty, resolveTitleToString } from "../util/previews";
|
|
47
50
|
import { getUserLabel, useResolvedUser } from "../hooks/useResolvedUsers";
|
|
48
|
-
import { JsonPreviewBinding } from "../components/JsonPreviewBinding";
|
|
49
51
|
|
|
50
|
-
const EntityHistoryView = lazy(() => import("../components/history").then(m => ({ default: m.EntityHistoryView })));
|
|
51
52
|
|
|
52
53
|
import { MAIN_TAB_VALUE, JSON_TAB_VALUE, HISTORY_TAB_VALUE } from "../util/view_constants";
|
|
53
54
|
|
|
@@ -76,6 +77,11 @@ export interface DetailViewBindingProps<M extends Record<string, unknown> = Reco
|
|
|
76
77
|
onEditClick?: () => void;
|
|
77
78
|
layout?: "side_panel" | "full_screen" | "split" | "dialog";
|
|
78
79
|
barActions?: (params: BarActionsParams) => React.ReactNode;
|
|
80
|
+
/**
|
|
81
|
+
* Controls the owning panel puts at the identity bar's leading edge, ahead
|
|
82
|
+
* of the breadcrumb — the side panel's close button.
|
|
83
|
+
*/
|
|
84
|
+
barActionsStart?: React.ReactNode;
|
|
79
85
|
}
|
|
80
86
|
|
|
81
87
|
export function DetailViewBinding<M extends Record<string, unknown>>({
|
|
@@ -134,7 +140,8 @@ function DetailViewBindingInner<M extends Record<string, unknown>>({
|
|
|
134
140
|
entity,
|
|
135
141
|
dataLoading,
|
|
136
142
|
layout = "full_screen",
|
|
137
|
-
barActions
|
|
143
|
+
barActions,
|
|
144
|
+
barActionsStart
|
|
138
145
|
}: DetailViewBindingProps<M> & {
|
|
139
146
|
entity?: Entity<M>,
|
|
140
147
|
dataLoading: boolean,
|
|
@@ -189,7 +196,18 @@ entityId }
|
|
|
189
196
|
const customViewsCount = customViews?.length ?? 0;
|
|
190
197
|
const includeJsonView = collection.includeJsonView === undefined ? true : collection.includeJsonView;
|
|
191
198
|
const includeHistoryView = Boolean(collection.history);
|
|
192
|
-
|
|
199
|
+
// Same split as the edit view: the strip is for destinations, the inspector
|
|
200
|
+
// for the developer tools that used to sit in front of them.
|
|
201
|
+
const hasAdditionalViews = customViewsCount > 0 || subcollectionsCount > 0;
|
|
202
|
+
|
|
203
|
+
const [inspectorTab, setInspectorTab] = useState<InspectorTab | null>(null);
|
|
204
|
+
|
|
205
|
+
const legacyInspectorTab: InspectorTab | undefined = selectedTab === JSON_TAB_VALUE
|
|
206
|
+
? "json"
|
|
207
|
+
: selectedTab === HISTORY_TAB_VALUE ? "history" : undefined;
|
|
208
|
+
useEffect(() => {
|
|
209
|
+
if (legacyInspectorTab) setInspectorTab(legacyInspectorTab);
|
|
210
|
+
}, [legacyInspectorTab]);
|
|
193
211
|
|
|
194
212
|
const {
|
|
195
213
|
resolvedEntityViews
|
|
@@ -198,13 +216,11 @@ entityId }
|
|
|
198
216
|
const validTabValues = useMemo(() => {
|
|
199
217
|
const set = new Set<string>([
|
|
200
218
|
MAIN_TAB_VALUE,
|
|
201
|
-
...(includeJsonView ? [JSON_TAB_VALUE] : []),
|
|
202
|
-
...(includeHistoryView ? [HISTORY_TAB_VALUE] : []),
|
|
203
219
|
...resolvedEntityViews.map(v => v.key),
|
|
204
220
|
...subcollections.map(s => s.slug)
|
|
205
221
|
]);
|
|
206
222
|
return set;
|
|
207
|
-
}, [
|
|
223
|
+
}, [resolvedEntityViews, subcollections]);
|
|
208
224
|
|
|
209
225
|
const activeTab = validTabValues.has(selectedTab) ? selectedTab : MAIN_TAB_VALUE;
|
|
210
226
|
|
|
@@ -321,35 +337,6 @@ entityId }
|
|
|
321
337
|
|
|
322
338
|
const globalLoading = dataLoading && !usedEntity;
|
|
323
339
|
|
|
324
|
-
// JSON view
|
|
325
|
-
const jsonTabMounted = mountedTabsRef.current.has(JSON_TAB_VALUE);
|
|
326
|
-
const jsonView = (activeTab === JSON_TAB_VALUE || jsonTabMounted) ? <div
|
|
327
|
-
className={cls("relative flex-1 h-full overflow-auto w-full",
|
|
328
|
-
{ "hidden": activeTab !== JSON_TAB_VALUE })}
|
|
329
|
-
key={"json_view"}
|
|
330
|
-
role="tabpanel">
|
|
331
|
-
<ErrorBoundary>
|
|
332
|
-
<JsonPreviewBinding values={usedEntity?.values ?? {}} />
|
|
333
|
-
</ErrorBoundary>
|
|
334
|
-
</div> : null;
|
|
335
|
-
|
|
336
|
-
// History view
|
|
337
|
-
const historyView = includeHistoryView && activeTab === HISTORY_TAB_VALUE ? <div
|
|
338
|
-
className={"relative flex-1 h-full overflow-auto w-full"}
|
|
339
|
-
key={"history_view"}
|
|
340
|
-
role="tabpanel">
|
|
341
|
-
<ErrorBoundary>
|
|
342
|
-
<Suspense fallback={<CircularProgressCenter />}>
|
|
343
|
-
<EntityHistoryView
|
|
344
|
-
collection={collection}
|
|
345
|
-
entity={usedEntity}
|
|
346
|
-
formContext={readOnlyFormContext as FormContext<Record<string, unknown>>}
|
|
347
|
-
modifiedValues={usedEntity?.values}
|
|
348
|
-
/>
|
|
349
|
-
</Suspense>
|
|
350
|
-
</ErrorBoundary>
|
|
351
|
-
</div> : null;
|
|
352
|
-
|
|
353
340
|
// Subcollection views
|
|
354
341
|
const subCollectionsViews = childViews && childViews.map(({ collection: subcollection }) => {
|
|
355
342
|
const subcollectionId = subcollection.slug;
|
|
@@ -490,7 +477,9 @@ entityId }
|
|
|
490
477
|
|
|
491
478
|
const shouldShowTopBar = Boolean(barActions) || hasAdditionalViews || layout === "side_panel" || layout === "dialog" || Boolean(backToCollectionButton);
|
|
492
479
|
|
|
493
|
-
const fullScreenButton = !barActions &&
|
|
480
|
+
const fullScreenButton = !barActions && // Not in split: the list panel beside it carries the close control,
|
|
481
|
+
// and an expand button there competes with it.
|
|
482
|
+
(layout === "side_panel" || layout === "dialog") && entityId ? (
|
|
494
483
|
<Tooltip title={"Open full screen"}>
|
|
495
484
|
<IconButton
|
|
496
485
|
size="small"
|
|
@@ -516,131 +505,99 @@ entityId }
|
|
|
516
505
|
</Button>
|
|
517
506
|
) : null;
|
|
518
507
|
|
|
519
|
-
// Main content view
|
|
508
|
+
// Main content view. The title, the `path/id` chip and the empty band under
|
|
509
|
+
// them have moved into the identity bar, and the `w-80 2xl:w-96` rail that
|
|
510
|
+
// held a single Edit button is gone — the bar holds it now.
|
|
520
511
|
const mainView = <div
|
|
521
512
|
className={cls(
|
|
522
513
|
"flex-1 flex flex-row w-full overflow-y-auto justify-center",
|
|
523
514
|
!mainViewVisible ? "hidden" : ""
|
|
524
515
|
)}>
|
|
525
|
-
<div
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
"
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
)}>
|
|
533
|
-
{/* Title and entity path */}
|
|
534
|
-
<div className={"w-full flex flex-col items-start my-4 lg:my-6"}>
|
|
535
|
-
<div className="flex items-center justify-between w-full">
|
|
536
|
-
<Typography
|
|
537
|
-
className={cls("my-4 grow line-clamp-1", collection.hideIdFromForm ? "mb-6" : "")}
|
|
538
|
-
variant={"h4"}>
|
|
539
|
-
{title}
|
|
540
|
-
</Typography>
|
|
541
|
-
{editButton}
|
|
542
|
-
</div>
|
|
543
|
-
|
|
544
|
-
<Alert color={"base"} outerClassName={"w-full"} size={"small"}>
|
|
545
|
-
<code
|
|
546
|
-
className={"text-xs select-all text-text-secondary dark:text-text-secondary-dark"}>
|
|
547
|
-
{usedEntity?.path ?? path}/{entityId}
|
|
548
|
-
</code>
|
|
549
|
-
</Alert>
|
|
550
|
-
</div>
|
|
551
|
-
|
|
552
|
-
{/* Property detail display */}
|
|
553
|
-
<div className="mt-12 flex flex-col gap-8">
|
|
554
|
-
{propertyDetailView()}
|
|
555
|
-
</div>
|
|
556
|
-
|
|
557
|
-
<div className="h-16" />
|
|
558
|
-
</div>
|
|
559
|
-
|
|
560
|
-
{/* Side action bar for large screens */}
|
|
561
|
-
{canEdit && onEditClick && layout === "full_screen" && <div
|
|
562
|
-
className={cls(
|
|
563
|
-
"overflow-auto h-full hidden @6xl:flex flex-col gap-2 w-80 2xl:w-96 px-4 py-16 sticky top-0 border-l",
|
|
564
|
-
defaultBorderMixin
|
|
565
|
-
)}>
|
|
566
|
-
<Button
|
|
567
|
-
fullWidth={true}
|
|
568
|
-
variant="filled"
|
|
569
|
-
color="primary"
|
|
570
|
-
startIcon={<PencilIcon size={iconSize.small} />}
|
|
571
|
-
onClick={onEditClick}>
|
|
572
|
-
{t("edit_entity")}
|
|
573
|
-
</Button>
|
|
574
|
-
</div>}
|
|
516
|
+
<div className={cls(
|
|
517
|
+
"w-full max-w-3xl 2xl:max-w-4xl flex flex-col",
|
|
518
|
+
layout === "dialog"
|
|
519
|
+
? "pt-5 pb-12 px-6 sm:px-8"
|
|
520
|
+
: "pt-6 pb-16 px-5 sm:px-8"
|
|
521
|
+
)}>
|
|
522
|
+
{propertyDetailView()}
|
|
575
523
|
</div>
|
|
576
524
|
</div>;
|
|
577
525
|
|
|
578
526
|
let result = <div className="relative flex flex-col h-full w-full bg-white dark:bg-surface-800">
|
|
579
527
|
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
{
|
|
584
|
-
|
|
585
|
-
{
|
|
586
|
-
|
|
587
|
-
{
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
</Tab>}
|
|
610
|
-
|
|
611
|
-
{includeHistoryView && <Tab
|
|
612
|
-
disabled={!hasAdditionalViews}
|
|
613
|
-
value={HISTORY_TAB_VALUE}
|
|
614
|
-
className={"text-sm"}>
|
|
615
|
-
<HistoryIcon size={iconSize.smallest} />
|
|
616
|
-
</Tab>}
|
|
617
|
-
|
|
618
|
-
<Tab
|
|
619
|
-
disabled={!hasAdditionalViews}
|
|
620
|
-
value={MAIN_TAB_VALUE}
|
|
621
|
-
className={"text-sm min-w-[90px]"}>
|
|
622
|
-
<span className="flex items-center gap-1.5">
|
|
623
|
-
{getIcon(collection.icon, undefined, undefined, "smallest")}
|
|
624
|
-
{collection.singularName ?? collection.name}
|
|
625
|
-
</span>
|
|
626
|
-
</Tab>
|
|
528
|
+
<EntityIdentityBar
|
|
529
|
+
collection={collection as AdminCollection}
|
|
530
|
+
title={title}
|
|
531
|
+
entityId={entityId}
|
|
532
|
+
status={"existing"}
|
|
533
|
+
dirty={false}
|
|
534
|
+
saving={false}
|
|
535
|
+
onInspect={includeJsonView ? () => setInspectorTab("json") : undefined}
|
|
536
|
+
onViewHistory={includeHistoryView ? () => setInspectorTab("history") : undefined}
|
|
537
|
+
leading={<>
|
|
538
|
+
{barActionsStart}
|
|
539
|
+
{/* Split view: closing the list is opening this record
|
|
540
|
+
full screen, and the control for it belongs at this
|
|
541
|
+
bar's leading edge. */}
|
|
542
|
+
{layout === "split" && <SplitListCloseButton/>}
|
|
543
|
+
</>}
|
|
544
|
+
trailing={<>
|
|
545
|
+
{editButton}
|
|
546
|
+
{pluginActionsTop}
|
|
547
|
+
{backToCollectionButton}
|
|
548
|
+
{fullScreenButton}
|
|
549
|
+
{barActions?.({
|
|
550
|
+
path,
|
|
551
|
+
entityId,
|
|
552
|
+
values: usedEntity?.values ?? {},
|
|
553
|
+
status: "existing"
|
|
554
|
+
})}
|
|
555
|
+
</>}
|
|
556
|
+
/>
|
|
627
557
|
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
558
|
+
{hasAdditionalViews && <div className={cls(
|
|
559
|
+
"h-10 shrink-0 flex items-stretch border-b px-2 min-w-0",
|
|
560
|
+
defaultBorderMixin
|
|
561
|
+
)}>
|
|
562
|
+
<Tabs
|
|
563
|
+
className={"!w-full"}
|
|
564
|
+
innerClassName={"h-full"}
|
|
565
|
+
variant={"boxy"}
|
|
566
|
+
value={activeTab}
|
|
567
|
+
onValueChange={(value) => {
|
|
568
|
+
onSideTabClick(value);
|
|
569
|
+
}}>
|
|
570
|
+
|
|
571
|
+
<Tab value={MAIN_TAB_VALUE}>
|
|
572
|
+
<span className="flex items-center gap-1.5">
|
|
573
|
+
{getIcon(collection.icon, undefined, undefined, "smallest")}
|
|
574
|
+
{collection.singularName ?? collection.name}
|
|
575
|
+
</span>
|
|
576
|
+
</Tab>
|
|
577
|
+
|
|
578
|
+
{customViewTabsStart}
|
|
579
|
+
{customViewTabsEnd}
|
|
580
|
+
{subcollectionTabs}
|
|
581
|
+
</Tabs>
|
|
633
582
|
</div>}
|
|
634
583
|
|
|
635
584
|
{globalLoading
|
|
636
585
|
? <DetailViewSkeleton collection={collection} />
|
|
637
586
|
: mainView}
|
|
638
587
|
|
|
639
|
-
{jsonView}
|
|
640
|
-
{historyView}
|
|
641
588
|
{customViewsView}
|
|
642
589
|
{subCollectionsViews}
|
|
643
590
|
|
|
591
|
+
<EntityInspector
|
|
592
|
+
tab={inspectorTab}
|
|
593
|
+
onTabChange={setInspectorTab}
|
|
594
|
+
onClose={() => setInspectorTab(null)}
|
|
595
|
+
collection={collection as AdminCollection}
|
|
596
|
+
entity={usedEntity as Entity<Record<string, unknown>> | undefined}
|
|
597
|
+
formContext={readOnlyFormContext as FormContext<Record<string, unknown>> | undefined}
|
|
598
|
+
values={usedEntity?.values}
|
|
599
|
+
includeHistory={includeHistoryView}/>
|
|
600
|
+
|
|
644
601
|
</div>;
|
|
645
602
|
|
|
646
603
|
if (plugins && plugins.length > 0) {
|