@rebasepro/admin 0.12.1-canary.gf5f1d39 → 0.13.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/README.md +1 -1
- package/dist/{CollectionEditorDialog-Dpc75wIZ.js → CollectionEditorDialog-CjXkWZ64.js} +75 -62
- package/dist/{CollectionEditorDialog-Dpc75wIZ.js.map → CollectionEditorDialog-CjXkWZ64.js.map} +1 -1
- package/dist/{PropertyEditView-DH3XZC2x.js → PropertyEditView-W8I3YV89.js} +36 -16
- package/dist/{PropertyEditView-DH3XZC2x.js.map → PropertyEditView-W8I3YV89.js.map} +1 -1
- package/dist/{RouterCollectionsStudioView-BZLq4MGp.js → RouterCollectionsStudioView-BsLTNoqX.js} +5 -5
- package/dist/{RouterCollectionsStudioView-BZLq4MGp.js.map → RouterCollectionsStudioView-BsLTNoqX.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/CollectionTableBinding/column_utils.d.ts +17 -0
- package/dist/components/CollectionViewBinding/CollectionViewStartActions.d.ts +7 -2
- package/dist/components/CollectionViewBinding/SortButton.d.ts +24 -0
- 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 +35 -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/history/EntityHistoryView.d.ts +4 -1
- 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-J79rMoNj.js} +2 -2
- package/dist/{export-BzIpUdSK.js.map → export-J79rMoNj.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-D7L_JlLO.js} +35 -33
- package/dist/history-D7L_JlLO.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/useHistory.d.ts +7 -0
- package/dist/hooks/useRecordActions.d.ts +17 -0
- package/dist/{import-BjUG-cIY.js → import-BvYl--U4.js} +2 -2
- package/dist/{import-BjUG-cIY.js.map → import-BvYl--U4.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-GiwPgxnL.js} +2106 -1117
- package/dist/util-GiwPgxnL.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 +59 -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 +9 -2
- package/src/components/CollectionViewBinding/CollectionViewStartActions.tsx +47 -12
- package/src/components/CollectionViewBinding/SortButton.tsx +189 -0
- 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 +112 -148
- package/src/components/DrawerNavigationItem.tsx +1 -1
- package/src/components/EditFormActions.tsx +54 -129
- package/src/components/EditViewBinding.tsx +202 -99
- package/src/components/EntityIdentityBar.tsx +288 -0
- package/src/components/EntityInspector.tsx +137 -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 +29 -24
- 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 +20 -10
- 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
|
@@ -4,11 +4,11 @@ import type { CustomizationController, SidePanelBindingProps, SideDialogPanelPro
|
|
|
4
4
|
import { useCallback, useEffect, useRef, useMemo } from "react";
|
|
5
5
|
import { CollectionRegistryController } from "@rebasepro/types";
|
|
6
6
|
import { AuthController } from "@rebasepro/admin-types";
|
|
7
|
-
import { useLocation } from "react-router
|
|
7
|
+
import { useLocation } from "react-router";
|
|
8
8
|
import { NavigationViewInternal } from "@rebasepro/app";
|
|
9
9
|
import { getNavigationEntriesFromPath, removeInitialAndTrailingSlashes, removeTrailingSlash, resolveDefaultSelectedView } from "@rebasepro/app";
|
|
10
10
|
import { resolvedSelectedEntityView } from "../util/resolutions";
|
|
11
|
-
import {
|
|
11
|
+
import { CONTAINER_FULL_WIDTH, SIDE_PANEL_DEFAULT_WIDTH } from "@rebasepro/app";
|
|
12
12
|
import { useCustomizationController, useLargeLayout, useComponentOverride, CollectionScopeProvider } from "@rebasepro/app";
|
|
13
13
|
import { JSON_TAB_VALUE, HISTORY_TAB_VALUE } from "../components/EditViewBinding";
|
|
14
14
|
import React from "react";
|
|
@@ -39,71 +39,32 @@ function ResolvedSidePanelBinding(props: SidePanelBindingProps) {
|
|
|
39
39
|
const NEW_URL_HASH = "new_side";
|
|
40
40
|
const SIDE_URL_HASH = "side";
|
|
41
41
|
|
|
42
|
+
/**
|
|
43
|
+
* How wide the side panel opens.
|
|
44
|
+
*
|
|
45
|
+
* One width for the whole panel, whatever tab is showing. It used to depend on
|
|
46
|
+
* the selected tab — the form got one width and a subcollection or custom view
|
|
47
|
+
* got `calc(55vw + 768px)` — so the panel physically resized under the cursor
|
|
48
|
+
* every time you switched tabs. And the "form" width was derived from how
|
|
49
|
+
* deeply nested the collection's *properties* were, which is not a thing anyone
|
|
50
|
+
* can predict from looking at the panel.
|
|
51
|
+
*
|
|
52
|
+
* Now: `width` prop, then the collection's `sideDialogWidth`, then a default
|
|
53
|
+
* sized for the content the panel actually has to hold — the form column plus
|
|
54
|
+
* its metadata rail — capped so it never eats the whole window.
|
|
55
|
+
*/
|
|
42
56
|
export function getEntityViewWidth(props: SidePanelBindingProps<any>, small: boolean, customizationController: CustomizationController): string {
|
|
43
57
|
if (small) return CONTAINER_FULL_WIDTH;
|
|
44
58
|
|
|
45
|
-
const {
|
|
46
|
-
selectedSecondaryForm
|
|
47
|
-
} = resolvedSelectedEntityView(props.collection?.entityViews, customizationController, props.selectedTab);
|
|
48
|
-
|
|
49
|
-
const shouldUseSmallLayout = !props.selectedTab || props.selectedTab === "edit" || props.selectedTab === JSON_TAB_VALUE || props.selectedTab === HISTORY_TAB_VALUE || Boolean(selectedSecondaryForm);
|
|
50
|
-
|
|
51
|
-
let resolvedWidth: string | undefined;
|
|
52
59
|
if (props.width) {
|
|
53
|
-
|
|
54
|
-
} else if (props.collection?.sideDialogWidth) {
|
|
55
|
-
resolvedWidth = typeof props.collection.sideDialogWidth === "number" ? `${props.collection.sideDialogWidth}px` : props.collection.sideDialogWidth;
|
|
60
|
+
return typeof props.width === "number" ? `${props.width}px` : props.width;
|
|
56
61
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
if (resolvedWidth) {
|
|
62
|
-
return resolvedWidth
|
|
63
|
-
} else if (!props.collection) {
|
|
64
|
-
return FORM_CONTAINER_WIDTH;
|
|
65
|
-
} else {
|
|
66
|
-
return calculateCollectionDesiredWidth(props.collection);
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const collectionViewWidthCache: { [key: string]: string } = {};
|
|
72
|
-
|
|
73
|
-
function calculateCollectionDesiredWidth(collection: AdminCollection<any>): string {
|
|
74
|
-
if (collectionViewWidthCache[collection.slug]) {
|
|
75
|
-
return collectionViewWidthCache[collection.slug];
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
let result = FORM_CONTAINER_WIDTH
|
|
79
|
-
if (collection?.properties) {
|
|
80
|
-
const values = Object.values(collection.properties).map((p: Property) => getNestedPropertiesDepth(p));
|
|
81
|
-
const maxDepth = Math.max(...values);
|
|
82
|
-
if (maxDepth < 3) {
|
|
83
|
-
result = FORM_CONTAINER_WIDTH;
|
|
84
|
-
} else {
|
|
85
|
-
result = 768 + 32 * (maxDepth - 2) + "px";
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
collectionViewWidthCache[collection.slug] = result;
|
|
89
|
-
return result;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function getNestedPropertiesDepth(property: Property, accumulator = 0): number {
|
|
93
|
-
if (property.type === "map" && property.properties) {
|
|
94
|
-
const values = Object.values(property.properties).flatMap((childProperty) => getNestedPropertiesDepth(childProperty as Readonly<Property>, accumulator + 1));
|
|
95
|
-
return Math.max(...values);
|
|
96
|
-
} else if (property.type === "array" && property.oneOf) {
|
|
97
|
-
return accumulator + 3;
|
|
98
|
-
} else if (property.type === "array" && property.of) {
|
|
99
|
-
if (Array.isArray(property.of)) {
|
|
100
|
-
return Math.max(...property.of.map((p) => getNestedPropertiesDepth(p, accumulator + 1)));
|
|
101
|
-
} else {
|
|
102
|
-
return getNestedPropertiesDepth(property.of, accumulator + 1);
|
|
103
|
-
}
|
|
104
|
-
} else {
|
|
105
|
-
return accumulator + 1;
|
|
62
|
+
if (props.collection?.sideDialogWidth) {
|
|
63
|
+
return typeof props.collection.sideDialogWidth === "number"
|
|
64
|
+
? `${props.collection.sideDialogWidth}px`
|
|
65
|
+
: props.collection.sideDialogWidth;
|
|
106
66
|
}
|
|
67
|
+
return SIDE_PANEL_DEFAULT_WIDTH;
|
|
107
68
|
}
|
|
108
69
|
|
|
109
70
|
export const useBuildSidePanel = (collectionRegistryController: CollectionRegistryController,
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { AdminCollection } from "@rebasepro/admin-types";
|
|
2
|
+
import type { EntityStatus } from "@rebasepro/types";
|
|
3
|
+
import { getValueInPath } from "@rebasepro/utils";
|
|
4
|
+
import { getEntityTitlePropertyKeyForEntity, isUserSelectProperty, resolveTitleToString } from "../util/previews";
|
|
5
|
+
import { getUserLabel, useResolvedUser } from "./useResolvedUsers";
|
|
6
|
+
|
|
7
|
+
export interface UseEntityDisplayTitleParams<M extends Record<string, unknown>> {
|
|
8
|
+
collection: AdminCollection<M>;
|
|
9
|
+
values?: Record<string, unknown>;
|
|
10
|
+
entityId?: string | number;
|
|
11
|
+
status: EntityStatus;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The human name for a record, for the identity bar and the breadcrumb.
|
|
16
|
+
*
|
|
17
|
+
* The guard on `status` is the point. The title resolver walks candidate
|
|
18
|
+
* properties and returns the first non-empty one, and on a brand new entity the
|
|
19
|
+
* only populated values are the property defaults — so creating a blog post
|
|
20
|
+
* opened a page headed **draft**, the default of its `status` enum. A record
|
|
21
|
+
* that does not exist yet has no name; it has an intent.
|
|
22
|
+
*/
|
|
23
|
+
export function useEntityDisplayTitle<M extends Record<string, unknown>>({
|
|
24
|
+
collection,
|
|
25
|
+
values,
|
|
26
|
+
entityId,
|
|
27
|
+
status
|
|
28
|
+
}: UseEntityDisplayTitleParams<M>): string {
|
|
29
|
+
|
|
30
|
+
const isNew = status === "new";
|
|
31
|
+
|
|
32
|
+
const titlePropertyKey = getEntityTitlePropertyKeyForEntity(collection, values, entityId);
|
|
33
|
+
const rawTitle = !isNew && values && titlePropertyKey
|
|
34
|
+
? getValueInPath(values, titlePropertyKey)
|
|
35
|
+
: undefined;
|
|
36
|
+
|
|
37
|
+
// A user picker stores an id: resolve it to the person, like a relation.
|
|
38
|
+
const titleUser = useResolvedUser(
|
|
39
|
+
isUserSelectProperty(collection, titlePropertyKey) && typeof rawTitle === "string"
|
|
40
|
+
? rawTitle
|
|
41
|
+
: undefined
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
const singular = collection.singularName ?? collection.name;
|
|
45
|
+
|
|
46
|
+
if (isNew) {
|
|
47
|
+
return `New ${singular.toLowerCase()}`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (titleUser) {
|
|
51
|
+
return getUserLabel(titleUser);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (rawTitle !== undefined && rawTitle !== null) {
|
|
55
|
+
const resolved = resolveTitleToString(rawTitle);
|
|
56
|
+
if (resolved) return resolved;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return singular;
|
|
60
|
+
}
|
package/src/hooks/useHistory.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useCallback, useEffect, useRef, useState, useMemo } from "react";
|
|
2
|
-
import { useApiConfig } from "@rebasepro/app";
|
|
2
|
+
import { useApiBase, useApiConfig } from "@rebasepro/app";
|
|
3
3
|
import type { EntityHistoryEntry } from "@rebasepro/types";
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -28,9 +28,17 @@ export function useHistory(params: {
|
|
|
28
28
|
entityId: string | number | undefined;
|
|
29
29
|
enabled?: boolean;
|
|
30
30
|
pageSize?: number;
|
|
31
|
+
/**
|
|
32
|
+
* Bumped by the caller when the record is saved. A save is the one event
|
|
33
|
+
* that adds a revision, and nothing else here would notice it: without
|
|
34
|
+
* this the open panel keeps showing the list it fetched when it opened,
|
|
35
|
+
* so the revision you just created never appears.
|
|
36
|
+
*/
|
|
37
|
+
refreshToken?: number;
|
|
31
38
|
}): UseEntityHistoryResult {
|
|
32
|
-
const { slug, entityId, enabled = true, pageSize = 10 } = params;
|
|
39
|
+
const { slug, entityId, enabled = true, pageSize = 10, refreshToken } = params;
|
|
33
40
|
const apiConfig = useApiConfig();
|
|
41
|
+
const apiBase = useApiBase();
|
|
34
42
|
|
|
35
43
|
const [entries, setEntries] = useState<HistoryEntryData[]>([]);
|
|
36
44
|
const [total, setTotal] = useState(0);
|
|
@@ -40,7 +48,11 @@ export function useHistory(params: {
|
|
|
40
48
|
const [offset, setOffset] = useState(0);
|
|
41
49
|
const [refreshTrigger, setRefreshTrigger] = useState(0);
|
|
42
50
|
|
|
43
|
-
// Track the current entity identity so we can detect stale responses
|
|
51
|
+
// Track the current entity identity so we can detect stale responses.
|
|
52
|
+
// `refreshToken` is part of the identity on purpose: a save makes the list
|
|
53
|
+
// we are holding a stale page of a longer list, which is the same problem
|
|
54
|
+
// as having moved to a different record.
|
|
55
|
+
const entityKey = `${slug}/${entityId}/${refreshToken ?? 0}`;
|
|
44
56
|
const currentEntityRef = useRef<string | undefined>(undefined);
|
|
45
57
|
|
|
46
58
|
const fetchEntries = useCallback(async (
|
|
@@ -59,13 +71,12 @@ export function useHistory(params: {
|
|
|
59
71
|
};
|
|
60
72
|
if (token) headers["Authorization"] = `Bearer ${token}`;
|
|
61
73
|
|
|
62
|
-
const url = `${
|
|
74
|
+
const url = `${apiBase}/data/${slug}/${entityId}/history?limit=${pageSize}&offset=${currentOffset}`;
|
|
63
75
|
const response = await fetch(url, { headers,
|
|
64
76
|
signal });
|
|
65
77
|
|
|
66
78
|
// Check if the request was aborted or the entity changed while in-flight
|
|
67
79
|
if (signal?.aborted) return;
|
|
68
|
-
const entityKey = `${slug}/${entityId}`;
|
|
69
80
|
if (currentEntityRef.current !== entityKey) return;
|
|
70
81
|
|
|
71
82
|
if (!response.ok) {
|
|
@@ -90,12 +101,11 @@ signal });
|
|
|
90
101
|
} finally {
|
|
91
102
|
setIsLoading(false);
|
|
92
103
|
}
|
|
93
|
-
}, [apiConfig, slug, entityId, enabled, pageSize]);
|
|
104
|
+
}, [apiConfig, apiBase, slug, entityId, entityKey, enabled, pageSize]);
|
|
94
105
|
|
|
95
106
|
// Single unified effect: reset state when entity changes, fetch when offset changes.
|
|
96
107
|
// Uses AbortController to cancel stale requests on entity change or unmount.
|
|
97
108
|
useEffect(() => {
|
|
98
|
-
const entityKey = `${slug}/${entityId}`;
|
|
99
109
|
const isNewEntity = currentEntityRef.current !== entityKey;
|
|
100
110
|
|
|
101
111
|
if (isNewEntity) {
|
|
@@ -117,7 +127,7 @@ signal });
|
|
|
117
127
|
return () => {
|
|
118
128
|
abortController.abort();
|
|
119
129
|
};
|
|
120
|
-
}, [fetchEntries, offset, enabled, entityId,
|
|
130
|
+
}, [fetchEntries, offset, enabled, entityId, entityKey, refreshTrigger]);
|
|
121
131
|
|
|
122
132
|
const loadMore = useCallback(() => {
|
|
123
133
|
if (!isLoading && hasMore && offset + entries.length < total) {
|
|
@@ -136,7 +146,7 @@ signal });
|
|
|
136
146
|
};
|
|
137
147
|
if (token) headers["Authorization"] = `Bearer ${token}`;
|
|
138
148
|
|
|
139
|
-
const url = `${
|
|
149
|
+
const url = `${apiBase}/data/${slug}/${entityId}/history/${historyId}/revert`;
|
|
140
150
|
const response = await fetch(url, { method: "POST",
|
|
141
151
|
headers });
|
|
142
152
|
|
|
@@ -154,7 +164,7 @@ headers });
|
|
|
154
164
|
|
|
155
165
|
// Return the reverted entity data so callers can update the form
|
|
156
166
|
return result.data as Record<string, unknown>;
|
|
157
|
-
}, [apiConfig, slug, entityId]);
|
|
167
|
+
}, [apiConfig, apiBase, slug, entityId]);
|
|
158
168
|
|
|
159
169
|
return useMemo(() => ({
|
|
160
170
|
entries,
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { EntityAction, AdminCollection } from "@rebasepro/admin-types";
|
|
2
|
+
import type { Entity } from "@rebasepro/types";
|
|
3
|
+
import { useMemo } from "react";
|
|
4
|
+
import { useCustomizationController, usePermissions } from "@rebasepro/app";
|
|
5
|
+
import { copyEntityAction, deleteEntityAction, unlinkEntityAction } from "../components/common/default_entity_actions";
|
|
6
|
+
import { mergeEntityActions } from "../util/entity_actions";
|
|
7
|
+
import { resolveEntityAction } from "../util/resolutions";
|
|
8
|
+
import { useChildViewSource } from "./useChildViewSource";
|
|
9
|
+
|
|
10
|
+
export interface UseRecordActionsParams<M extends Record<string, unknown>> {
|
|
11
|
+
collection: AdminCollection<M>;
|
|
12
|
+
path: string;
|
|
13
|
+
entity?: Entity<M>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The actions you can perform *on* a record — copy, delete, and whatever the
|
|
18
|
+
* collection adds — resolved against the current user's permissions.
|
|
19
|
+
*
|
|
20
|
+
* Extracted from the form's footer so the identity bar can own them. They are
|
|
21
|
+
* not ways to leave the form, so they never belonged next to Save; putting them
|
|
22
|
+
* in the bar's overflow menu is what let the footer disappear instead of
|
|
23
|
+
* becoming a full-width strip holding two icon buttons.
|
|
24
|
+
*/
|
|
25
|
+
export function useRecordActions<M extends Record<string, unknown>>({
|
|
26
|
+
collection,
|
|
27
|
+
path,
|
|
28
|
+
entity
|
|
29
|
+
}: UseRecordActionsParams<M>): EntityAction[] {
|
|
30
|
+
|
|
31
|
+
const { canCreate, canDelete } = usePermissions();
|
|
32
|
+
const customizationController = useCustomizationController();
|
|
33
|
+
|
|
34
|
+
// Rows shared through a junction: the delete action here removes the link,
|
|
35
|
+
// because that is what the server does for this path.
|
|
36
|
+
const childViewSource = useChildViewSource(path);
|
|
37
|
+
const isLinkedChildView = childViewSource?.kind === "relation" && childViewSource.mode === "linked";
|
|
38
|
+
|
|
39
|
+
return useMemo((): EntityAction[] => {
|
|
40
|
+
const customEntityActions = (collection.entityActions ?? [])
|
|
41
|
+
.map(action => resolveEntityAction(action, customizationController.entityActions))
|
|
42
|
+
.filter(Boolean) as EntityAction[];
|
|
43
|
+
|
|
44
|
+
const createEnabled = canCreate(collection, path);
|
|
45
|
+
const deleteEnabled = entity ? canDelete(collection, path, entity) : false;
|
|
46
|
+
const disableActions = collection.disableDefaultActions ?? [];
|
|
47
|
+
|
|
48
|
+
const actions: EntityAction[] = [];
|
|
49
|
+
if (createEnabled && !disableActions.includes("copy"))
|
|
50
|
+
actions.push(copyEntityAction);
|
|
51
|
+
if (deleteEnabled && !disableActions.includes("delete"))
|
|
52
|
+
actions.push(isLinkedChildView ? unlinkEntityAction : deleteEntityAction);
|
|
53
|
+
|
|
54
|
+
const merged = customEntityActions.length
|
|
55
|
+
? mergeEntityActions(actions, customEntityActions)
|
|
56
|
+
: actions;
|
|
57
|
+
|
|
58
|
+
return merged.filter(a => a.includeInForm === undefined || a.includeInForm);
|
|
59
|
+
}, [
|
|
60
|
+
canCreate,
|
|
61
|
+
canDelete,
|
|
62
|
+
collection,
|
|
63
|
+
path,
|
|
64
|
+
customizationController.entityActions?.length,
|
|
65
|
+
entity,
|
|
66
|
+
collection.disableDefaultActions,
|
|
67
|
+
isLinkedChildView
|
|
68
|
+
]);
|
|
69
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useEffect, useState } from "react";
|
|
2
2
|
import type { User } from "@rebasepro/types";
|
|
3
|
-
import { useAuthController, useRebaseClient } from "@rebasepro/app";
|
|
3
|
+
import { apiBaseOf, useAuthController, useRebaseClient } from "@rebasepro/app";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Resolves the auth users behind `userSelect` values.
|
|
@@ -31,12 +31,14 @@ queue: new Set() };
|
|
|
31
31
|
return cache;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
async function fetchUsers(
|
|
34
|
+
async function fetchUsers(apiBase: string, ids: string[], token: string | undefined): Promise<Map<string, User | null>> {
|
|
35
35
|
const resolved = new Map<string, User | null>();
|
|
36
36
|
for (const id of ids) resolved.set(id, null);
|
|
37
37
|
|
|
38
38
|
const params = new URLSearchParams({ ids: ids.join(",") });
|
|
39
|
-
|
|
39
|
+
// Same route as the selector — see the note there; `/api/users` is a 404,
|
|
40
|
+
// so every `userSelect` value rendered as a bare id.
|
|
41
|
+
const response = await fetch(`${apiBase}/admin/users?${params}`, {
|
|
40
42
|
headers: token ? { Authorization: `Bearer ${token}` } : {}
|
|
41
43
|
});
|
|
42
44
|
if (!response.ok) {
|
|
@@ -103,9 +105,12 @@ function requestUser(
|
|
|
103
105
|
* when it cannot be resolved.
|
|
104
106
|
*/
|
|
105
107
|
export function useResolvedUser(uid: string | undefined): User | null {
|
|
106
|
-
const client = useRebaseClient<{ baseUrl?: string }>();
|
|
108
|
+
const client = useRebaseClient<{ baseUrl?: string, apiPath?: string }>();
|
|
107
109
|
const { getAuthToken } = useAuthController();
|
|
108
|
-
|
|
110
|
+
/* The API prefix, not the bare origin: it keys the cache and is what a
|
|
111
|
+
request is built from, and a backend with a non-default `basePath`
|
|
112
|
+
serves neither at `/api`. */
|
|
113
|
+
const baseUrl = apiBaseOf(client);
|
|
109
114
|
|
|
110
115
|
const [user, setUser] = useState<User | null>(() =>
|
|
111
116
|
(baseUrl && uid) ? getCache(baseUrl).users.get(uid) ?? null : null);
|
|
@@ -25,6 +25,8 @@ import { BooleanPreview } from "./components/BooleanPreview";
|
|
|
25
25
|
import { NumberPropertyPreview } from "./property_previews/NumberPropertyPreview";
|
|
26
26
|
import { ErrorView } from "@rebasepro/app";
|
|
27
27
|
import { RelationPreview } from "./components/RelationPreview";
|
|
28
|
+
import { InlineEntityListPreview } from "./components/InlineEntityListPreview";
|
|
29
|
+
import { useIsNestedEntityPreview } from "../components/EntityPreviewNesting";
|
|
28
30
|
import { UserPreview } from "./components/UserPreview";
|
|
29
31
|
|
|
30
32
|
/**
|
|
@@ -34,6 +36,7 @@ export const PropertyPreview = React.memo(function PropertyPreview<P extends Pro
|
|
|
34
36
|
|
|
35
37
|
const authController = useAuthController();
|
|
36
38
|
const customizationController = useCustomizationController();
|
|
39
|
+
const nested = useIsNestedEntityPreview();
|
|
37
40
|
|
|
38
41
|
let content: React.ReactNode | any;
|
|
39
42
|
const {
|
|
@@ -181,9 +184,15 @@ export const PropertyPreview = React.memo(function PropertyPreview<P extends Pro
|
|
|
181
184
|
content = buildWrongValueType(propertyKey, property.type, value);
|
|
182
185
|
}
|
|
183
186
|
} else if (property.type === "date") {
|
|
184
|
-
|
|
187
|
+
// Not every caller comes through the data layer's Date coercion: the
|
|
188
|
+
// revision history renders raw API payloads, where a timestamp is still
|
|
189
|
+
// the string Postgres sent (`2026-06-19 17:20:33.032+02`). Requiring a
|
|
190
|
+
// `Date` instance turned every audit column in every history entry into
|
|
191
|
+
// a red "Unexpected value" box.
|
|
192
|
+
const date = asDate(value);
|
|
193
|
+
if (date) {
|
|
185
194
|
content = <DatePreview
|
|
186
|
-
date={
|
|
195
|
+
date={date}
|
|
187
196
|
mode={property.mode}
|
|
188
197
|
/>;
|
|
189
198
|
} else {
|
|
@@ -213,28 +222,34 @@ export const PropertyPreview = React.memo(function PropertyPreview<P extends Pro
|
|
|
213
222
|
content = <EmptyValue/>;
|
|
214
223
|
} else if (Array.isArray(value)) {
|
|
215
224
|
// Many-cardinality relation: value is an array of EntityRelation (or plain objects)
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
225
|
+
const relations = (value as unknown[])
|
|
226
|
+
.map(item => normalizeToEntityRelation(item, "relation"))
|
|
227
|
+
.filter(Boolean) as EntityRelation[];
|
|
228
|
+
const renderRelation = (entityRelation: EntityRelation, index: number) => <RelationPreview
|
|
229
|
+
key={`preview_rel_${propertyKey}_${index}`}
|
|
230
|
+
disabled={!property.relation}
|
|
231
|
+
previewProperties={property.admin?.previewProperties}
|
|
232
|
+
includeId={property.admin?.includeId}
|
|
233
|
+
includeEntityLink={property.admin?.includeEntityLink}
|
|
234
|
+
size={"small"}
|
|
235
|
+
relation={entityRelation}
|
|
236
|
+
textOnly={props.textOnly}
|
|
237
|
+
/>;
|
|
238
|
+
|
|
239
|
+
content = nested || props.textOnly
|
|
240
|
+
// See ArrayOfReferencesPreview: nested lists stay on one line.
|
|
241
|
+
? <InlineEntityListPreview items={relations}
|
|
242
|
+
renderItem={renderRelation}/>
|
|
243
|
+
: (
|
|
244
|
+
<div className="flex flex-col w-full gap-0.5">
|
|
245
|
+
{relations.map((entityRelation, index) => (
|
|
222
246
|
<div className="w-full"
|
|
223
|
-
key={`
|
|
224
|
-
|
|
225
|
-
disabled={!property.relation}
|
|
226
|
-
previewProperties={property.admin?.previewProperties}
|
|
227
|
-
includeId={property.admin?.includeId}
|
|
228
|
-
includeEntityLink={property.admin?.includeEntityLink}
|
|
229
|
-
size={"small"}
|
|
230
|
-
relation={entityRelation}
|
|
231
|
-
textOnly={props.textOnly}
|
|
232
|
-
/>
|
|
247
|
+
key={`preview_rel_wrapper_${propertyKey}_${index}`}>
|
|
248
|
+
{renderRelation(entityRelation, index)}
|
|
233
249
|
</div>
|
|
234
|
-
)
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
);
|
|
250
|
+
))}
|
|
251
|
+
</div>
|
|
252
|
+
);
|
|
238
253
|
} else {
|
|
239
254
|
// Single-cardinality relation
|
|
240
255
|
const relationValue = normalizeToEntityRelation(value, "relation");
|
|
@@ -276,6 +291,17 @@ export const PropertyPreview = React.memo(function PropertyPreview<P extends Pro
|
|
|
276
291
|
: content;
|
|
277
292
|
}, equal);
|
|
278
293
|
|
|
294
|
+
/**
|
|
295
|
+
* A `Date`, or a string/number that unambiguously names one. Returns null for
|
|
296
|
+
* anything else, so an actual type mismatch still reports as one.
|
|
297
|
+
*/
|
|
298
|
+
function asDate(value: unknown): Date | null {
|
|
299
|
+
if (value instanceof Date) return isNaN(value.getTime()) ? null : value;
|
|
300
|
+
if (typeof value !== "string" && typeof value !== "number") return null;
|
|
301
|
+
const parsed = new Date(value);
|
|
302
|
+
return isNaN(parsed.getTime()) ? null : parsed;
|
|
303
|
+
}
|
|
304
|
+
|
|
279
305
|
function buildWrongValueType(name: string | undefined, type: string, value: any) {
|
|
280
306
|
console.warn(`Unexpected value for property ${name}, of type ${type}`, value);
|
|
281
307
|
return (
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
import { cls } from "@rebasepro/ui";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* How many links a nested list shows before collapsing the rest into a count.
|
|
7
|
+
*/
|
|
8
|
+
const MAX_INLINE_ITEMS = 3;
|
|
9
|
+
|
|
10
|
+
export type InlineEntityListPreviewProps<T> = {
|
|
11
|
+
items: T[];
|
|
12
|
+
renderItem: (item: T, index: number) => React.ReactNode;
|
|
13
|
+
max?: number;
|
|
14
|
+
className?: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* One wrapping line of inline entity links, with the overflow collapsed into a
|
|
19
|
+
* `+N`. Used for arrays of references or relations that are nested inside
|
|
20
|
+
* another preview, where stacking full-width cards turns a card into a wall.
|
|
21
|
+
*
|
|
22
|
+
* @group Preview components
|
|
23
|
+
*/
|
|
24
|
+
export function InlineEntityListPreview<T>({
|
|
25
|
+
items,
|
|
26
|
+
renderItem,
|
|
27
|
+
max = MAX_INLINE_ITEMS,
|
|
28
|
+
className
|
|
29
|
+
}: InlineEntityListPreviewProps<T>) {
|
|
30
|
+
|
|
31
|
+
if (!items || items.length === 0) return null;
|
|
32
|
+
|
|
33
|
+
const shown = items.slice(0, max);
|
|
34
|
+
const overflow = items.length - shown.length;
|
|
35
|
+
|
|
36
|
+
return <span className={cls("inline-flex flex-wrap items-center gap-x-2 gap-y-0.5 min-w-0 max-w-full", className)}>
|
|
37
|
+
{shown.map((item, index) => renderItem(item, index))}
|
|
38
|
+
{overflow > 0 && <span className={"text-text-secondary dark:text-text-secondary-dark shrink-0"}>
|
|
39
|
+
+{overflow}
|
|
40
|
+
</span>}
|
|
41
|
+
</span>;
|
|
42
|
+
}
|
|
@@ -8,9 +8,13 @@ import { Skeleton } from "@rebasepro/ui";
|
|
|
8
8
|
import { ErrorBoundary } from "@rebasepro/ui";
|
|
9
9
|
import { ErrorView } from "@rebasepro/app";
|
|
10
10
|
import { EntityPreviewBinding, EntityPreviewContainer } from "../../components/EntityPreviewBinding";
|
|
11
|
+
import {
|
|
12
|
+
InlineEntityPreview,
|
|
13
|
+
InlineEntityPreviewMissing,
|
|
14
|
+
InlineEntityPreviewSkeleton
|
|
15
|
+
} from "../../components/InlineEntityPreview";
|
|
16
|
+
import { useIsNestedEntityPreview } from "../../components/EntityPreviewNesting";
|
|
11
17
|
import { useCollectionRegistryController } from "../../hooks/navigation/contexts/CollectionRegistryContext";
|
|
12
|
-
import { getEntityTitlePropertyKeyForEntity } from "../../util/previews";
|
|
13
|
-
import { getValueInPath } from "@rebasepro/utils";
|
|
14
18
|
import type { AdminCollection } from "@rebasepro/admin-types";
|
|
15
19
|
|
|
16
20
|
export type ReferencePreviewProps = {
|
|
@@ -30,9 +34,10 @@ export type ReferencePreviewProps = {
|
|
|
30
34
|
*/
|
|
31
35
|
export const ReferencePreview = function ReferencePreview(props: ReferencePreviewProps) {
|
|
32
36
|
const reference = props.reference;
|
|
37
|
+
const nested = useIsNestedEntityPreview();
|
|
33
38
|
if (!(typeof reference === "object" && "isEntityReference" in reference && reference.isEntityReference())) {
|
|
34
39
|
console.warn("Reference preview received value of type", typeof reference);
|
|
35
|
-
if (props.textOnly) {
|
|
40
|
+
if (props.textOnly || nested) {
|
|
36
41
|
return <span>{String(reference)}</span>;
|
|
37
42
|
}
|
|
38
43
|
return <EntityPreviewContainer
|
|
@@ -62,6 +67,7 @@ function ReferencePreviewInternalInner({
|
|
|
62
67
|
collection
|
|
63
68
|
}: ReferencePreviewProps & { collection?: AdminCollection }) {
|
|
64
69
|
const ResolvedMissingReference = useComponentOverride("Entity.MissingReference", DefaultMissingReference);
|
|
70
|
+
const nested = useIsNestedEntityPreview();
|
|
65
71
|
|
|
66
72
|
if (!collection) {
|
|
67
73
|
if (ResolvedMissingReference !== DefaultMissingReference) {
|
|
@@ -70,6 +76,10 @@ function ReferencePreviewInternalInner({
|
|
|
70
76
|
if (textOnly) {
|
|
71
77
|
return <span>{reference.path}</span>;
|
|
72
78
|
}
|
|
79
|
+
if (nested) {
|
|
80
|
+
return <InlineEntityPreviewMissing label={String(reference.id)}
|
|
81
|
+
tooltip={`Collection not found: ${reference.path}`}/>;
|
|
82
|
+
}
|
|
73
83
|
return <EntityPreviewContainer
|
|
74
84
|
onClick={onClick}
|
|
75
85
|
size={size ?? "medium"}>
|
|
@@ -130,6 +140,11 @@ function ReferencePreviewExisting<M extends Record<string, unknown> = Record<str
|
|
|
130
140
|
|
|
131
141
|
const ResolvedEntityPreview = useComponentOverride("EntityPreview", EntityPreviewBinding);
|
|
132
142
|
const customizationController = useCustomizationController();
|
|
143
|
+
const nested = useIsNestedEntityPreview();
|
|
144
|
+
|
|
145
|
+
// Nested inside another preview, or filling a title slot: one line of text,
|
|
146
|
+
// not a second card. See {@link InlineEntityPreview}.
|
|
147
|
+
const inline = nested || Boolean(textOnly);
|
|
133
148
|
|
|
134
149
|
const {
|
|
135
150
|
entity,
|
|
@@ -157,8 +172,9 @@ function ReferencePreviewExisting<M extends Record<string, unknown> = Record<str
|
|
|
157
172
|
tooltip={reference.path}/>;
|
|
158
173
|
}
|
|
159
174
|
if (body) {
|
|
160
|
-
if (
|
|
161
|
-
return <
|
|
175
|
+
if (inline) {
|
|
176
|
+
return <InlineEntityPreviewMissing label={String(reference.id)}
|
|
177
|
+
tooltip={reference.pathWithId}/>;
|
|
162
178
|
}
|
|
163
179
|
|
|
164
180
|
return (
|
|
@@ -171,8 +187,8 @@ function ReferencePreviewExisting<M extends Record<string, unknown> = Record<str
|
|
|
171
187
|
}
|
|
172
188
|
|
|
173
189
|
if (dataLoading && !usedEntity) {
|
|
174
|
-
if (
|
|
175
|
-
return <
|
|
190
|
+
if (inline) {
|
|
191
|
+
return <InlineEntityPreviewSkeleton/>;
|
|
176
192
|
}
|
|
177
193
|
return (
|
|
178
194
|
<EntityPreviewContainer onClick={disabled ? undefined : onClick}
|
|
@@ -184,8 +200,9 @@ function ReferencePreviewExisting<M extends Record<string, unknown> = Record<str
|
|
|
184
200
|
}
|
|
185
201
|
|
|
186
202
|
if (!usedEntity) {
|
|
187
|
-
if (
|
|
188
|
-
return <
|
|
203
|
+
if (inline) {
|
|
204
|
+
return <InlineEntityPreviewMissing label={String(reference.id)}
|
|
205
|
+
tooltip={"Entity not found"}/>;
|
|
189
206
|
}
|
|
190
207
|
return (
|
|
191
208
|
<EntityPreviewContainer onClick={disabled ? undefined : onClick}
|
|
@@ -196,11 +213,14 @@ function ReferencePreviewExisting<M extends Record<string, unknown> = Record<str
|
|
|
196
213
|
);
|
|
197
214
|
}
|
|
198
215
|
|
|
199
|
-
if (
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
216
|
+
if (inline) {
|
|
217
|
+
return <InlineEntityPreview entity={usedEntity}
|
|
218
|
+
collection={collection}
|
|
219
|
+
disabled={disabled}
|
|
220
|
+
onClick={onClick}
|
|
221
|
+
// In a title slot the row itself is the click target; only a
|
|
222
|
+
// preview nested inside a card opens its own side panel.
|
|
223
|
+
includeEntityLink={!textOnly && includeEntityLink !== false}/>;
|
|
204
224
|
}
|
|
205
225
|
|
|
206
226
|
return <ResolvedEntityPreview size={size}
|