@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
|
@@ -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";
|
|
@@ -19,6 +19,7 @@ export function EntityHistoryView<M extends Record<string, unknown>>({
|
|
|
19
19
|
formContext
|
|
20
20
|
}: EntityCustomViewParams<M>) {
|
|
21
21
|
|
|
22
|
+
const { t } = useTranslation();
|
|
22
23
|
const snackbarController = useSnackbarController();
|
|
23
24
|
const authController = useAuthController();
|
|
24
25
|
const dirty = formContext?.formex.dirty;
|
|
@@ -72,7 +73,7 @@ export function EntityHistoryView<M extends Record<string, unknown>>({
|
|
|
72
73
|
|
|
73
74
|
if (!entity) {
|
|
74
75
|
return <div className="flex items-center justify-center h-full">
|
|
75
|
-
<Label>History is only available for existing
|
|
76
|
+
<Label>{t("history_new_entity") ?? "History is only available for existing records"}</Label>
|
|
76
77
|
</div>;
|
|
77
78
|
}
|
|
78
79
|
|
|
@@ -93,13 +94,13 @@ export function EntityHistoryView<M extends Record<string, unknown>>({
|
|
|
93
94
|
}
|
|
94
95
|
|
|
95
96
|
snackbarController.open({
|
|
96
|
-
message: "Reverted to selected version",
|
|
97
|
+
message: t("history_reverted") ?? "Reverted to selected version",
|
|
97
98
|
type: "info"
|
|
98
99
|
});
|
|
99
100
|
} catch (error) {
|
|
100
101
|
console.error("Error reverting entity:", error);
|
|
101
102
|
snackbarController.open({
|
|
102
|
-
message: "Error reverting entity",
|
|
103
|
+
message: t("history_revert_error") ?? "Error reverting entity",
|
|
103
104
|
type: "error"
|
|
104
105
|
});
|
|
105
106
|
} finally {
|
|
@@ -111,17 +112,16 @@ export function EntityHistoryView<M extends Record<string, unknown>>({
|
|
|
111
112
|
? entries.find(e => e.id === revertHistoryId)
|
|
112
113
|
: undefined;
|
|
113
114
|
|
|
115
|
+
// `p-8` plus a `mt-24` heading were sized for a full-width tab that no
|
|
116
|
+
// longer exists. In the inspector panel that was 96px of nothing above the
|
|
117
|
+
// first revision, and 32px of gutter on each side of a 448px column.
|
|
114
118
|
return <div
|
|
115
119
|
ref={containerRef}
|
|
116
|
-
className={cls("relative flex-1 h-full overflow-auto w-full flex flex-col gap-4 p-
|
|
120
|
+
className={cls("relative flex-1 h-full overflow-auto w-full flex flex-col gap-4 p-4 sm:p-5")}>
|
|
117
121
|
<div className="flex flex-col gap-2 max-w-6xl mx-auto w-full">
|
|
118
122
|
|
|
119
|
-
<Typography variant={"h5"} className={"mt-24 ml-4"}>
|
|
120
|
-
History
|
|
121
|
-
</Typography>
|
|
122
|
-
|
|
123
123
|
{isLoading && entries.length === 0 && (
|
|
124
|
-
<div className="flex flex-col gap-4
|
|
124
|
+
<div className="flex flex-col gap-4">
|
|
125
125
|
{[1, 2, 3].map(i => (
|
|
126
126
|
<div key={i} className="flex flex-col gap-2 animate-pulse">
|
|
127
127
|
<div className="h-4 w-48 bg-surface-200 dark:bg-surface-700 rounded"/>
|
|
@@ -132,28 +132,28 @@ export function EntityHistoryView<M extends Record<string, unknown>>({
|
|
|
132
132
|
)}
|
|
133
133
|
|
|
134
134
|
{!isLoading && entries.length === 0 && <>
|
|
135
|
-
<Label
|
|
136
|
-
No history available
|
|
135
|
+
<Label>
|
|
136
|
+
{t("history_empty") ?? "No history available"}
|
|
137
137
|
</Label>
|
|
138
|
-
<Typography variant={"caption"}
|
|
139
|
-
When you save a
|
|
138
|
+
<Typography variant={"caption"}>
|
|
139
|
+
{t("history_empty_description") ?? "When you save a record, a new version is created and stored in the history."}
|
|
140
140
|
</Typography>
|
|
141
141
|
</>}
|
|
142
142
|
|
|
143
143
|
{entries.map((entry) => (
|
|
144
|
-
<div key={entry.id} className="flex flex-
|
|
144
|
+
<div key={entry.id} className="flex flex-col gap-2 w-full">
|
|
145
145
|
<EntityHistoryEntry
|
|
146
146
|
entry={entry}
|
|
147
147
|
collection={collection}
|
|
148
148
|
size={"medium"}
|
|
149
149
|
actions={
|
|
150
|
-
<Tooltip title={"Revert to this version"}
|
|
150
|
+
<Tooltip title={t("history_revert") ?? "Revert to this version"}
|
|
151
151
|
className={"m-2 grow-0 self-start"}>
|
|
152
152
|
<IconButton
|
|
153
153
|
onClick={() => {
|
|
154
154
|
if (dirty) {
|
|
155
155
|
snackbarController.open({
|
|
156
|
-
message: "Please save or discard your changes before reverting",
|
|
156
|
+
message: t("history_revert_dirty") ?? "Please save or discard your changes before reverting",
|
|
157
157
|
type: "warning"
|
|
158
158
|
});
|
|
159
159
|
} else {
|
|
@@ -171,8 +171,8 @@ export function EntityHistoryView<M extends Record<string, unknown>>({
|
|
|
171
171
|
{/* Load more sentinel */}
|
|
172
172
|
{entries.length > 0 && (
|
|
173
173
|
<div ref={loadMoreRef} className="py-4 text-center">
|
|
174
|
-
{isLoading && <Label>
|
|
175
|
-
{!hasMore && entries.length > 10 && <Label>No more history
|
|
174
|
+
{isLoading && <Label>{t("loading")}</Label>}
|
|
175
|
+
{!hasMore && entries.length > 10 && <Label>{t("history_no_more") ?? "No more history"}</Label>}
|
|
176
176
|
</div>
|
|
177
177
|
)}
|
|
178
178
|
</div>
|
|
@@ -184,7 +184,7 @@ export function EntityHistoryView<M extends Record<string, unknown>>({
|
|
|
184
184
|
if (revertHistoryId) doRevert(revertHistoryId);
|
|
185
185
|
}}
|
|
186
186
|
onCancel={() => setRevertHistoryId(undefined)}
|
|
187
|
-
title={<Typography variant={"subtitle2"}>Revert data to this version
|
|
187
|
+
title={<Typography variant={"subtitle2"}>{t("history_revert_title") ?? "Revert data to this version?"}</Typography>}
|
|
188
188
|
body={revertEntry
|
|
189
189
|
? <div className="p-4">
|
|
190
190
|
<Typography variant={"caption"} color={"secondary"}>
|