@rebasepro/admin 0.12.1-canary.gf4240e3 → 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/dist/{CollectionEditorDialog-Dhn0S4FT.js → CollectionEditorDialog-CjXkWZ64.js} +3 -3
- package/dist/{CollectionEditorDialog-Dhn0S4FT.js.map → CollectionEditorDialog-CjXkWZ64.js.map} +1 -1
- package/dist/{PropertyEditView-JPN12E-0.js → PropertyEditView-W8I3YV89.js} +2 -2
- package/dist/{PropertyEditView-JPN12E-0.js.map → PropertyEditView-W8I3YV89.js.map} +1 -1
- package/dist/{RouterCollectionsStudioView-Cv0tAxtu.js → RouterCollectionsStudioView-BsLTNoqX.js} +4 -4
- package/dist/{RouterCollectionsStudioView-Cv0tAxtu.js.map → RouterCollectionsStudioView-BsLTNoqX.js.map} +1 -1
- package/dist/collection_editor_ui.js +4 -4
- package/dist/components/EntityInspector.d.ts +9 -3
- package/dist/components/history/EntityHistoryView.d.ts +4 -1
- package/dist/{export-DghFk-W0.js → export-J79rMoNj.js} +2 -2
- package/dist/{export-DghFk-W0.js.map → export-J79rMoNj.js.map} +1 -1
- package/dist/{history-c-iE7d4k.js → history-D7L_JlLO.js} +5 -4
- package/dist/history-D7L_JlLO.js.map +1 -0
- package/dist/hooks/useHistory.d.ts +7 -0
- package/dist/{import-DLY5UYDa.js → import-BvYl--U4.js} +2 -2
- package/dist/{import-DLY5UYDa.js.map → import-BvYl--U4.js.map} +1 -1
- package/dist/index.js +6 -6
- package/dist/{util-BnRLzp4j.js → util-GiwPgxnL.js} +95 -84
- package/dist/util-GiwPgxnL.js.map +1 -0
- package/package.json +9 -9
- package/src/components/DetailViewBinding.tsx +21 -14
- package/src/components/EditViewBinding.tsx +31 -17
- package/src/components/EntityInspector.tsx +56 -51
- package/src/components/history/EntityHistoryView.tsx +8 -3
- package/src/hooks/useHistory.ts +15 -6
- package/dist/history-c-iE7d4k.js.map +0 -1
- package/dist/util-BnRLzp4j.js.map +0 -1
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rebasepro/admin",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.13.0",
|
|
5
5
|
"description": "Rebase CMS — content management views, forms, and routing",
|
|
6
6
|
"funding": {
|
|
7
7
|
"url": "https://github.com/sponsors/rebaseco"
|
|
@@ -83,14 +83,14 @@
|
|
|
83
83
|
"react-dropzone": "^19.1.1",
|
|
84
84
|
"react-use-measure": "^2.1.7",
|
|
85
85
|
"zod": "^4.4.3",
|
|
86
|
-
"@rebasepro/
|
|
87
|
-
"@rebasepro/
|
|
88
|
-
"@rebasepro/
|
|
89
|
-
"@rebasepro/
|
|
90
|
-
"@rebasepro/forms": "0.
|
|
91
|
-
"@rebasepro/
|
|
92
|
-
"@rebasepro/
|
|
93
|
-
"@rebasepro/utils": "0.
|
|
86
|
+
"@rebasepro/admin-types": "0.13.0",
|
|
87
|
+
"@rebasepro/app": "0.13.0",
|
|
88
|
+
"@rebasepro/common": "0.13.0",
|
|
89
|
+
"@rebasepro/inference": "0.13.0",
|
|
90
|
+
"@rebasepro/forms": "0.13.0",
|
|
91
|
+
"@rebasepro/ui": "0.13.0",
|
|
92
|
+
"@rebasepro/types": "0.13.0",
|
|
93
|
+
"@rebasepro/utils": "0.13.0"
|
|
94
94
|
},
|
|
95
95
|
"peerDependencies": {
|
|
96
96
|
"react": ">=19.2.7",
|
|
@@ -581,22 +581,29 @@ entityId }
|
|
|
581
581
|
</Tabs>
|
|
582
582
|
</div>}
|
|
583
583
|
|
|
584
|
-
{
|
|
585
|
-
|
|
586
|
-
|
|
584
|
+
{/* See EditViewBinding: the inspector docks beside the record rather
|
|
585
|
+
than over it, so the record stays readable and scrollable. */}
|
|
586
|
+
<div className={"flex-1 min-h-0 flex"}>
|
|
587
587
|
|
|
588
|
-
|
|
589
|
-
|
|
588
|
+
<div className={"flex-1 min-w-0 flex flex-col"}>
|
|
589
|
+
{globalLoading
|
|
590
|
+
? <DetailViewSkeleton collection={collection} />
|
|
591
|
+
: mainView}
|
|
590
592
|
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
593
|
+
{customViewsView}
|
|
594
|
+
{subCollectionsViews}
|
|
595
|
+
</div>
|
|
596
|
+
|
|
597
|
+
<EntityInspector
|
|
598
|
+
tab={inspectorTab}
|
|
599
|
+
onTabChange={setInspectorTab}
|
|
600
|
+
onClose={() => setInspectorTab(null)}
|
|
601
|
+
collection={collection as AdminCollection}
|
|
602
|
+
entity={usedEntity as Entity<Record<string, unknown>> | undefined}
|
|
603
|
+
formContext={readOnlyFormContext as FormContext<Record<string, unknown>> | undefined}
|
|
604
|
+
values={usedEntity?.values}
|
|
605
|
+
includeHistory={includeHistoryView}/>
|
|
606
|
+
</div>
|
|
600
607
|
|
|
601
608
|
</div>;
|
|
602
609
|
|
|
@@ -421,6 +421,10 @@ parentEntityIds,
|
|
|
421
421
|
);
|
|
422
422
|
|
|
423
423
|
const [inspectorTab, setInspectorTab] = useState<InspectorTab | null>(null);
|
|
424
|
+
// A save adds a revision, and the inspector can be open while it happens:
|
|
425
|
+
// Save lives in the identity bar, above the panel. Counting saves gives the
|
|
426
|
+
// history list something to react to.
|
|
427
|
+
const [savedCount, setSavedCount] = useState(0);
|
|
424
428
|
|
|
425
429
|
/* ---- record actions, in the bar's overflow menu ---------------------- */
|
|
426
430
|
|
|
@@ -600,6 +604,7 @@ parentEntityIds,
|
|
|
600
604
|
formProps?.onFormContextReady?.(formContext);
|
|
601
605
|
}}
|
|
602
606
|
onSaved={(params) => {
|
|
607
|
+
setSavedCount(count => count + 1);
|
|
603
608
|
const res = {
|
|
604
609
|
...params,
|
|
605
610
|
selectedTab: MAIN_TAB_VALUE === activeTab ? undefined : activeTab
|
|
@@ -758,26 +763,35 @@ parentEntityIds,
|
|
|
758
763
|
</Tabs>
|
|
759
764
|
</div>}
|
|
760
765
|
|
|
761
|
-
{
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
{entityView}
|
|
766
|
-
</>}
|
|
766
|
+
{/* The record and the inspector share the space below the bar rather
|
|
767
|
+
than stacking, so the form stays reachable while the panel is open.
|
|
768
|
+
`min-w-0` keeps a wide form from pushing the panel off the edge. */}
|
|
769
|
+
<div className={"flex-1 min-h-0 flex"}>
|
|
767
770
|
|
|
768
|
-
|
|
771
|
+
<div className={"flex-1 min-w-0 flex flex-col"}>
|
|
772
|
+
{globalLoading
|
|
773
|
+
? <EntityFormSkeleton collection={collection} />
|
|
774
|
+
: <>
|
|
775
|
+
{entityReadOnlyView}
|
|
776
|
+
{entityView}
|
|
777
|
+
</>}
|
|
769
778
|
|
|
770
|
-
|
|
779
|
+
{customViewsView}
|
|
771
780
|
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
+
{subCollectionsViews}
|
|
782
|
+
</div>
|
|
783
|
+
|
|
784
|
+
<EntityInspector
|
|
785
|
+
tab={inspectorTab}
|
|
786
|
+
onTabChange={setInspectorTab}
|
|
787
|
+
onClose={() => setInspectorTab(null)}
|
|
788
|
+
collection={collection as AdminCollection}
|
|
789
|
+
entity={usedEntity as Entity<Record<string, unknown>> | undefined}
|
|
790
|
+
formContext={formContext as FormContext<Record<string, unknown>> | undefined}
|
|
791
|
+
values={formContext?.values ?? usedEntity?.values}
|
|
792
|
+
refreshToken={savedCount}
|
|
793
|
+
includeHistory={includeHistoryView}/>
|
|
794
|
+
</div>
|
|
781
795
|
|
|
782
796
|
</div>;
|
|
783
797
|
|
|
@@ -31,6 +31,12 @@ export interface EntityInspectorProps {
|
|
|
31
31
|
values?: Record<string, unknown>;
|
|
32
32
|
/** History is only offered when the collection records it. */
|
|
33
33
|
includeHistory: boolean;
|
|
34
|
+
/**
|
|
35
|
+
* Bumped once per save by the view that owns the form. The panel stays open
|
|
36
|
+
* across a save — the bar's Save button sits above it — so without this the
|
|
37
|
+
* history list would still be the one fetched when the panel opened.
|
|
38
|
+
*/
|
|
39
|
+
refreshToken?: number;
|
|
34
40
|
}
|
|
35
41
|
|
|
36
42
|
/**
|
|
@@ -41,8 +47,8 @@ export interface EntityInspectorProps {
|
|
|
41
47
|
* to edit. They are developer tools, not places the record lives, so they belong
|
|
42
48
|
* behind one affordance in the bar; a content editor should never meet `<>`.
|
|
43
49
|
*
|
|
44
|
-
* The panel
|
|
45
|
-
* record you are inspecting
|
|
50
|
+
* The panel sits below the identity bar and beside the record deliberately: you
|
|
51
|
+
* keep seeing which record you are inspecting, and keep being able to edit it.
|
|
46
52
|
*/
|
|
47
53
|
export function EntityInspector({
|
|
48
54
|
tab,
|
|
@@ -52,7 +58,8 @@ export function EntityInspector({
|
|
|
52
58
|
entity,
|
|
53
59
|
formContext,
|
|
54
60
|
values,
|
|
55
|
-
includeHistory
|
|
61
|
+
includeHistory,
|
|
62
|
+
refreshToken
|
|
56
63
|
}: EntityInspectorProps) {
|
|
57
64
|
|
|
58
65
|
const { t } = useTranslation();
|
|
@@ -73,59 +80,57 @@ export function EntityInspector({
|
|
|
73
80
|
if (!open) return null;
|
|
74
81
|
|
|
75
82
|
return (
|
|
76
|
-
//
|
|
77
|
-
//
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
83
|
+
// Docked beside the record rather than over it. As an overlay with a
|
|
84
|
+
// scrim this was modal in effect: the first click into the form went to
|
|
85
|
+
// the scrim and closed the panel, so a revision list could never be
|
|
86
|
+
// open while you edited the record it belongs to — which is the only
|
|
87
|
+
// time watching it is worth anything.
|
|
88
|
+
<div className={cls(
|
|
89
|
+
"relative flex flex-col shrink-0 h-full w-full max-w-md border-l",
|
|
90
|
+
"bg-white dark:bg-surface-800",
|
|
91
|
+
defaultBorderMixin
|
|
92
|
+
)}
|
|
93
|
+
role={"complementary"}
|
|
94
|
+
aria-label={t("inspect_record") ?? "Inspect record"}>
|
|
81
95
|
|
|
82
|
-
<div className={"
|
|
83
|
-
|
|
84
|
-
aria-hidden={true}/>
|
|
96
|
+
<div className={cls("h-11 shrink-0 flex items-center gap-2 pl-4 pr-1.5 border-b", defaultBorderMixin)}>
|
|
97
|
+
<span className={"text-sm font-semibold"}>{t("inspect") ?? "Inspect"}</span>
|
|
85
98
|
|
|
86
|
-
|
|
87
|
-
"relative flex flex-col w-full max-w-md border-l shadow-xl",
|
|
88
|
-
"bg-white dark:bg-surface-800",
|
|
89
|
-
defaultBorderMixin
|
|
90
|
-
)}>
|
|
91
|
-
<div className={cls("h-11 shrink-0 flex items-center gap-2 pl-4 pr-1.5 border-b", defaultBorderMixin)}>
|
|
92
|
-
<span className={"text-sm font-semibold"}>{t("inspect") ?? "Inspect"}</span>
|
|
99
|
+
<div className={"flex-1"}/>
|
|
93
100
|
|
|
94
|
-
|
|
101
|
+
{includeHistory
|
|
102
|
+
? <Tabs value={tab ?? "json"}
|
|
103
|
+
className={"!w-fit"}
|
|
104
|
+
onValueChange={(v) => onTabChange(v as InspectorTab)}>
|
|
105
|
+
<Tab value={"json"} className={"text-xs"}>{t("json")}</Tab>
|
|
106
|
+
<Tab value={"history"} className={"text-xs"}>
|
|
107
|
+
{t("record_history") ?? "History"}
|
|
108
|
+
</Tab>
|
|
109
|
+
</Tabs>
|
|
110
|
+
: null}
|
|
95
111
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
<Tab value={"history"} className={"text-xs"}>
|
|
102
|
-
{t("record_history") ?? "History"}
|
|
103
|
-
</Tab>
|
|
104
|
-
</Tabs>
|
|
105
|
-
: null}
|
|
106
|
-
|
|
107
|
-
<IconButton size={"small"} onClick={onClose}
|
|
108
|
-
aria-label={t("close_inspector") ?? "Close inspector"}>
|
|
109
|
-
<XIcon size={iconSize.smallest}/>
|
|
110
|
-
</IconButton>
|
|
111
|
-
</div>
|
|
112
|
+
<IconButton size={"small"} onClick={onClose}
|
|
113
|
+
aria-label={t("close_inspector") ?? "Close inspector"}>
|
|
114
|
+
<XIcon size={iconSize.smallest}/>
|
|
115
|
+
</IconButton>
|
|
116
|
+
</div>
|
|
112
117
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
</
|
|
118
|
+
<div className={"flex-1 min-h-0 overflow-auto"}>
|
|
119
|
+
<ErrorBoundary>
|
|
120
|
+
{tab === "json" && (
|
|
121
|
+
<JsonPreviewBinding values={values ?? entity?.values ?? {}}/>
|
|
122
|
+
)}
|
|
123
|
+
{tab === "history" && includeHistory && formContext && (
|
|
124
|
+
<Suspense fallback={<CircularProgressCenter/>}>
|
|
125
|
+
<EntityHistoryView
|
|
126
|
+
collection={collection}
|
|
127
|
+
entity={entity}
|
|
128
|
+
formContext={formContext}
|
|
129
|
+
refreshToken={refreshToken}
|
|
130
|
+
modifiedValues={values ?? entity?.values}/>
|
|
131
|
+
</Suspense>
|
|
132
|
+
)}
|
|
133
|
+
</ErrorBoundary>
|
|
129
134
|
</div>
|
|
130
135
|
</div>
|
|
131
136
|
);
|
|
@@ -16,8 +16,12 @@ import { useHistory } from "../../hooks/useHistory";
|
|
|
16
16
|
export function EntityHistoryView<M extends Record<string, unknown>>({
|
|
17
17
|
entity,
|
|
18
18
|
collection,
|
|
19
|
-
formContext
|
|
20
|
-
|
|
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
|
+
}) {
|
|
21
25
|
|
|
22
26
|
const { t } = useTranslation();
|
|
23
27
|
const snackbarController = useSnackbarController();
|
|
@@ -37,7 +41,8 @@ export function EntityHistoryView<M extends Record<string, unknown>>({
|
|
|
37
41
|
slug,
|
|
38
42
|
entityId,
|
|
39
43
|
enabled: !!entityId,
|
|
40
|
-
pageSize: 10
|
|
44
|
+
pageSize: 10,
|
|
45
|
+
refreshToken
|
|
41
46
|
});
|
|
42
47
|
|
|
43
48
|
const [revertHistoryId, setRevertHistoryId] = useState<string | undefined>();
|
package/src/hooks/useHistory.ts
CHANGED
|
@@ -28,8 +28,15 @@ 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();
|
|
34
41
|
const apiBase = useApiBase();
|
|
35
42
|
|
|
@@ -41,7 +48,11 @@ export function useHistory(params: {
|
|
|
41
48
|
const [offset, setOffset] = useState(0);
|
|
42
49
|
const [refreshTrigger, setRefreshTrigger] = useState(0);
|
|
43
50
|
|
|
44
|
-
// 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}`;
|
|
45
56
|
const currentEntityRef = useRef<string | undefined>(undefined);
|
|
46
57
|
|
|
47
58
|
const fetchEntries = useCallback(async (
|
|
@@ -66,7 +77,6 @@ signal });
|
|
|
66
77
|
|
|
67
78
|
// Check if the request was aborted or the entity changed while in-flight
|
|
68
79
|
if (signal?.aborted) return;
|
|
69
|
-
const entityKey = `${slug}/${entityId}`;
|
|
70
80
|
if (currentEntityRef.current !== entityKey) return;
|
|
71
81
|
|
|
72
82
|
if (!response.ok) {
|
|
@@ -91,12 +101,11 @@ signal });
|
|
|
91
101
|
} finally {
|
|
92
102
|
setIsLoading(false);
|
|
93
103
|
}
|
|
94
|
-
}, [apiConfig, apiBase, slug, entityId, enabled, pageSize]);
|
|
104
|
+
}, [apiConfig, apiBase, slug, entityId, entityKey, enabled, pageSize]);
|
|
95
105
|
|
|
96
106
|
// Single unified effect: reset state when entity changes, fetch when offset changes.
|
|
97
107
|
// Uses AbortController to cancel stale requests on entity change or unmount.
|
|
98
108
|
useEffect(() => {
|
|
99
|
-
const entityKey = `${slug}/${entityId}`;
|
|
100
109
|
const isNewEntity = currentEntityRef.current !== entityKey;
|
|
101
110
|
|
|
102
111
|
if (isNewEntity) {
|
|
@@ -118,7 +127,7 @@ signal });
|
|
|
118
127
|
return () => {
|
|
119
128
|
abortController.abort();
|
|
120
129
|
};
|
|
121
|
-
}, [fetchEntries, offset, enabled, entityId,
|
|
130
|
+
}, [fetchEntries, offset, enabled, entityId, entityKey, refreshTrigger]);
|
|
122
131
|
|
|
123
132
|
const loadMore = useCallback(() => {
|
|
124
133
|
if (!isLoading && hasMore && offset + entries.length < total) {
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"history-c-iE7d4k.js","names":[],"sources":["../src/components/history/UserChip.tsx","../src/components/history/EntityHistoryEntry.tsx","../src/components/history/EntityHistoryView.tsx"],"sourcesContent":["import { User } from \"@rebasepro/types\";\nimport { Chip, Tooltip } from \"@rebasepro/ui\";\n\nexport function UserChip({ user }: { user: User }) {\n return (\n <Tooltip title={user.email ?? user.uid}>\n <Chip size={\"small\"} className={\"flex items-center\"}>\n {user.photoURL && <img\n className={\"rounded-full w-6 h-6 mr-2\"}\n src={user.photoURL} alt={user.displayName ?? \"User picture\"}/>}\n <span>{user.displayName ?? user.email ?? user.uid}</span>\n </Chip>\n </Tooltip>\n );\n}\n","\nimport type { Property } from \"@rebasepro/types\";\nimport * as React from \"react\";\n\nimport { ArrowLeftIcon, Chip, cls, defaultBorderMixin, iconSize, Tooltip, Typography } from \"@rebasepro/ui\";\nimport { PreviewSize } from \"../../types/components/PropertyPreviewProps\";\nimport { getPropertyInPath } from \"../../util/property_utils\";\nimport { PropertyPreview } from \"../../preview/PropertyPreview\";\nimport { SkeletonPropertyComponent } from \"../../preview/property_previews/SkeletonPropertyComponent\";\nimport { useAuthController } from \"@rebasepro/app\";\nimport { UserChip } from \"./UserChip\";\nimport { HistoryEntryData } from \"../../hooks\";\nimport { getValueInPath } from \"@rebasepro/utils\";\nimport type { AdminCollection } from \"@rebasepro/admin-types\";\n\n/**\n * Recursive deep equality for primitives, arrays and plain objects.\n */\nfunction deepEqual(a: unknown, b: unknown): boolean {\n if (a === b) return true;\n if (a == null || b == null) return a === b;\n if (typeof a !== typeof b) return false;\n if (typeof a !== \"object\") return false;\n\n if (Array.isArray(a)) {\n if (!Array.isArray(b) || a.length !== (b as unknown[]).length) return false;\n return a.every((item, i) => deepEqual(item, (b as unknown[])[i]));\n }\n\n if (Array.isArray(b)) return false;\n\n const aObj = a as Record<string, unknown>;\n const bObj = b as Record<string, unknown>;\n const aKeys = Object.keys(aObj);\n const bKeys = Object.keys(bObj);\n if (aKeys.length !== bKeys.length) return false;\n return aKeys.every(key => key in bObj && deepEqual(aObj[key], bObj[key]));\n}\n\nexport type EntityHistoryEntryProps = {\n size: PreviewSize;\n actions?: React.ReactNode;\n collection?: AdminCollection;\n hover?: boolean;\n entry: HistoryEntryData;\n onClick?: (e: React.SyntheticEvent) => void;\n};\n\nfunction PreviousValueView({\n previousValueInPath,\n childProperty,\n propertyKey\n}: {\n previousValueInPath: unknown;\n childProperty: Property;\n propertyKey: string;\n}) {\n if (typeof previousValueInPath === \"string\" || typeof previousValueInPath === \"number\") {\n return <Typography variant={\"caption\"} color={\"secondary\"} className=\"line-through\">\n {previousValueInPath}\n </Typography>;\n } else if (typeof previousValueInPath === \"boolean\") {\n return <Typography variant={\"caption\"} color={\"secondary\"} className=\"line-through\">\n {previousValueInPath ? \"true\" : \"false\"}\n </Typography>;\n } else {\n return <Tooltip\n side={\"left\"}\n title={<div className={\"flex flex-col gap-2\"}>\n <Typography variant={\"caption\"} color={\"secondary\"}>\n Previous value\n </Typography>\n <PropertyPreview\n propertyKey={propertyKey as string}\n value={previousValueInPath as never}\n property={childProperty as Property}\n size={\"small\"}/>\n </div>}>\n <ArrowLeftIcon size={iconSize.smallest} color={\"disabled\"} className={\"mb-1\"}/>\n </Tooltip>\n }\n}\n\n/**\n * Displays a single entity history revision entry.\n * Adapted from the entity_history plugin — now reads from backend API data.\n */\nexport function EntityHistoryEntry({\n actions,\n hover,\n collection,\n size,\n entry\n}: EntityHistoryEntryProps) {\n\n const authController = useAuthController();\n\n // The backend reports the whole row as changed, which includes the columns\n // that cannot meaningfully change: a timestamp it stamps itself on every\n // write, and the primary key, which is what identifies the row being\n // revised. Every revision was listing `updated_at: <now>` and `id: <the id\n // in the title bar>` around the one field the user actually edited.\n const changedFields = React.useMemo(() => {\n const all = entry.changed_fields;\n if (!all || !collection) return all;\n return all.filter(key => {\n const property = getPropertyInPath(collection.properties, key);\n if (!property) return true;\n if (\"isId\" in property && property.isId) return false;\n return !(property.type === \"date\" && \"autoValue\" in property && property.autoValue);\n });\n }, [entry.changed_fields, collection]);\n\n const previousValues = entry.previous_values;\n const updatedOn = new Date(entry.updated_at);\n const updatedBy = entry.updated_by;\n\n // Resolve user display\n const currentUser = authController.user;\n const userDisplay = updatedBy === currentUser?.uid\n ? currentUser\n : undefined;\n\n return <div className={\"@container/histentry w-full flex flex-col gap-2\"}>\n <div className={\"flex items-center flex-wrap gap-2\"}>\n <Typography variant={\"body2\"} color={\"secondary\"}>\n {updatedOn.toLocaleString()}\n </Typography>\n <Chip size={\"small\"}>\n {entry.action}\n </Chip>\n {!userDisplay && updatedBy && <Chip size={\"small\"}>{updatedBy}</Chip>}\n {userDisplay && <UserChip user={userDisplay}/>}\n </div>\n <div\n className={cls(\n \"bg-white dark:bg-surface-900\",\n \"min-h-[44px]\",\n \"w-full\",\n \"items-center\",\n hover ? \"hover:bg-surface-accent-50 dark:hover:bg-surface-800\" : \"\",\n size === \"small\" ? \"p-1\" : \"px-2 py-1\",\n \"flex border rounded-lg\",\n defaultBorderMixin\n )}>\n\n {actions}\n\n <div className={\"flex flex-col grow w-full m-1 shrink min-w-0\"}>\n\n {changedFields && collection && changedFields.map((key: string) => {\n const childProperty = getPropertyInPath(collection.properties, key);\n const valueInPath = entry.values ? getValueInPath(entry.values, key) : undefined;\n const previousValueInPath = previousValues ? getValueInPath(previousValues, key) : undefined;\n\n const element = childProperty\n ? <PropertyPreview\n propertyKey={key}\n value={valueInPath as never}\n property={childProperty as Property}\n size={\"small\"}/>\n : <Typography variant={\"body2\"}>\n {typeof valueInPath === \"string\" ? valueInPath : JSON.stringify(valueInPath)}\n </Typography>;\n\n // The key column used to be `min-w-[200px] w-1/5` beside a\n // `w-4/5` value — 200px of label and 80% of the row, which\n // adds up to more than the row in anything narrower than a\n // full page. In the inspector that pushed every value off\n // the right edge and wrapped ids one character per line.\n // It stacks until the entry itself is wide enough.\n return (\n <div key={\"ref_prev_\" + key}\n className=\"flex flex-col @sm/histentry:flex-row @sm/histentry:items-baseline gap-x-4 gap-y-0.5 w-full my-1.5\">\n <Typography variant={\"caption\"}\n color={\"secondary\"}\n className=\"shrink-0 break-all @sm/histentry:w-40 @sm/histentry:text-right @sm/histentry:break-normal @sm/histentry:truncate\">\n {key}\n </Typography>\n <div className=\"min-w-0 flex-1 flex flex-col items-start gap-0.5\">\n {previousValueInPath !== undefined && !deepEqual(previousValueInPath, valueInPath) &&\n <PreviousValueView previousValueInPath={previousValueInPath}\n childProperty={childProperty as Property}\n propertyKey={key}/>\n }\n {element}\n </div>\n </div>\n );\n })}\n\n {(!changedFields || changedFields.length === 0) && (\n <Typography variant={\"caption\"} color={\"secondary\"}>\n {entry.action === \"create\" ? \"Entity created\" :\n entry.action === \"delete\" ? \"Entity deleted\" : \"No field changes recorded\"}\n </Typography>\n )}\n\n </div>\n\n </div>\n </div>;\n}\n","\nimport type { EntityCustomViewParams } from \"@rebasepro/admin-types\";\nimport { useRef, useEffect } from \"react\";\nimport { cls, IconButton, Label, Tooltip, Typography } from \"@rebasepro/ui\";\nimport { ErrorBoundary, HistoryIcon } from \"@rebasepro/ui\";\nimport { EntityHistoryEntry } from \"./EntityHistoryEntry\";\nimport { useSnackbarController, useAuthController, useTranslation } from \"@rebasepro/app\";\nimport { ConfirmationDialog } from \"@rebasepro/app\";\nimport { useState } from \"react\";\nimport { useHistory } from \"../../hooks/useHistory\";\n\n/**\n * Entity history tab view. Shows a paginated list of entity revisions\n * fetched from the backend API. Supports infinite scroll and revert.\n */\nexport function EntityHistoryView<M extends Record<string, unknown>>({\n entity,\n collection,\n formContext\n}: EntityCustomViewParams<M>) {\n\n const { t } = useTranslation();\n const snackbarController = useSnackbarController();\n const authController = useAuthController();\n const dirty = formContext?.formex.dirty;\n\n const slug = collection.slug;\n const entityId = entity?.id;\n\n const {\n entries,\n isLoading,\n hasMore,\n loadMore,\n revert\n } = useHistory({\n slug,\n entityId,\n enabled: !!entityId,\n pageSize: 10\n });\n\n const [revertHistoryId, setRevertHistoryId] = useState<string | undefined>();\n const [isReverting, setIsReverting] = useState(false);\n\n const containerRef = useRef<HTMLDivElement>(null);\n const loadMoreRef = useRef<HTMLDivElement>(null);\n\n // Intersection observer for infinite scroll\n useEffect(() => {\n const currentContainer = containerRef.current;\n const currentLoadMore = loadMoreRef.current;\n\n if (!currentContainer || !currentLoadMore || !hasMore || isLoading) return;\n\n const observer = new IntersectionObserver(\n (observerEntries) => {\n if (observerEntries[0].isIntersecting && hasMore && !isLoading) {\n loadMore();\n }\n },\n {\n root: currentContainer,\n rootMargin: \"0px 0px 200px 0px\",\n threshold: 0.01\n }\n );\n\n observer.observe(currentLoadMore);\n\n return () => observer.disconnect();\n }, [hasMore, isLoading, entries.length, loadMore]);\n\n if (!entity) {\n return <div className=\"flex items-center justify-center h-full\">\n <Label>{t(\"history_new_entity\") ?? \"History is only available for existing records\"}</Label>\n </div>;\n }\n\n async function doRevert(historyId: string) {\n setIsReverting(true);\n try {\n const revertedValues = await revert(historyId);\n setRevertHistoryId(undefined);\n\n // Reset the form with the reverted values so the UI updates\n // immediately without requiring a page refresh.\n if (formContext?.formex?.resetForm && revertedValues) {\n formContext.formex.resetForm({\n values: revertedValues as M,\n submitCount: 0,\n touched: {}\n });\n }\n\n snackbarController.open({\n message: t(\"history_reverted\") ?? \"Reverted to selected version\",\n type: \"info\"\n });\n } catch (error) {\n console.error(\"Error reverting entity:\", error);\n snackbarController.open({\n message: t(\"history_revert_error\") ?? \"Error reverting entity\",\n type: \"error\"\n });\n } finally {\n setIsReverting(false);\n }\n }\n\n const revertEntry = revertHistoryId\n ? entries.find(e => e.id === revertHistoryId)\n : undefined;\n\n // `p-8` plus a `mt-24` heading were sized for a full-width tab that no\n // longer exists. In the inspector panel that was 96px of nothing above the\n // first revision, and 32px of gutter on each side of a 448px column.\n return <div\n ref={containerRef}\n className={cls(\"relative flex-1 h-full overflow-auto w-full flex flex-col gap-4 p-4 sm:p-5\")}>\n <div className=\"flex flex-col gap-2 max-w-6xl mx-auto w-full\">\n\n {isLoading && entries.length === 0 && (\n <div className=\"flex flex-col gap-4\">\n {[1, 2, 3].map(i => (\n <div key={i} className=\"flex flex-col gap-2 animate-pulse\">\n <div className=\"h-4 w-48 bg-surface-200 dark:bg-surface-700 rounded\"/>\n <div className=\"h-12 w-full bg-surface-100 dark:bg-surface-900 rounded-lg border border-surface-200 dark:border-surface-700\"/>\n </div>\n ))}\n </div>\n )}\n\n {!isLoading && entries.length === 0 && <>\n <Label>\n {t(\"history_empty\") ?? \"No history available\"}\n </Label>\n <Typography variant={\"caption\"}>\n {t(\"history_empty_description\") ?? \"When you save a record, a new version is created and stored in the history.\"}\n </Typography>\n </>}\n\n {entries.map((entry) => (\n <div key={entry.id} className=\"flex flex-col gap-2 w-full\">\n <EntityHistoryEntry\n entry={entry}\n collection={collection}\n size={\"medium\"}\n actions={\n <Tooltip title={t(\"history_revert\") ?? \"Revert to this version\"}\n className={\"m-2 grow-0 self-start\"}>\n <IconButton\n onClick={() => {\n if (dirty) {\n snackbarController.open({\n message: t(\"history_revert_dirty\") ?? \"Please save or discard your changes before reverting\",\n type: \"warning\"\n });\n } else {\n setRevertHistoryId(entry.id);\n }\n }}>\n <HistoryIcon/>\n </IconButton>\n </Tooltip>\n }\n />\n </div>\n ))}\n\n {/* Load more sentinel */}\n {entries.length > 0 && (\n <div ref={loadMoreRef} className=\"py-4 text-center\">\n {isLoading && <Label>{t(\"loading\")}</Label>}\n {!hasMore && entries.length > 10 && <Label>{t(\"history_no_more\") ?? \"No more history\"}</Label>}\n </div>\n )}\n </div>\n\n <ErrorBoundary>\n <ConfirmationDialog\n open={Boolean(revertHistoryId)}\n onAccept={() => {\n if (revertHistoryId) doRevert(revertHistoryId);\n }}\n onCancel={() => setRevertHistoryId(undefined)}\n title={<Typography variant={\"subtitle2\"}>{t(\"history_revert_title\") ?? \"Revert data to this version?\"}</Typography>}\n body={revertEntry\n ? <div className=\"p-4\">\n <Typography variant={\"caption\"} color={\"secondary\"}>\n This will save the entity with the values from{\" \"}\n {new Date(revertEntry.updated_at).toLocaleString()}.\n A new history entry will be created for the revert.\n </Typography>\n </div>\n : null\n }\n />\n </ErrorBoundary>\n </div>;\n}\n"],"mappings":"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"}
|