@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
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
import { Entity } from "@rebasepro/types";
|
|
4
|
+
import { cls, LinkIcon, Skeleton, Tooltip } from "@rebasepro/ui";
|
|
5
|
+
import { useAnalyticsController } from "@rebasepro/app";
|
|
6
|
+
import { getValueInPath } from "@rebasepro/utils";
|
|
7
|
+
import type { AdminCollection } from "@rebasepro/admin-types";
|
|
8
|
+
|
|
9
|
+
import { getEntityTitlePropertyKeyForEntity } from "../util/previews";
|
|
10
|
+
import { useCollectionRegistryController } from "../hooks/navigation/contexts/CollectionRegistryContext";
|
|
11
|
+
import { useSidePanel } from "../hooks/useSidePanel";
|
|
12
|
+
|
|
13
|
+
export type InlineEntityPreviewProps = {
|
|
14
|
+
entity: Entity<any>;
|
|
15
|
+
collection?: AdminCollection;
|
|
16
|
+
/** Overrides the resolved title. */
|
|
17
|
+
label?: string;
|
|
18
|
+
disabled?: boolean;
|
|
19
|
+
/** When set, takes over the click. Otherwise the row opens the side panel. */
|
|
20
|
+
onClick?: (e: React.SyntheticEvent) => void;
|
|
21
|
+
/** Set to false to render text only, with no click target. */
|
|
22
|
+
includeEntityLink?: boolean;
|
|
23
|
+
/** Icon size in px. Defaults to a hair under the surrounding text. */
|
|
24
|
+
iconSize?: number;
|
|
25
|
+
className?: string;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const inlineMixin = "inline-flex items-center gap-1 min-w-0 max-w-full align-middle";
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* An entity rendered as one line of text: a small link glyph that marks it as
|
|
32
|
+
* pointing elsewhere, then the target's title in whatever typography surrounds
|
|
33
|
+
* it.
|
|
34
|
+
*
|
|
35
|
+
* This is what a reference or relation renders as when it is *not* the subject
|
|
36
|
+
* of its slot — nested inside another entity preview, or filling a title slot.
|
|
37
|
+
* The card treatment (image, id, preview properties, side-panel button) only
|
|
38
|
+
* makes sense at the top level; repeating it one level down produces a bordered
|
|
39
|
+
* box inside a bordered box, each with its own action buttons.
|
|
40
|
+
*
|
|
41
|
+
* @group Preview components
|
|
42
|
+
*/
|
|
43
|
+
export function InlineEntityPreview({
|
|
44
|
+
entity,
|
|
45
|
+
collection: collectionProp,
|
|
46
|
+
label,
|
|
47
|
+
disabled,
|
|
48
|
+
onClick,
|
|
49
|
+
includeEntityLink = true,
|
|
50
|
+
iconSize = 12,
|
|
51
|
+
className
|
|
52
|
+
}: InlineEntityPreviewProps) {
|
|
53
|
+
|
|
54
|
+
const collectionRegistryController = useCollectionRegistryController();
|
|
55
|
+
const analyticsController = useAnalyticsController();
|
|
56
|
+
const sidePanelController = useSidePanel();
|
|
57
|
+
|
|
58
|
+
const collection = collectionProp ?? collectionRegistryController.getCollection(entity.path);
|
|
59
|
+
|
|
60
|
+
const titlePropertyKey = collection
|
|
61
|
+
? getEntityTitlePropertyKeyForEntity(collection, entity.values, entity.id)
|
|
62
|
+
: undefined;
|
|
63
|
+
const titleValue = titlePropertyKey ? getValueInPath(entity.values, titlePropertyKey) : undefined;
|
|
64
|
+
const resolvedLabel = label
|
|
65
|
+
?? (titleValue !== undefined && titleValue !== null && String(titleValue).trim().length > 0
|
|
66
|
+
? String(titleValue)
|
|
67
|
+
: String(entity.id));
|
|
68
|
+
|
|
69
|
+
const interactive = !disabled && (onClick !== undefined || (includeEntityLink && collection !== undefined));
|
|
70
|
+
|
|
71
|
+
const handleClick = (event: React.SyntheticEvent) => {
|
|
72
|
+
if (!interactive) return;
|
|
73
|
+
// This sits inside a container with its own click handler (the parent
|
|
74
|
+
// card opens the parent entity); a click here is about the target.
|
|
75
|
+
event.preventDefault();
|
|
76
|
+
event.stopPropagation();
|
|
77
|
+
if (onClick) {
|
|
78
|
+
onClick(event);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
analyticsController.onAnalyticsEvent?.("entity_click_from_reference", {
|
|
82
|
+
path: entity.path,
|
|
83
|
+
entityId: entity.id
|
|
84
|
+
});
|
|
85
|
+
sidePanelController.open({
|
|
86
|
+
entityId: entity.id,
|
|
87
|
+
path: entity.path,
|
|
88
|
+
collection,
|
|
89
|
+
updateUrl: true
|
|
90
|
+
});
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const content = <span
|
|
94
|
+
tabIndex={interactive ? 0 : undefined}
|
|
95
|
+
role={interactive ? "button" : undefined}
|
|
96
|
+
onClick={interactive ? handleClick : undefined}
|
|
97
|
+
onKeyDown={interactive
|
|
98
|
+
? (event) => {
|
|
99
|
+
if (event.key === "Enter" || event.key === " ") handleClick(event);
|
|
100
|
+
}
|
|
101
|
+
: undefined}
|
|
102
|
+
className={cls(
|
|
103
|
+
inlineMixin,
|
|
104
|
+
interactive ? "cursor-pointer hover:underline underline-offset-2" : "",
|
|
105
|
+
className)}>
|
|
106
|
+
<LinkIcon size={iconSize}
|
|
107
|
+
className={"shrink-0 opacity-40"}/>
|
|
108
|
+
<span className={"truncate"}>{resolvedLabel}</span>
|
|
109
|
+
</span>;
|
|
110
|
+
|
|
111
|
+
if (!interactive) return content;
|
|
112
|
+
|
|
113
|
+
return <Tooltip title={`See details for ${resolvedLabel}`}
|
|
114
|
+
asChild={true}>
|
|
115
|
+
{content}
|
|
116
|
+
</Tooltip>;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Placeholder shown while the target of an inline reference is being fetched.
|
|
121
|
+
*/
|
|
122
|
+
export function InlineEntityPreviewSkeleton({ className }: { className?: string }) {
|
|
123
|
+
return <span className={cls(inlineMixin, className)}>
|
|
124
|
+
<LinkIcon size={12}
|
|
125
|
+
className={"shrink-0 opacity-40"}/>
|
|
126
|
+
<Skeleton className={"w-20 h-3"}/>
|
|
127
|
+
</span>;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Shown when the target of an inline reference cannot be resolved — a deleted
|
|
132
|
+
* row, or a collection that is not registered. Keeps the line readable instead
|
|
133
|
+
* of dropping an error box into the middle of a card.
|
|
134
|
+
*/
|
|
135
|
+
export function InlineEntityPreviewMissing({
|
|
136
|
+
label,
|
|
137
|
+
tooltip,
|
|
138
|
+
className
|
|
139
|
+
}: { label: string, tooltip?: string, className?: string }) {
|
|
140
|
+
const content = <span
|
|
141
|
+
className={cls(inlineMixin, "text-text-secondary dark:text-text-secondary-dark", className)}>
|
|
142
|
+
<LinkIcon size={12}
|
|
143
|
+
className={"shrink-0 opacity-40"}/>
|
|
144
|
+
<span className={"truncate italic"}>{label}</span>
|
|
145
|
+
</span>;
|
|
146
|
+
if (!tooltip) return content;
|
|
147
|
+
return <Tooltip title={tooltip}
|
|
148
|
+
asChild={true}>{content}</Tooltip>;
|
|
149
|
+
}
|
|
@@ -38,6 +38,7 @@ import { SidePanelProvider } from "./SidePanelProvider";
|
|
|
38
38
|
// Collection editor internals — used when collectionEditor is enabled
|
|
39
39
|
import { useLocalCollectionsConfigController } from "../collection_editor/useLocalCollectionsConfigController";
|
|
40
40
|
import { ConfigControllerProvider } from "../collection_editor/ConfigControllerProvider";
|
|
41
|
+
import { useCollectionsConfigController } from "../collection_editor/useCollectionsConfigController";
|
|
41
42
|
|
|
42
43
|
// Lazy-load the schema view — only fetched when studio schema tool is active
|
|
43
44
|
const CollectionsStudioView = lazy(() =>
|
|
@@ -168,12 +169,19 @@ export function RebaseNavigation({ children }: RebaseNavigationProps) {
|
|
|
168
169
|
}
|
|
169
170
|
}, [dataSources]);
|
|
170
171
|
|
|
172
|
+
// `readOnly` is left undefined unless the developer set it: the controller
|
|
173
|
+
// asks the backend whether its schema editor will accept a write, which is
|
|
174
|
+
// the only process that knows. Deciding it here from `process.env.NODE_ENV`
|
|
175
|
+
// — the *frontend bundle's* build mode — is what made the editor offer
|
|
176
|
+
// itself against production backends, `baas` projects and servers without
|
|
177
|
+
// `ts-morph`, and turned every save into a bare 404.
|
|
171
178
|
const internalConfigController = useLocalCollectionsConfigController(
|
|
172
179
|
rebaseClient,
|
|
173
180
|
resolvedCollections,
|
|
174
181
|
collectionEditorEnabled ? {
|
|
175
|
-
readOnly: collectionEditorOptions?.readOnly
|
|
176
|
-
getAuthToken: collectionEditorOptions?.getAuthToken ?? authController?.getAuthToken
|
|
182
|
+
readOnly: collectionEditorOptions?.readOnly,
|
|
183
|
+
getAuthToken: collectionEditorOptions?.getAuthToken ?? authController?.getAuthToken,
|
|
184
|
+
authKey: authController?.user?.uid ?? null
|
|
177
185
|
} : { readOnly: true }
|
|
178
186
|
);
|
|
179
187
|
|
|
@@ -189,13 +197,22 @@ export function RebaseNavigation({ children }: RebaseNavigationProps) {
|
|
|
189
197
|
group: "Database",
|
|
190
198
|
icon: "LayoutList",
|
|
191
199
|
nestedRoutes: true,
|
|
200
|
+
// Reads the controller from context at render time rather than
|
|
201
|
+
// capturing it here. A view is a React *element*, and the
|
|
202
|
+
// navigation state controller resolves the view list once and holds
|
|
203
|
+
// on to the elements it resolved — so a controller passed as a prop
|
|
204
|
+
// is frozen at whatever it was on the first render, forever. That
|
|
205
|
+
// hid every later change: the editor stayed writable after the
|
|
206
|
+
// backend said it was read-only, and the collection list never
|
|
207
|
+
// refreshed. Everything else in the editor already reads this from
|
|
208
|
+
// `ConfigControllerProvider`, which wraps this subtree.
|
|
192
209
|
view: (
|
|
193
210
|
<Suspense fallback={<CircularProgressCenter/>}>
|
|
194
|
-
<
|
|
211
|
+
<CollectionsStudioViewFromContext/>
|
|
195
212
|
</Suspense>
|
|
196
213
|
)
|
|
197
214
|
};
|
|
198
|
-
}, [collectionEditorEnabled, registry.studioConfig
|
|
215
|
+
}, [collectionEditorEnabled, registry.studioConfig]);
|
|
199
216
|
|
|
200
217
|
const devViews = useMemo(() => {
|
|
201
218
|
const base = registry.studioConfig?.devViews ?? [];
|
|
@@ -275,6 +292,7 @@ export function RebaseNavigation({ children }: RebaseNavigationProps) {
|
|
|
275
292
|
collectionRegistryController={collectionRegistryController}
|
|
276
293
|
urlController={urlController}
|
|
277
294
|
navigationStateController={navigationStateController}
|
|
295
|
+
canWriteToCodebase={!internalConfigController.readOnly}
|
|
278
296
|
/>
|
|
279
297
|
{children}
|
|
280
298
|
</SidePanelProvider>
|
|
@@ -301,6 +319,17 @@ export function RebaseNavigation({ children }: RebaseNavigationProps) {
|
|
|
301
319
|
return navigationContent;
|
|
302
320
|
}
|
|
303
321
|
|
|
322
|
+
/**
|
|
323
|
+
* The injected "Edit collections" view, bound to the live config controller.
|
|
324
|
+
*
|
|
325
|
+
* Exists so the element stored in the navigation state does not close over a
|
|
326
|
+
* controller instance — see the comment at its construction site.
|
|
327
|
+
*/
|
|
328
|
+
function CollectionsStudioViewFromContext() {
|
|
329
|
+
const configController = useCollectionsConfigController();
|
|
330
|
+
return <CollectionsStudioView configController={configController}/>;
|
|
331
|
+
}
|
|
332
|
+
|
|
304
333
|
/**
|
|
305
334
|
* Internal component that auto-registers admin controllers into the
|
|
306
335
|
* self-assembling Studio bridge. Must be rendered inside both the
|
|
@@ -309,14 +338,25 @@ export function RebaseNavigation({ children }: RebaseNavigationProps) {
|
|
|
309
338
|
function BridgeAutoRegistrar({
|
|
310
339
|
collectionRegistryController,
|
|
311
340
|
urlController,
|
|
312
|
-
navigationStateController
|
|
341
|
+
navigationStateController,
|
|
342
|
+
canWriteToCodebase
|
|
313
343
|
}: {
|
|
314
344
|
collectionRegistryController: CollectionRegistryController;
|
|
315
345
|
urlController: UrlController;
|
|
316
346
|
navigationStateController: NavigationStateController;
|
|
347
|
+
canWriteToCodebase: boolean;
|
|
317
348
|
}) {
|
|
318
349
|
useBridgeRegistration("collectionRegistry", collectionRegistryController);
|
|
319
350
|
useBridgeRegistration("urlController", urlController);
|
|
320
351
|
useBridgeRegistration("navigationState", navigationStateController);
|
|
352
|
+
|
|
353
|
+
// Studio tools that would write into the project's collection source — the
|
|
354
|
+
// RLS editor's policy save for a mapped table, its "Import to codebase" —
|
|
355
|
+
// ask the bridge whether that write can land. The default is `true`, which
|
|
356
|
+
// is what an admin panel sitting next to its own `collectionsDir` assumes;
|
|
357
|
+
// it is wrong for exactly the same backends that make the collection editor
|
|
358
|
+
// read-only, so the two now answer from one source.
|
|
359
|
+
const capabilities = useMemo(() => ({ codebase: canWriteToCodebase }), [canWriteToCodebase]);
|
|
360
|
+
useBridgeRegistration("capabilities", capabilities);
|
|
321
361
|
return null;
|
|
322
362
|
}
|
|
@@ -3,7 +3,7 @@ import type { Property, WhereFilterOp } from "@rebasepro/types";
|
|
|
3
3
|
import React, { useCallback, useEffect, useMemo, useRef } from "react";
|
|
4
4
|
import { Entity, FilterValues } from "@rebasepro/types";
|
|
5
5
|
import { CollectionSize, EntityTableController, SelectedCellProps, AdminCollection } from "@rebasepro/admin-types";
|
|
6
|
-
import { CellRendererParams,
|
|
6
|
+
import { CellRendererParams, VirtualTable, VirtualTableColumn, VirtualTableFilterValues, OnRowClickParams } from "@rebasepro/ui";
|
|
7
7
|
import { DEFAULT_PAGE_SIZE, DataCollectionTableController, OnCellValueChange, OnColumnResizeParams } from "@rebasepro/app";
|
|
8
8
|
import { FilterFormFieldProps } from "@rebasepro/ui";
|
|
9
9
|
import { useOutsideAlerter } from "@rebasepro/ui";
|
|
@@ -270,7 +270,7 @@ export const SelectableTable = function SelectableTable<M extends Record<string,
|
|
|
270
270
|
<div className="h-full w-full flex flex-col bg-white dark:bg-surface-900"
|
|
271
271
|
ref={ref}>
|
|
272
272
|
|
|
273
|
-
<
|
|
273
|
+
<VirtualTable
|
|
274
274
|
data={data as unknown as Record<string, unknown>[]}
|
|
275
275
|
columns={columns}
|
|
276
276
|
cellRenderer={(props) => cellRenderer(props as unknown as CellRendererParams<Entity<M>>)}
|
|
@@ -8,7 +8,7 @@ import { IconButton, Maximize2Icon, XIcon } from "@rebasepro/ui";
|
|
|
8
8
|
import { EditViewBinding } from "./EditViewBinding";
|
|
9
9
|
import { DetailViewBinding } from "./DetailViewBinding";
|
|
10
10
|
import { useSideDialogContext } from "./SideDialogs";
|
|
11
|
-
import { useNavigate } from "react-router
|
|
11
|
+
import { useNavigate } from "react-router";
|
|
12
12
|
import { saveEntityToMemoryCache, useComponentOverride } from "@rebasepro/app";
|
|
13
13
|
import { useCollectionRegistryController } from "../hooks/navigation/contexts/CollectionRegistryContext";
|
|
14
14
|
import { useSidePanel } from "../hooks/useSidePanel";
|
|
@@ -193,16 +193,19 @@ entityId }
|
|
|
193
193
|
parentCollectionSlugs={parentCollectionSlugs}
|
|
194
194
|
parentEntityIds={parentEntityIds}
|
|
195
195
|
onEditClick={openEditView}
|
|
196
|
+
barActionsStart={
|
|
197
|
+
// At the bar's leading edge, where the breadcrumb
|
|
198
|
+
// starts: the corner of the panel it closes.
|
|
199
|
+
<IconButton
|
|
200
|
+
className="self-center"
|
|
201
|
+
size={"small"}
|
|
202
|
+
onClick={onClose}>
|
|
203
|
+
<XIcon/>
|
|
204
|
+
</IconButton>}
|
|
196
205
|
barActions={({
|
|
197
206
|
status,
|
|
198
207
|
values
|
|
199
208
|
}) => <div className="flex gap-1">
|
|
200
|
-
<IconButton
|
|
201
|
-
className="self-center"
|
|
202
|
-
size={"small"}
|
|
203
|
-
onClick={onClose}>
|
|
204
|
-
<XIcon/>
|
|
205
|
-
</IconButton>
|
|
206
209
|
{allowFullScreen && <IconButton
|
|
207
210
|
className="self-center"
|
|
208
211
|
size={"small"}
|
|
@@ -242,16 +245,19 @@ entityId }
|
|
|
242
245
|
onValuesModified={onValuesModified}
|
|
243
246
|
onSaved={onUpdate}
|
|
244
247
|
navigateBack={closeEditView}
|
|
248
|
+
barActionsStart={
|
|
249
|
+
// At the bar's leading edge, where the breadcrumb
|
|
250
|
+
// starts: the corner of the panel it closes.
|
|
251
|
+
<IconButton
|
|
252
|
+
className="self-center"
|
|
253
|
+
size={"small"}
|
|
254
|
+
onClick={onClose}>
|
|
255
|
+
<XIcon/>
|
|
256
|
+
</IconButton>}
|
|
245
257
|
barActions={({
|
|
246
258
|
status,
|
|
247
259
|
values
|
|
248
260
|
}) => <div className="flex gap-1">
|
|
249
|
-
<IconButton
|
|
250
|
-
className="self-center"
|
|
251
|
-
size={"small"}
|
|
252
|
-
onClick={onClose}>
|
|
253
|
-
<XIcon/>
|
|
254
|
-
</IconButton>
|
|
255
261
|
{allowFullScreen && <IconButton
|
|
256
262
|
className="self-center"
|
|
257
263
|
size={"small"}
|
|
@@ -21,7 +21,7 @@ import * as React from "react";
|
|
|
21
21
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
22
22
|
import { Command as CommandPrimitive } from "cmdk";
|
|
23
23
|
import { User } from "@rebasepro/types";
|
|
24
|
-
import { useRebaseClient, useAuthController, UserDisplay } from "@rebasepro/app";
|
|
24
|
+
import { apiBaseOf, useRebaseClient, useAuthController, UserDisplay } from "@rebasepro/app";
|
|
25
25
|
import { EmptyValue } from "../preview";
|
|
26
26
|
|
|
27
27
|
interface UserSelectorItem {
|
|
@@ -37,7 +37,7 @@ const DEFAULT_PAGE_SIZE = 10;
|
|
|
37
37
|
* Local hook to fetch users from the collection REST API with search and pagination.
|
|
38
38
|
*/
|
|
39
39
|
function useUserSelector({ pageSize = DEFAULT_PAGE_SIZE }: { pageSize?: number } = {}) {
|
|
40
|
-
const client = useRebaseClient<{ baseUrl?: string }>();
|
|
40
|
+
const client = useRebaseClient<{ baseUrl?: string, apiPath?: string }>();
|
|
41
41
|
const { getAuthToken } = useAuthController();
|
|
42
42
|
|
|
43
43
|
const [items, setItems] = useState<UserSelectorItem[]>([]);
|
|
@@ -48,7 +48,8 @@ function useUserSelector({ pageSize = DEFAULT_PAGE_SIZE }: { pageSize?: number }
|
|
|
48
48
|
const userCacheRef = useRef<Map<string, User>>(new Map());
|
|
49
49
|
|
|
50
50
|
const fetchUsers = useCallback(async (searchStr: string, offset: number, append: boolean) => {
|
|
51
|
-
|
|
51
|
+
const apiBase = apiBaseOf(client);
|
|
52
|
+
if (!apiBase) return;
|
|
52
53
|
setIsLoading(true);
|
|
53
54
|
try {
|
|
54
55
|
const token = await getAuthToken?.();
|
|
@@ -57,7 +58,12 @@ function useUserSelector({ pageSize = DEFAULT_PAGE_SIZE }: { pageSize?: number }
|
|
|
57
58
|
offset: String(offset)
|
|
58
59
|
});
|
|
59
60
|
if (searchStr) params.set("search", searchStr);
|
|
60
|
-
|
|
61
|
+
// `/admin/users`, not `/users`. `apiBaseOf` returns the API root
|
|
62
|
+
// (`/api`), and the users endpoint lives under the auth adapter's
|
|
63
|
+
// admin router — `/api/users` is a 404, which is why this list was
|
|
64
|
+
// always empty. The collection route `/api/data/users` would answer,
|
|
65
|
+
// but it serves the raw row, `passwordHash` included.
|
|
66
|
+
const response = await fetch(`${apiBase}/admin/users?${params}`, {
|
|
61
67
|
headers: token ? { Authorization: `Bearer ${token}` } : {}
|
|
62
68
|
});
|
|
63
69
|
if (!response.ok) throw new Error("Failed to fetch users");
|
|
@@ -65,7 +71,7 @@ function useUserSelector({ pageSize = DEFAULT_PAGE_SIZE }: { pageSize?: number }
|
|
|
65
71
|
const rows: Record<string, unknown>[] = data.data ?? data.users ?? [];
|
|
66
72
|
const newItems: UserSelectorItem[] = rows.map((r: Record<string, unknown>) => {
|
|
67
73
|
const user: User = {
|
|
68
|
-
uid: (r.id as string) ?? "",
|
|
74
|
+
uid: (r.uid as string) ?? (r.id as string) ?? "",
|
|
69
75
|
email: (r.email as string) ?? "",
|
|
70
76
|
displayName: (r.displayName as string) ?? (r.display_name as string) ?? null,
|
|
71
77
|
photoURL: (r.photoURL as string) ?? (r.photo_url as string) ?? null,
|
|
@@ -86,7 +92,7 @@ user };
|
|
|
86
92
|
} finally {
|
|
87
93
|
setIsLoading(false);
|
|
88
94
|
}
|
|
89
|
-
}, [client?.baseUrl, getAuthToken, pageSize]);
|
|
95
|
+
}, [client?.baseUrl, client?.apiPath, getAuthToken, pageSize]);
|
|
90
96
|
|
|
91
97
|
useEffect(() => {
|
|
92
98
|
offsetRef.current = 0;
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
import type { User } from "@rebasepro/types";
|
|
22
22
|
import type { EntityAction, UserCreationResult } from "@rebasepro/admin-types";
|
|
23
23
|
import {
|
|
24
|
+
apiBaseOf,
|
|
24
25
|
useAuthController,
|
|
25
26
|
useRebaseClient,
|
|
26
27
|
useSnackbarController,
|
|
@@ -246,7 +247,7 @@ export function ResetPasswordActionDialog({
|
|
|
246
247
|
open: boolean;
|
|
247
248
|
onClose: () => void;
|
|
248
249
|
}) {
|
|
249
|
-
const client = useRebaseClient<{ baseUrl?: string }>();
|
|
250
|
+
const client = useRebaseClient<{ baseUrl?: string, apiPath?: string }>();
|
|
250
251
|
const { getAuthToken } = useAuthController();
|
|
251
252
|
const snackbarController = useSnackbarController();
|
|
252
253
|
const { t } = useTranslation();
|
|
@@ -270,9 +271,9 @@ export function ResetPasswordActionDialog({
|
|
|
270
271
|
setLoading(true);
|
|
271
272
|
setError(null);
|
|
272
273
|
try {
|
|
273
|
-
const
|
|
274
|
+
const apiBase = apiBaseOf(client) ?? "";
|
|
274
275
|
const token = await getAuthToken?.();
|
|
275
|
-
const response = await fetch(`${
|
|
276
|
+
const response = await fetch(`${apiBase}/admin/users/${user.uid}/reset-password`, {
|
|
276
277
|
method: "POST",
|
|
277
278
|
headers: {
|
|
278
279
|
"Content-Type": "application/json",
|
|
@@ -95,7 +95,22 @@ export function EntityHistoryEntry({
|
|
|
95
95
|
|
|
96
96
|
const authController = useAuthController();
|
|
97
97
|
|
|
98
|
-
|
|
98
|
+
// The backend reports the whole row as changed, which includes the columns
|
|
99
|
+
// that cannot meaningfully change: a timestamp it stamps itself on every
|
|
100
|
+
// write, and the primary key, which is what identifies the row being
|
|
101
|
+
// revised. Every revision was listing `updated_at: <now>` and `id: <the id
|
|
102
|
+
// in the title bar>` around the one field the user actually edited.
|
|
103
|
+
const changedFields = React.useMemo(() => {
|
|
104
|
+
const all = entry.changed_fields;
|
|
105
|
+
if (!all || !collection) return all;
|
|
106
|
+
return all.filter(key => {
|
|
107
|
+
const property = getPropertyInPath(collection.properties, key);
|
|
108
|
+
if (!property) return true;
|
|
109
|
+
if ("isId" in property && property.isId) return false;
|
|
110
|
+
return !(property.type === "date" && "autoValue" in property && property.autoValue);
|
|
111
|
+
});
|
|
112
|
+
}, [entry.changed_fields, collection]);
|
|
113
|
+
|
|
99
114
|
const previousValues = entry.previous_values;
|
|
100
115
|
const updatedOn = new Date(entry.updated_at);
|
|
101
116
|
const updatedBy = entry.updated_by;
|
|
@@ -106,8 +121,8 @@ export function EntityHistoryEntry({
|
|
|
106
121
|
? currentUser
|
|
107
122
|
: undefined;
|
|
108
123
|
|
|
109
|
-
return <div className={"w-full flex flex-col gap-2
|
|
110
|
-
<div className={"
|
|
124
|
+
return <div className={"@container/histentry w-full flex flex-col gap-2"}>
|
|
125
|
+
<div className={"flex items-center flex-wrap gap-2"}>
|
|
111
126
|
<Typography variant={"body2"} color={"secondary"}>
|
|
112
127
|
{updatedOn.toLocaleString()}
|
|
113
128
|
</Typography>
|
|
@@ -148,15 +163,21 @@ export function EntityHistoryEntry({
|
|
|
148
163
|
{typeof valueInPath === "string" ? valueInPath : JSON.stringify(valueInPath)}
|
|
149
164
|
</Typography>;
|
|
150
165
|
|
|
166
|
+
// The key column used to be `min-w-[200px] w-1/5` beside a
|
|
167
|
+
// `w-4/5` value — 200px of label and 80% of the row, which
|
|
168
|
+
// adds up to more than the row in anything narrower than a
|
|
169
|
+
// full page. In the inspector that pushed every value off
|
|
170
|
+
// the right edge and wrapped ids one character per line.
|
|
171
|
+
// It stacks until the entry itself is wide enough.
|
|
151
172
|
return (
|
|
152
173
|
<div key={"ref_prev_" + key}
|
|
153
|
-
className="flex w-full my-1
|
|
174
|
+
className="flex flex-col @sm/histentry:flex-row @sm/histentry:items-baseline gap-x-4 gap-y-0.5 w-full my-1.5">
|
|
154
175
|
<Typography variant={"caption"}
|
|
155
176
|
color={"secondary"}
|
|
156
|
-
className="
|
|
177
|
+
className="shrink-0 break-all @sm/histentry:w-40 @sm/histentry:text-right @sm/histentry:break-normal @sm/histentry:truncate">
|
|
157
178
|
{key}
|
|
158
179
|
</Typography>
|
|
159
|
-
<div className="w-
|
|
180
|
+
<div className="min-w-0 flex-1 flex flex-col items-start gap-0.5">
|
|
160
181
|
{previousValueInPath !== undefined && !deepEqual(previousValueInPath, valueInPath) &&
|
|
161
182
|
<PreviousValueView previousValueInPath={previousValueInPath}
|
|
162
183
|
childProperty={childProperty as Property}
|
|
@@ -169,7 +190,7 @@ export function EntityHistoryEntry({
|
|
|
169
190
|
})}
|
|
170
191
|
|
|
171
192
|
{(!changedFields || changedFields.length === 0) && (
|
|
172
|
-
<Typography variant={"caption"} color={"secondary"}
|
|
193
|
+
<Typography variant={"caption"} color={"secondary"}>
|
|
173
194
|
{entry.action === "create" ? "Entity created" :
|
|
174
195
|
entry.action === "delete" ? "Entity deleted" : "No field changes recorded"}
|
|
175
196
|
</Typography>
|
|
@@ -4,7 +4,7 @@ import { useRef, useEffect } from "react";
|
|
|
4
4
|
import { cls, IconButton, Label, Tooltip, Typography } from "@rebasepro/ui";
|
|
5
5
|
import { ErrorBoundary, HistoryIcon } from "@rebasepro/ui";
|
|
6
6
|
import { EntityHistoryEntry } from "./EntityHistoryEntry";
|
|
7
|
-
import { useSnackbarController, useAuthController } from "@rebasepro/app";
|
|
7
|
+
import { useSnackbarController, useAuthController, useTranslation } from "@rebasepro/app";
|
|
8
8
|
import { ConfirmationDialog } from "@rebasepro/app";
|
|
9
9
|
import { useState } from "react";
|
|
10
10
|
import { useHistory } from "../../hooks/useHistory";
|
|
@@ -16,9 +16,14 @@ import { useHistory } from "../../hooks/useHistory";
|
|
|
16
16
|
export function EntityHistoryView<M extends Record<string, unknown>>({
|
|
17
17
|
entity,
|
|
18
18
|
collection,
|
|
19
|
-
formContext
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
formContext,
|
|
20
|
+
refreshToken
|
|
21
|
+
}: EntityCustomViewParams<M> & {
|
|
22
|
+
/** Bumped once per save, so the list picks up the revision it just made. */
|
|
23
|
+
refreshToken?: number;
|
|
24
|
+
}) {
|
|
25
|
+
|
|
26
|
+
const { t } = useTranslation();
|
|
22
27
|
const snackbarController = useSnackbarController();
|
|
23
28
|
const authController = useAuthController();
|
|
24
29
|
const dirty = formContext?.formex.dirty;
|
|
@@ -36,7 +41,8 @@ export function EntityHistoryView<M extends Record<string, unknown>>({
|
|
|
36
41
|
slug,
|
|
37
42
|
entityId,
|
|
38
43
|
enabled: !!entityId,
|
|
39
|
-
pageSize: 10
|
|
44
|
+
pageSize: 10,
|
|
45
|
+
refreshToken
|
|
40
46
|
});
|
|
41
47
|
|
|
42
48
|
const [revertHistoryId, setRevertHistoryId] = useState<string | undefined>();
|
|
@@ -72,7 +78,7 @@ export function EntityHistoryView<M extends Record<string, unknown>>({
|
|
|
72
78
|
|
|
73
79
|
if (!entity) {
|
|
74
80
|
return <div className="flex items-center justify-center h-full">
|
|
75
|
-
<Label>History is only available for existing
|
|
81
|
+
<Label>{t("history_new_entity") ?? "History is only available for existing records"}</Label>
|
|
76
82
|
</div>;
|
|
77
83
|
}
|
|
78
84
|
|
|
@@ -93,13 +99,13 @@ export function EntityHistoryView<M extends Record<string, unknown>>({
|
|
|
93
99
|
}
|
|
94
100
|
|
|
95
101
|
snackbarController.open({
|
|
96
|
-
message: "Reverted to selected version",
|
|
102
|
+
message: t("history_reverted") ?? "Reverted to selected version",
|
|
97
103
|
type: "info"
|
|
98
104
|
});
|
|
99
105
|
} catch (error) {
|
|
100
106
|
console.error("Error reverting entity:", error);
|
|
101
107
|
snackbarController.open({
|
|
102
|
-
message: "Error reverting entity",
|
|
108
|
+
message: t("history_revert_error") ?? "Error reverting entity",
|
|
103
109
|
type: "error"
|
|
104
110
|
});
|
|
105
111
|
} finally {
|
|
@@ -111,17 +117,16 @@ export function EntityHistoryView<M extends Record<string, unknown>>({
|
|
|
111
117
|
? entries.find(e => e.id === revertHistoryId)
|
|
112
118
|
: undefined;
|
|
113
119
|
|
|
120
|
+
// `p-8` plus a `mt-24` heading were sized for a full-width tab that no
|
|
121
|
+
// longer exists. In the inspector panel that was 96px of nothing above the
|
|
122
|
+
// first revision, and 32px of gutter on each side of a 448px column.
|
|
114
123
|
return <div
|
|
115
124
|
ref={containerRef}
|
|
116
|
-
className={cls("relative flex-1 h-full overflow-auto w-full flex flex-col gap-4 p-
|
|
125
|
+
className={cls("relative flex-1 h-full overflow-auto w-full flex flex-col gap-4 p-4 sm:p-5")}>
|
|
117
126
|
<div className="flex flex-col gap-2 max-w-6xl mx-auto w-full">
|
|
118
127
|
|
|
119
|
-
<Typography variant={"h5"} className={"mt-24 ml-4"}>
|
|
120
|
-
History
|
|
121
|
-
</Typography>
|
|
122
|
-
|
|
123
128
|
{isLoading && entries.length === 0 && (
|
|
124
|
-
<div className="flex flex-col gap-4
|
|
129
|
+
<div className="flex flex-col gap-4">
|
|
125
130
|
{[1, 2, 3].map(i => (
|
|
126
131
|
<div key={i} className="flex flex-col gap-2 animate-pulse">
|
|
127
132
|
<div className="h-4 w-48 bg-surface-200 dark:bg-surface-700 rounded"/>
|
|
@@ -132,28 +137,28 @@ export function EntityHistoryView<M extends Record<string, unknown>>({
|
|
|
132
137
|
)}
|
|
133
138
|
|
|
134
139
|
{!isLoading && entries.length === 0 && <>
|
|
135
|
-
<Label
|
|
136
|
-
No history available
|
|
140
|
+
<Label>
|
|
141
|
+
{t("history_empty") ?? "No history available"}
|
|
137
142
|
</Label>
|
|
138
|
-
<Typography variant={"caption"}
|
|
139
|
-
When you save a
|
|
143
|
+
<Typography variant={"caption"}>
|
|
144
|
+
{t("history_empty_description") ?? "When you save a record, a new version is created and stored in the history."}
|
|
140
145
|
</Typography>
|
|
141
146
|
</>}
|
|
142
147
|
|
|
143
148
|
{entries.map((entry) => (
|
|
144
|
-
<div key={entry.id} className="flex flex-
|
|
149
|
+
<div key={entry.id} className="flex flex-col gap-2 w-full">
|
|
145
150
|
<EntityHistoryEntry
|
|
146
151
|
entry={entry}
|
|
147
152
|
collection={collection}
|
|
148
153
|
size={"medium"}
|
|
149
154
|
actions={
|
|
150
|
-
<Tooltip title={"Revert to this version"}
|
|
155
|
+
<Tooltip title={t("history_revert") ?? "Revert to this version"}
|
|
151
156
|
className={"m-2 grow-0 self-start"}>
|
|
152
157
|
<IconButton
|
|
153
158
|
onClick={() => {
|
|
154
159
|
if (dirty) {
|
|
155
160
|
snackbarController.open({
|
|
156
|
-
message: "Please save or discard your changes before reverting",
|
|
161
|
+
message: t("history_revert_dirty") ?? "Please save or discard your changes before reverting",
|
|
157
162
|
type: "warning"
|
|
158
163
|
});
|
|
159
164
|
} else {
|
|
@@ -171,8 +176,8 @@ export function EntityHistoryView<M extends Record<string, unknown>>({
|
|
|
171
176
|
{/* Load more sentinel */}
|
|
172
177
|
{entries.length > 0 && (
|
|
173
178
|
<div ref={loadMoreRef} className="py-4 text-center">
|
|
174
|
-
{isLoading && <Label>
|
|
175
|
-
{!hasMore && entries.length > 10 && <Label>No more history
|
|
179
|
+
{isLoading && <Label>{t("loading")}</Label>}
|
|
180
|
+
{!hasMore && entries.length > 10 && <Label>{t("history_no_more") ?? "No more history"}</Label>}
|
|
176
181
|
</div>
|
|
177
182
|
)}
|
|
178
183
|
</div>
|
|
@@ -184,7 +189,7 @@ export function EntityHistoryView<M extends Record<string, unknown>>({
|
|
|
184
189
|
if (revertHistoryId) doRevert(revertHistoryId);
|
|
185
190
|
}}
|
|
186
191
|
onCancel={() => setRevertHistoryId(undefined)}
|
|
187
|
-
title={<Typography variant={"subtitle2"}>Revert data to this version
|
|
192
|
+
title={<Typography variant={"subtitle2"}>{t("history_revert_title") ?? "Revert data to this version?"}</Typography>}
|
|
188
193
|
body={revertEntry
|
|
189
194
|
? <div className="p-4">
|
|
190
195
|
<Typography variant={"caption"} color={"secondary"}>
|