@rebasepro/admin 0.12.1-canary.gf5f1d39 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (178) hide show
  1. package/README.md +1 -1
  2. package/dist/{CollectionEditorDialog-Dpc75wIZ.js → CollectionEditorDialog-CjXkWZ64.js} +75 -62
  3. package/dist/{CollectionEditorDialog-Dpc75wIZ.js.map → CollectionEditorDialog-CjXkWZ64.js.map} +1 -1
  4. package/dist/{PropertyEditView-DH3XZC2x.js → PropertyEditView-W8I3YV89.js} +36 -16
  5. package/dist/{PropertyEditView-DH3XZC2x.js.map → PropertyEditView-W8I3YV89.js.map} +1 -1
  6. package/dist/{RouterCollectionsStudioView-BZLq4MGp.js → RouterCollectionsStudioView-BsLTNoqX.js} +5 -5
  7. package/dist/{RouterCollectionsStudioView-BZLq4MGp.js.map → RouterCollectionsStudioView-BsLTNoqX.js.map} +1 -1
  8. package/dist/collection_editor/serializable_types.d.ts +4 -1
  9. package/dist/collection_editor/ui/collection_editor/CollectionRelationsTab.d.ts +1 -0
  10. package/dist/collection_editor/useLocalCollectionsConfigController.d.ts +8 -0
  11. package/dist/collection_editor_ui.js +5 -5
  12. package/dist/collection_editor_ui.js.map +1 -1
  13. package/dist/components/CollectionTableBinding/column_utils.d.ts +17 -0
  14. package/dist/components/CollectionViewBinding/CollectionViewStartActions.d.ts +7 -2
  15. package/dist/components/CollectionViewBinding/SortButton.d.ts +24 -0
  16. package/dist/components/CollectionViewBinding/SplitListCloseButton.d.ts +10 -0
  17. package/dist/components/CollectionViewBinding/SplitViewContext.d.ts +23 -0
  18. package/dist/components/CollectionViewBinding/hooks/useKanbanDragAndDrop.d.ts +26 -0
  19. package/dist/components/DetailViewBinding.d.ts +5 -0
  20. package/dist/components/EditFormActions.d.ts +12 -1
  21. package/dist/components/EditViewBinding.d.ts +6 -1
  22. package/dist/components/EntityIdentityBar.d.ts +53 -0
  23. package/dist/components/EntityInspector.d.ts +35 -0
  24. package/dist/components/EntityPreviewNesting.d.ts +25 -0
  25. package/dist/components/EntityViewBinding.d.ts +9 -0
  26. package/dist/components/InlineEntityPreview.d.ts +47 -0
  27. package/dist/components/history/EntityHistoryView.d.ts +4 -1
  28. package/dist/components/index.d.ts +2 -0
  29. package/dist/editor.js +18 -10
  30. package/dist/editor.js.map +1 -1
  31. package/dist/{export-BzIpUdSK.js → export-J79rMoNj.js} +2 -2
  32. package/dist/{export-BzIpUdSK.js.map → export-J79rMoNj.js.map} +1 -1
  33. package/dist/form/EntityForm.d.ts +1 -1
  34. package/dist/form/EntityFormActions.d.ts +11 -1
  35. package/dist/form/PropertyFieldBinding.d.ts +1 -1
  36. package/dist/form/components/FieldBlock.d.ts +30 -0
  37. package/dist/form/components/FormRail.d.ts +18 -0
  38. package/dist/form/components/FormSections.d.ts +16 -0
  39. package/dist/form/components/index.d.ts +3 -2
  40. package/dist/form/components/useRailVisible.d.ts +24 -0
  41. package/dist/form/field_bindings/BinaryFieldBinding.d.ts +1 -1
  42. package/dist/form/field_bindings/DateTimeFieldBinding.d.ts +1 -1
  43. package/dist/form/field_bindings/GeopointFieldBinding.d.ts +1 -1
  44. package/dist/form/field_bindings/MarkdownEditorFieldBinding.d.ts +1 -1
  45. package/dist/form/field_bindings/MultiSelectFieldBinding.d.ts +1 -1
  46. package/dist/form/field_bindings/ReadOnlyFieldBinding.d.ts +1 -1
  47. package/dist/form/field_bindings/SelectFieldBinding.d.ts +1 -1
  48. package/dist/form/field_bindings/StorageUploadFieldBinding.d.ts +1 -1
  49. package/dist/form/field_bindings/SwitchFieldBinding.d.ts +1 -1
  50. package/dist/form/field_bindings/TextFieldBinding.d.ts +1 -1
  51. package/dist/form/field_bindings/UserSelectFieldBinding.d.ts +1 -1
  52. package/dist/form/field_bindings/VectorFieldBinding.d.ts +1 -1
  53. package/dist/{history-g9688IaK.js → history-D7L_JlLO.js} +35 -33
  54. package/dist/history-D7L_JlLO.js.map +1 -0
  55. package/dist/hooks/navigation/utils.d.ts +16 -0
  56. package/dist/hooks/useBuildSidePanel.d.ts +14 -0
  57. package/dist/hooks/useEntityDisplayTitle.d.ts +18 -0
  58. package/dist/hooks/useHistory.d.ts +7 -0
  59. package/dist/hooks/useRecordActions.d.ts +17 -0
  60. package/dist/{import-BjUG-cIY.js → import-BvYl--U4.js} +2 -2
  61. package/dist/{import-BjUG-cIY.js.map → import-BvYl--U4.js.map} +1 -1
  62. package/dist/index.js +106 -35
  63. package/dist/index.js.map +1 -1
  64. package/dist/markdown-CklalUUk.js.map +1 -1
  65. package/dist/preview/components/InlineEntityListPreview.d.ts +15 -0
  66. package/dist/preview/index.d.ts +1 -0
  67. package/dist/preview/property_previews/ArrayOfReferencesPreview.d.ts +1 -1
  68. package/dist/preview/property_previews/ArrayOfRelationsPreview.d.ts +1 -1
  69. package/dist/types/components/EntityFormActionsProps.d.ts +0 -1
  70. package/dist/types/components/EntityFormProps.d.ts +0 -5
  71. package/dist/types/fields.d.ts +24 -0
  72. package/dist/{util-B5fJm1FA.js → util-GiwPgxnL.js} +2106 -1117
  73. package/dist/util-GiwPgxnL.js.map +1 -0
  74. package/package.json +32 -34
  75. package/src/collection_editor/ConfigControllerProvider.tsx +1 -1
  76. package/src/collection_editor/serializable_types.ts +4 -3
  77. package/src/collection_editor/serializable_utils.ts +51 -17
  78. package/src/collection_editor/ui/collection_editor/CollectionEditorDialog.tsx +65 -22
  79. package/src/collection_editor/ui/collection_editor/CollectionRLSTab.tsx +38 -30
  80. package/src/collection_editor/ui/collection_editor/CollectionRelationsTab.tsx +3 -1
  81. package/src/collection_editor/ui/collection_editor/RouterCollectionStudioView.tsx +1 -1
  82. package/src/collection_editor/ui/collection_editor/RouterCollectionsStudioView.tsx +1 -1
  83. package/src/collection_editor/ui/collection_editor/properties/RelationPropertyField.tsx +16 -0
  84. package/src/collection_editor/useLocalCollectionsConfigController.tsx +127 -15
  85. package/src/collection_editor/validateCollectionJson.ts +3 -3
  86. package/src/components/AdminModeSyncer.tsx +1 -1
  87. package/src/components/CollectionTableBinding/PropertyTableCell.tsx +10 -2
  88. package/src/components/CollectionTableBinding/column_utils.tsx +59 -2
  89. package/src/components/CollectionTableBinding/table_bindings.tsx +2 -1
  90. package/src/components/CollectionViewBinding/CollectionBoardViewBinding.tsx +23 -16
  91. package/src/components/CollectionViewBinding/CollectionViewActions.tsx +7 -1
  92. package/src/components/CollectionViewBinding/CollectionViewBinding.tsx +9 -2
  93. package/src/components/CollectionViewBinding/CollectionViewStartActions.tsx +47 -12
  94. package/src/components/CollectionViewBinding/SortButton.tsx +189 -0
  95. package/src/components/CollectionViewBinding/SplitListCloseButton.tsx +33 -0
  96. package/src/components/CollectionViewBinding/SplitListView.tsx +18 -3
  97. package/src/components/CollectionViewBinding/SplitViewContext.tsx +34 -0
  98. package/src/components/CollectionViewBinding/hooks/useKanbanDragAndDrop.ts +48 -9
  99. package/src/components/CollectionViewBinding/useBoardDataController.tsx +80 -25
  100. package/src/components/CollectionViewBinding/usePreviewSlots.ts +18 -6
  101. package/src/components/DefaultAppBar.tsx +1 -1
  102. package/src/components/DefaultDrawer.tsx +1 -1
  103. package/src/components/DetailViewBinding.tsx +112 -148
  104. package/src/components/DrawerNavigationItem.tsx +1 -1
  105. package/src/components/EditFormActions.tsx +54 -129
  106. package/src/components/EditViewBinding.tsx +202 -99
  107. package/src/components/EntityIdentityBar.tsx +288 -0
  108. package/src/components/EntityInspector.tsx +137 -0
  109. package/src/components/EntityPreviewBinding.tsx +6 -2
  110. package/src/components/EntityPreviewNesting.tsx +50 -0
  111. package/src/components/EntityViewBinding.tsx +35 -32
  112. package/src/components/HomePage/FavouritesView.tsx +1 -1
  113. package/src/components/HomePage/NavigationCardBinding.tsx +1 -1
  114. package/src/components/HomePage/SmallNavigationCard.tsx +1 -1
  115. package/src/components/InlineEntityPreview.tsx +149 -0
  116. package/src/components/RebaseLayout.tsx +1 -1
  117. package/src/components/RebaseNavigation.tsx +45 -5
  118. package/src/components/RebaseRouteDefs.tsx +1 -1
  119. package/src/components/SelectableTable/SelectableTable.tsx +2 -2
  120. package/src/components/SidePanelBinding.tsx +19 -13
  121. package/src/components/UserSelector.tsx +12 -6
  122. package/src/components/common/default_entity_actions.tsx +4 -3
  123. package/src/components/history/EntityHistoryEntry.tsx +28 -7
  124. package/src/components/history/EntityHistoryView.tsx +29 -24
  125. package/src/components/history/LastEditedByIndicator.tsx +4 -3
  126. package/src/components/index.ts +2 -0
  127. package/src/data_import/utils/get_import_inference_type.ts +6 -0
  128. package/src/form/EntityForm.tsx +267 -179
  129. package/src/form/EntityFormActions.tsx +50 -99
  130. package/src/form/EntityFormBinding.tsx +4 -2
  131. package/src/form/PropertyFieldBinding.tsx +4 -0
  132. package/src/form/components/FieldBlock.tsx +119 -0
  133. package/src/form/components/FormRail.tsx +125 -0
  134. package/src/form/components/FormSections.tsx +118 -0
  135. package/src/form/components/index.tsx +3 -2
  136. package/src/form/components/useRailVisible.tsx +46 -0
  137. package/src/form/field_bindings/BinaryFieldBinding.tsx +3 -2
  138. package/src/form/field_bindings/DateTimeFieldBinding.tsx +3 -2
  139. package/src/form/field_bindings/GeopointFieldBinding.tsx +3 -2
  140. package/src/form/field_bindings/MapFieldBinding.tsx +1 -2
  141. package/src/form/field_bindings/MarkdownEditorFieldBinding.tsx +3 -2
  142. package/src/form/field_bindings/MultiSelectFieldBinding.tsx +2 -1
  143. package/src/form/field_bindings/ReadOnlyFieldBinding.tsx +2 -1
  144. package/src/form/field_bindings/ReferenceFieldBinding.tsx +2 -1
  145. package/src/form/field_bindings/RelationFieldBinding.tsx +6 -4
  146. package/src/form/field_bindings/RepeatFieldBinding.tsx +4 -1
  147. package/src/form/field_bindings/SelectFieldBinding.tsx +3 -2
  148. package/src/form/field_bindings/StorageUploadFieldBinding.tsx +31 -8
  149. package/src/form/field_bindings/SwitchFieldBinding.tsx +7 -3
  150. package/src/form/field_bindings/TextFieldBinding.tsx +18 -16
  151. package/src/form/field_bindings/UserSelectFieldBinding.tsx +3 -2
  152. package/src/form/field_bindings/VectorFieldBinding.tsx +2 -1
  153. package/src/hooks/navigation/useBuildUrlController.tsx +1 -1
  154. package/src/hooks/navigation/useTopLevelNavigation.ts +18 -7
  155. package/src/hooks/navigation/utils.ts +20 -0
  156. package/src/hooks/useBuildSideDialogsController.tsx +2 -2
  157. package/src/hooks/useBuildSidePanel.tsx +22 -61
  158. package/src/hooks/useEntityDisplayTitle.tsx +60 -0
  159. package/src/hooks/useHistory.ts +20 -10
  160. package/src/hooks/useRecordActions.tsx +69 -0
  161. package/src/hooks/useResolvedUsers.ts +10 -5
  162. package/src/preview/PropertyPreview.tsx +48 -22
  163. package/src/preview/components/InlineEntityListPreview.tsx +42 -0
  164. package/src/preview/components/ReferencePreview.tsx +34 -14
  165. package/src/preview/components/RelationPreview.tsx +34 -14
  166. package/src/preview/index.ts +1 -0
  167. package/src/preview/property_previews/ArrayOfReferencesPreview.tsx +24 -1
  168. package/src/preview/property_previews/ArrayOfRelationsPreview.tsx +25 -1
  169. package/src/routes/RebaseRoute.tsx +2 -2
  170. package/src/types/components/EntityFormActionsProps.tsx +0 -1
  171. package/src/types/components/EntityFormProps.tsx +0 -6
  172. package/src/types/fields.tsx +27 -0
  173. package/dist/form/components/FormEntry.d.ts +0 -6
  174. package/dist/form/components/FormLayout.d.ts +0 -5
  175. package/dist/history-g9688IaK.js.map +0 -1
  176. package/dist/util-B5fJm1FA.js.map +0 -1
  177. package/src/form/components/FormEntry.tsx +0 -22
  178. 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
+ }
@@ -1,5 +1,5 @@
1
1
  import React from "react";
2
- import { Outlet } from "react-router-dom";
2
+ import { Outlet } from "react-router";
3
3
  import {
4
4
  useAdminModeController,
5
5
  useComponentOverride,
@@ -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 ?? process.env.NODE_ENV === "production",
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
- <CollectionsStudioView configController={internalConfigController}/>
211
+ <CollectionsStudioViewFromContext/>
195
212
  </Suspense>
196
213
  )
197
214
  };
198
- }, [collectionEditorEnabled, registry.studioConfig, internalConfigController]);
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
  }
@@ -1,5 +1,5 @@
1
1
  import React, { lazy, Suspense, useMemo } from "react";
2
- import { Route } from "react-router-dom";
2
+ import { Route } from "react-router";
3
3
  import {
4
4
  useRebaseRegistry,
5
5
  RebaseRoutes,
@@ -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, TableView, VirtualTableColumn, VirtualTableFilterValues, OnRowClickParams } from "@rebasepro/ui";
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
- <TableView
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-dom";
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
- if (!client?.baseUrl) return;
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
- const response = await fetch(`${client.baseUrl}/api/users?${params}`, {
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 baseUrl = client?.baseUrl || "";
274
+ const apiBase = apiBaseOf(client) ?? "";
274
275
  const token = await getAuthToken?.();
275
- const response = await fetch(`${baseUrl}/api/admin/users/${user.uid}/reset-password`, {
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
- const changedFields = entry.changed_fields;
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 mt-4"}>
110
- <div className={"ml-4 flex items-center gap-4"}>
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 items-center">
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="min-w-[140px] md:min-w-[200px] w-1/5 pr-8 overflow-hidden text-ellipsis text-right">
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-4/5">
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"} className="ml-4">
193
+ <Typography variant={"caption"} color={"secondary"}>
173
194
  {entry.action === "create" ? "Entity created" :
174
195
  entry.action === "delete" ? "Entity deleted" : "No field changes recorded"}
175
196
  </Typography>
@@ -4,7 +4,7 @@ import { useRef, useEffect } from "react";
4
4
  import { cls, IconButton, Label, Tooltip, Typography } from "@rebasepro/ui";
5
5
  import { ErrorBoundary, HistoryIcon } from "@rebasepro/ui";
6
6
  import { EntityHistoryEntry } from "./EntityHistoryEntry";
7
- import { useSnackbarController, useAuthController } from "@rebasepro/app";
7
+ import { useSnackbarController, useAuthController, useTranslation } from "@rebasepro/app";
8
8
  import { ConfirmationDialog } from "@rebasepro/app";
9
9
  import { useState } from "react";
10
10
  import { useHistory } from "../../hooks/useHistory";
@@ -16,9 +16,14 @@ import { useHistory } from "../../hooks/useHistory";
16
16
  export function EntityHistoryView<M extends Record<string, unknown>>({
17
17
  entity,
18
18
  collection,
19
- formContext
20
- }: EntityCustomViewParams<M>) {
21
-
19
+ formContext,
20
+ refreshToken
21
+ }: EntityCustomViewParams<M> & {
22
+ /** Bumped once per save, so the list picks up the revision it just made. */
23
+ refreshToken?: number;
24
+ }) {
25
+
26
+ const { t } = useTranslation();
22
27
  const snackbarController = useSnackbarController();
23
28
  const authController = useAuthController();
24
29
  const dirty = formContext?.formex.dirty;
@@ -36,7 +41,8 @@ export function EntityHistoryView<M extends Record<string, unknown>>({
36
41
  slug,
37
42
  entityId,
38
43
  enabled: !!entityId,
39
- pageSize: 10
44
+ pageSize: 10,
45
+ refreshToken
40
46
  });
41
47
 
42
48
  const [revertHistoryId, setRevertHistoryId] = useState<string | undefined>();
@@ -72,7 +78,7 @@ export function EntityHistoryView<M extends Record<string, unknown>>({
72
78
 
73
79
  if (!entity) {
74
80
  return <div className="flex items-center justify-center h-full">
75
- <Label>History is only available for existing entities</Label>
81
+ <Label>{t("history_new_entity") ?? "History is only available for existing records"}</Label>
76
82
  </div>;
77
83
  }
78
84
 
@@ -93,13 +99,13 @@ export function EntityHistoryView<M extends Record<string, unknown>>({
93
99
  }
94
100
 
95
101
  snackbarController.open({
96
- message: "Reverted to selected version",
102
+ message: t("history_reverted") ?? "Reverted to selected version",
97
103
  type: "info"
98
104
  });
99
105
  } catch (error) {
100
106
  console.error("Error reverting entity:", error);
101
107
  snackbarController.open({
102
- message: "Error reverting entity",
108
+ message: t("history_revert_error") ?? "Error reverting entity",
103
109
  type: "error"
104
110
  });
105
111
  } finally {
@@ -111,17 +117,16 @@ export function EntityHistoryView<M extends Record<string, unknown>>({
111
117
  ? entries.find(e => e.id === revertHistoryId)
112
118
  : undefined;
113
119
 
120
+ // `p-8` plus a `mt-24` heading were sized for a full-width tab that no
121
+ // longer exists. In the inspector panel that was 96px of nothing above the
122
+ // first revision, and 32px of gutter on each side of a 448px column.
114
123
  return <div
115
124
  ref={containerRef}
116
- className={cls("relative flex-1 h-full overflow-auto w-full flex flex-col gap-4 p-8")}>
125
+ className={cls("relative flex-1 h-full overflow-auto w-full flex flex-col gap-4 p-4 sm:p-5")}>
117
126
  <div className="flex flex-col gap-2 max-w-6xl mx-auto w-full">
118
127
 
119
- <Typography variant={"h5"} className={"mt-24 ml-4"}>
120
- History
121
- </Typography>
122
-
123
128
  {isLoading && entries.length === 0 && (
124
- <div className="flex flex-col gap-4 mt-8 ml-4">
129
+ <div className="flex flex-col gap-4">
125
130
  {[1, 2, 3].map(i => (
126
131
  <div key={i} className="flex flex-col gap-2 animate-pulse">
127
132
  <div className="h-4 w-48 bg-surface-200 dark:bg-surface-700 rounded"/>
@@ -132,28 +137,28 @@ export function EntityHistoryView<M extends Record<string, unknown>>({
132
137
  )}
133
138
 
134
139
  {!isLoading && entries.length === 0 && <>
135
- <Label className={"ml-4 mt-8"}>
136
- No history available
140
+ <Label>
141
+ {t("history_empty") ?? "No history available"}
137
142
  </Label>
138
- <Typography variant={"caption"} className={"ml-4"}>
139
- When you save a entity, a new version is created and stored in the history.
143
+ <Typography variant={"caption"}>
144
+ {t("history_empty_description") ?? "When you save a record, a new version is created and stored in the history."}
140
145
  </Typography>
141
146
  </>}
142
147
 
143
148
  {entries.map((entry) => (
144
- <div key={entry.id} className="flex flex-cols gap-2 w-full">
149
+ <div key={entry.id} className="flex flex-col gap-2 w-full">
145
150
  <EntityHistoryEntry
146
151
  entry={entry}
147
152
  collection={collection}
148
153
  size={"medium"}
149
154
  actions={
150
- <Tooltip title={"Revert to this version"}
155
+ <Tooltip title={t("history_revert") ?? "Revert to this version"}
151
156
  className={"m-2 grow-0 self-start"}>
152
157
  <IconButton
153
158
  onClick={() => {
154
159
  if (dirty) {
155
160
  snackbarController.open({
156
- message: "Please save or discard your changes before reverting",
161
+ message: t("history_revert_dirty") ?? "Please save or discard your changes before reverting",
157
162
  type: "warning"
158
163
  });
159
164
  } else {
@@ -171,8 +176,8 @@ export function EntityHistoryView<M extends Record<string, unknown>>({
171
176
  {/* Load more sentinel */}
172
177
  {entries.length > 0 && (
173
178
  <div ref={loadMoreRef} className="py-4 text-center">
174
- {isLoading && <Label>Loading more...</Label>}
175
- {!hasMore && entries.length > 10 && <Label>No more history available</Label>}
179
+ {isLoading && <Label>{t("loading")}</Label>}
180
+ {!hasMore && entries.length > 10 && <Label>{t("history_no_more") ?? "No more history"}</Label>}
176
181
  </div>
177
182
  )}
178
183
  </div>
@@ -184,7 +189,7 @@ export function EntityHistoryView<M extends Record<string, unknown>>({
184
189
  if (revertHistoryId) doRevert(revertHistoryId);
185
190
  }}
186
191
  onCancel={() => setRevertHistoryId(undefined)}
187
- title={<Typography variant={"subtitle2"}>Revert data to this version?</Typography>}
192
+ title={<Typography variant={"subtitle2"}>{t("history_revert_title") ?? "Revert data to this version?"}</Typography>}
188
193
  body={revertEntry
189
194
  ? <div className="p-4">
190
195
  <Typography variant={"caption"} color={"secondary"}>