@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.
Files changed (27) hide show
  1. package/dist/{CollectionEditorDialog-Dhn0S4FT.js → CollectionEditorDialog-CjXkWZ64.js} +3 -3
  2. package/dist/{CollectionEditorDialog-Dhn0S4FT.js.map → CollectionEditorDialog-CjXkWZ64.js.map} +1 -1
  3. package/dist/{PropertyEditView-JPN12E-0.js → PropertyEditView-W8I3YV89.js} +2 -2
  4. package/dist/{PropertyEditView-JPN12E-0.js.map → PropertyEditView-W8I3YV89.js.map} +1 -1
  5. package/dist/{RouterCollectionsStudioView-Cv0tAxtu.js → RouterCollectionsStudioView-BsLTNoqX.js} +4 -4
  6. package/dist/{RouterCollectionsStudioView-Cv0tAxtu.js.map → RouterCollectionsStudioView-BsLTNoqX.js.map} +1 -1
  7. package/dist/collection_editor_ui.js +4 -4
  8. package/dist/components/EntityInspector.d.ts +9 -3
  9. package/dist/components/history/EntityHistoryView.d.ts +4 -1
  10. package/dist/{export-DghFk-W0.js → export-J79rMoNj.js} +2 -2
  11. package/dist/{export-DghFk-W0.js.map → export-J79rMoNj.js.map} +1 -1
  12. package/dist/{history-c-iE7d4k.js → history-D7L_JlLO.js} +5 -4
  13. package/dist/history-D7L_JlLO.js.map +1 -0
  14. package/dist/hooks/useHistory.d.ts +7 -0
  15. package/dist/{import-DLY5UYDa.js → import-BvYl--U4.js} +2 -2
  16. package/dist/{import-DLY5UYDa.js.map → import-BvYl--U4.js.map} +1 -1
  17. package/dist/index.js +6 -6
  18. package/dist/{util-BnRLzp4j.js → util-GiwPgxnL.js} +95 -84
  19. package/dist/util-GiwPgxnL.js.map +1 -0
  20. package/package.json +9 -9
  21. package/src/components/DetailViewBinding.tsx +21 -14
  22. package/src/components/EditViewBinding.tsx +31 -17
  23. package/src/components/EntityInspector.tsx +56 -51
  24. package/src/components/history/EntityHistoryView.tsx +8 -3
  25. package/src/hooks/useHistory.ts +15 -6
  26. package/dist/history-c-iE7d4k.js.map +0 -1
  27. 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.12.1-canary.gf4240e3",
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/app": "0.12.1-canary.gf4240e3",
87
- "@rebasepro/admin-types": "0.12.1-canary.gf4240e3",
88
- "@rebasepro/types": "0.12.1-canary.gf4240e3",
89
- "@rebasepro/common": "0.12.1-canary.gf4240e3",
90
- "@rebasepro/forms": "0.12.1-canary.gf4240e3",
91
- "@rebasepro/inference": "0.12.1-canary.gf4240e3",
92
- "@rebasepro/ui": "0.12.1-canary.gf4240e3",
93
- "@rebasepro/utils": "0.12.1-canary.gf4240e3"
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
- {globalLoading
585
- ? <DetailViewSkeleton collection={collection} />
586
- : mainView}
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
- {customViewsView}
589
- {subCollectionsViews}
588
+ <div className={"flex-1 min-w-0 flex flex-col"}>
589
+ {globalLoading
590
+ ? <DetailViewSkeleton collection={collection} />
591
+ : mainView}
590
592
 
591
- <EntityInspector
592
- tab={inspectorTab}
593
- onTabChange={setInspectorTab}
594
- onClose={() => setInspectorTab(null)}
595
- collection={collection as AdminCollection}
596
- entity={usedEntity as Entity<Record<string, unknown>> | undefined}
597
- formContext={readOnlyFormContext as FormContext<Record<string, unknown>> | undefined}
598
- values={usedEntity?.values}
599
- includeHistory={includeHistoryView}/>
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
- {globalLoading
762
- ? <EntityFormSkeleton collection={collection} />
763
- : <>
764
- {entityReadOnlyView}
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
- {customViewsView}
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
- {subCollectionsViews}
779
+ {customViewsView}
771
780
 
772
- <EntityInspector
773
- tab={inspectorTab}
774
- onTabChange={setInspectorTab}
775
- onClose={() => setInspectorTab(null)}
776
- collection={collection as AdminCollection}
777
- entity={usedEntity as Entity<Record<string, unknown>> | undefined}
778
- formContext={formContext as FormContext<Record<string, unknown>> | undefined}
779
- values={formContext?.values ?? usedEntity?.values}
780
- includeHistory={includeHistoryView}/>
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 starts below the identity bar deliberately: you keep seeing which
45
- * record you are inspecting while you inspect it.
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
- // Starts below the 52px identity bar, so the record you are inspecting
77
- // stays named and its actions stay reachable while the panel is open.
78
- <div className={"absolute inset-x-0 bottom-0 top-[52px] z-30 flex justify-end"}
79
- role={"dialog"}
80
- aria-label={t("inspect_record") ?? "Inspect record"}>
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={"absolute inset-0 bg-black/25"}
83
- onClick={onClose}
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
- <div className={cls(
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
- <div className={"flex-1"}/>
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
- {includeHistory
97
- ? <Tabs value={tab ?? "json"}
98
- className={"!w-fit"}
99
- onValueChange={(v) => onTabChange(v as InspectorTab)}>
100
- <Tab value={"json"} className={"text-xs"}>{t("json")}</Tab>
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
- <div className={"flex-1 min-h-0 overflow-auto"}>
114
- <ErrorBoundary>
115
- {tab === "json" && (
116
- <JsonPreviewBinding values={values ?? entity?.values ?? {}}/>
117
- )}
118
- {tab === "history" && includeHistory && formContext && (
119
- <Suspense fallback={<CircularProgressCenter/>}>
120
- <EntityHistoryView
121
- collection={collection}
122
- entity={entity}
123
- formContext={formContext}
124
- modifiedValues={values ?? entity?.values}/>
125
- </Suspense>
126
- )}
127
- </ErrorBoundary>
128
- </div>
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
- }: EntityCustomViewParams<M>) {
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>();
@@ -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, slug, refreshTrigger]);
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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"}