@rebasepro/admin 0.13.0 → 0.13.1-canary.g06dbe5b

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 (188) hide show
  1. package/dist/{CollectionEditorDialog-CjXkWZ64.js → CollectionEditorDialog-C3DbFNqp.js} +3 -3
  2. package/dist/{CollectionEditorDialog-CjXkWZ64.js.map → CollectionEditorDialog-C3DbFNqp.js.map} +1 -1
  3. package/dist/{PropertyEditView-W8I3YV89.js → PropertyEditView-DFH8rXRN.js} +5 -5
  4. package/dist/PropertyEditView-DFH8rXRN.js.map +1 -0
  5. package/dist/{RouterCollectionsStudioView-BsLTNoqX.js → RouterCollectionsStudioView-DiUDOu4b.js} +9 -13
  6. package/dist/RouterCollectionsStudioView-DiUDOu4b.js.map +1 -0
  7. package/dist/collection_editor/serializable_types.d.ts +1 -0
  8. package/dist/collection_editor_ui.js +4 -4
  9. package/dist/components/AdditionalFieldValue.d.ts +25 -0
  10. package/dist/components/ClearFilterSortButton.d.ts +7 -1
  11. package/dist/components/CollectionViewBinding/CollectionViewBinding.d.ts +16 -0
  12. package/dist/components/CollectionViewBinding/EntityCardBinding.d.ts +3 -1
  13. package/dist/components/CollectionViewBinding/SearchExplanation.d.ts +48 -0
  14. package/dist/components/CollectionViewBinding/SearchHighlight.d.ts +101 -0
  15. package/dist/components/CollectionViewBinding/SlotValue.d.ts +63 -0
  16. package/dist/components/CollectionViewBinding/SortButton.d.ts +7 -1
  17. package/dist/components/CollectionViewBinding/SplitListCloseButton.d.ts +17 -4
  18. package/dist/components/CollectionViewBinding/SplitListShowButton.d.ts +12 -0
  19. package/dist/components/CollectionViewBinding/usePreviewSlots.d.ts +55 -5
  20. package/dist/components/DetailViewBinding.d.ts +15 -0
  21. package/dist/components/DrawerNavigationGroup.d.ts +9 -6
  22. package/dist/components/DrawerNavigationItem.d.ts +16 -6
  23. package/dist/components/EditViewBinding.d.ts +30 -1
  24. package/dist/components/EntityDisplayHeader.d.ts +53 -0
  25. package/dist/components/EntityIdentityBar.d.ts +43 -1
  26. package/dist/components/EntityViewBinding.d.ts +60 -6
  27. package/dist/components/RebaseLayout.d.ts +1 -1
  28. package/dist/components/RebaseShell.d.ts +1 -1
  29. package/dist/components/RelationSelector.d.ts +8 -1
  30. package/dist/components/UserSelector.d.ts +8 -1
  31. package/dist/components/app/Scaffold.d.ts +9 -3
  32. package/dist/components/index.d.ts +2 -0
  33. package/dist/data_export/export/export.d.ts +7 -0
  34. package/dist/data_export/export/fetch_export_data.d.ts +48 -0
  35. package/dist/data_export/export/index.d.ts +1 -0
  36. package/dist/data_import/utils/csv.d.ts +30 -0
  37. package/dist/data_import/utils/index.d.ts +2 -0
  38. package/dist/data_import/utils/save_entities.d.ts +38 -0
  39. package/dist/data_import/utils/transforms.d.ts +9 -0
  40. package/dist/{export-J79rMoNj.js → export-Bd5i6Uvm.js} +175 -39
  41. package/dist/export-Bd5i6Uvm.js.map +1 -0
  42. package/dist/form/components/FieldBlock.d.ts +58 -4
  43. package/dist/form/components/FormRail.d.ts +13 -0
  44. package/dist/form/components/FormSections.d.ts +15 -1
  45. package/dist/form/components/useRailVisible.d.ts +5 -4
  46. package/dist/form/form_utils.d.ts +38 -1
  47. package/dist/{history-D7L_JlLO.js → history-D9R6LUrH.js} +4 -4
  48. package/dist/{history-D7L_JlLO.js.map → history-D9R6LUrH.js.map} +1 -1
  49. package/dist/hooks/useEntityDisplay.d.ts +102 -0
  50. package/dist/hooks/useEntityDisplayTitle.d.ts +18 -9
  51. package/dist/{import-BvYl--U4.js → import-pt_j-0G3.js} +6 -5
  52. package/dist/import-pt_j-0G3.js.map +1 -0
  53. package/dist/index.d.ts +2 -2
  54. package/dist/index.js +158 -60
  55. package/dist/index.js.map +1 -1
  56. package/dist/preview/compact_text.d.ts +23 -0
  57. package/dist/preview/components/BooleanPreview.d.ts +3 -1
  58. package/dist/preview/property_previews/ArrayOfStringsPreview.d.ts +1 -1
  59. package/dist/preview/property_previews/ArrayOneOfPreview.d.ts +1 -1
  60. package/dist/preview/property_previews/ArrayPropertyPreview.d.ts +1 -1
  61. package/dist/preview/property_previews/MapPropertyPreview.d.ts +1 -1
  62. package/dist/preview/property_previews/StringPropertyPreview.d.ts +1 -1
  63. package/dist/routes/RebaseRoute.d.ts +1 -1
  64. package/dist/types/components/PropertyPreviewProps.d.ts +19 -0
  65. package/dist/util/number_format.d.ts +14 -0
  66. package/dist/util/previews.d.ts +9 -2
  67. package/dist/util/view_constants.d.ts +24 -0
  68. package/dist/util/view_mode.d.ts +18 -0
  69. package/dist/{util-GiwPgxnL.js → util-nX_-JRuF.js} +3984 -1774
  70. package/dist/util-nX_-JRuF.js.map +1 -0
  71. package/package.json +10 -9
  72. package/src/collection_editor/serializable_types.ts +1 -0
  73. package/src/collection_editor/ui/collection_editor/CollectionsStudioView.tsx +9 -11
  74. package/src/collection_editor/ui/collection_editor/properties/MapPropertyField.tsx +1 -1
  75. package/src/collection_editor/ui/collection_editor/properties/advanced/AdvancedPropertyValidation.tsx +5 -2
  76. package/src/collection_editor/useLocalCollectionsConfigController.tsx +10 -3
  77. package/src/components/AdditionalFieldValue.tsx +68 -0
  78. package/src/components/ClearFilterSortButton.tsx +16 -7
  79. package/src/components/CollectionTableBinding/CollectionTableBinding.tsx +7 -8
  80. package/src/components/CollectionTableBinding/PropertyTableCell.tsx +2 -2
  81. package/src/components/CollectionTableBinding/column_utils.tsx +15 -6
  82. package/src/components/CollectionTableBinding/internal/popup_field/PopupFormField.tsx +2 -2
  83. package/src/components/CollectionTableBinding/table_bindings.tsx +7 -1
  84. package/src/components/CollectionViewBinding/BoardCardBinding.tsx +26 -27
  85. package/src/components/CollectionViewBinding/CollectionBoardViewBinding.tsx +13 -7
  86. package/src/components/CollectionViewBinding/CollectionCardViewBinding.tsx +2 -1
  87. package/src/components/CollectionViewBinding/CollectionListViewBinding.tsx +822 -354
  88. package/src/components/CollectionViewBinding/CollectionViewActions.tsx +1 -1
  89. package/src/components/CollectionViewBinding/CollectionViewBinding.tsx +51 -12
  90. package/src/components/CollectionViewBinding/CollectionViewStartActions.tsx +9 -0
  91. package/src/components/CollectionViewBinding/EntityCardBinding.tsx +51 -39
  92. package/src/components/CollectionViewBinding/SearchExplanation.tsx +106 -0
  93. package/src/components/CollectionViewBinding/SearchHighlight.tsx +311 -0
  94. package/src/components/CollectionViewBinding/SlotValue.tsx +156 -0
  95. package/src/components/CollectionViewBinding/SortButton.tsx +16 -4
  96. package/src/components/CollectionViewBinding/SplitListCloseButton.tsx +23 -7
  97. package/src/components/CollectionViewBinding/SplitListShowButton.tsx +28 -0
  98. package/src/components/CollectionViewBinding/SplitListView.tsx +47 -7
  99. package/src/components/CollectionViewBinding/ViewModeToggle.tsx +10 -5
  100. package/src/components/CollectionViewBinding/useBoardDataController.tsx +23 -6
  101. package/src/components/CollectionViewBinding/usePreviewSlots.ts +183 -64
  102. package/src/components/CollectionViewBinding/utils.ts +7 -4
  103. package/src/components/DefaultDrawer.tsx +19 -23
  104. package/src/components/DetailViewBinding.tsx +109 -62
  105. package/src/components/DrawerNavigationGroup.tsx +16 -39
  106. package/src/components/DrawerNavigationItem.tsx +44 -16
  107. package/src/components/EditViewBinding.tsx +233 -50
  108. package/src/components/EntityDisplayHeader.tsx +170 -0
  109. package/src/components/EntityIdentityBar.tsx +163 -23
  110. package/src/components/EntityInspector.tsx +24 -6
  111. package/src/components/EntityPreviewBinding.tsx +58 -18
  112. package/src/components/EntityViewBinding.tsx +344 -41
  113. package/src/components/InlineEntityPreview.tsx +10 -10
  114. package/src/components/RebaseLayout.tsx +2 -2
  115. package/src/components/RebaseShell.tsx +2 -2
  116. package/src/components/ReferenceTable/SelectionTableBinding.tsx +4 -2
  117. package/src/components/RelationSelector.tsx +35 -10
  118. package/src/components/SideDialogs.tsx +15 -3
  119. package/src/components/SidePanelBinding.tsx +122 -66
  120. package/src/components/UserSelector.tsx +67 -13
  121. package/src/components/app/Scaffold.tsx +112 -10
  122. package/src/components/history/LastEditedByIndicator.tsx +7 -12
  123. package/src/components/index.ts +3 -0
  124. package/src/data_export/export/ExportCollectionAction.tsx +101 -48
  125. package/src/data_export/export/export.ts +63 -16
  126. package/src/data_export/export/fetch_export_data.ts +90 -0
  127. package/src/data_export/export/index.ts +1 -0
  128. package/src/data_import/components/ImportSaveInProgress.tsx +35 -36
  129. package/src/data_import/import/ImportCollectionAction.tsx +13 -2
  130. package/src/data_import/utils/csv.ts +153 -0
  131. package/src/data_import/utils/data.ts +16 -2
  132. package/src/data_import/utils/file_to_json.ts +51 -5
  133. package/src/data_import/utils/index.ts +2 -0
  134. package/src/data_import/utils/save_entities.ts +138 -0
  135. package/src/data_import/utils/transforms.ts +23 -0
  136. package/src/form/EntityForm.tsx +74 -53
  137. package/src/form/EntityFormBinding.tsx +24 -11
  138. package/src/form/PropertyFieldBinding.tsx +2 -2
  139. package/src/form/components/FieldBlock.tsx +80 -10
  140. package/src/form/components/FormRail.tsx +19 -2
  141. package/src/form/components/FormSections.tsx +32 -5
  142. package/src/form/components/StorageUploadProgress.tsx +9 -3
  143. package/src/form/components/useRailVisible.tsx +5 -4
  144. package/src/form/field_bindings/ArrayCustomShapedFieldBinding.tsx +2 -2
  145. package/src/form/field_bindings/BinaryFieldBinding.tsx +10 -5
  146. package/src/form/field_bindings/BlockFieldBinding.tsx +3 -3
  147. package/src/form/field_bindings/GeopointFieldBinding.tsx +23 -16
  148. package/src/form/field_bindings/MapFieldBinding.tsx +2 -2
  149. package/src/form/field_bindings/MarkdownEditorFieldBinding.tsx +13 -7
  150. package/src/form/field_bindings/MultiSelectFieldBinding.tsx +7 -1
  151. package/src/form/field_bindings/ReadOnlyFieldBinding.tsx +21 -2
  152. package/src/form/field_bindings/ReferenceFieldBinding.tsx +1 -1
  153. package/src/form/field_bindings/RelationFieldBinding.tsx +5 -2
  154. package/src/form/field_bindings/RepeatFieldBinding.tsx +3 -3
  155. package/src/form/field_bindings/StorageUploadFieldBinding.tsx +2 -2
  156. package/src/form/field_bindings/UserSelectFieldBinding.tsx +4 -1
  157. package/src/form/field_bindings/VectorFieldBinding.tsx +6 -4
  158. package/src/form/form_utils.ts +78 -0
  159. package/src/hooks/navigation/useBuildCollectionRegistryController.tsx +29 -5
  160. package/src/hooks/useEntityDisplay.tsx +262 -0
  161. package/src/hooks/useEntityDisplayTitle.tsx +27 -38
  162. package/src/index.ts +9 -1
  163. package/src/preview/PropertyPreview.tsx +25 -5
  164. package/src/preview/compact_text.ts +68 -0
  165. package/src/preview/components/BooleanPreview.tsx +5 -2
  166. package/src/preview/components/ReferencePreview.tsx +5 -8
  167. package/src/preview/components/RelationPreview.tsx +8 -7
  168. package/src/preview/components/StorageThumbnail.tsx +5 -2
  169. package/src/preview/property_previews/ArrayOfRelationsPreview.tsx +6 -3
  170. package/src/preview/property_previews/ArrayOfStringsPreview.tsx +26 -1
  171. package/src/preview/property_previews/ArrayOneOfPreview.tsx +10 -1
  172. package/src/preview/property_previews/ArrayPropertyPreview.tsx +10 -1
  173. package/src/preview/property_previews/MapPropertyPreview.tsx +52 -1
  174. package/src/preview/property_previews/NumberPropertyPreview.tsx +9 -1
  175. package/src/preview/property_previews/StringPropertyPreview.tsx +8 -5
  176. package/src/routes/RebaseRoute.tsx +62 -4
  177. package/src/types/components/PropertyPreviewProps.tsx +21 -0
  178. package/src/util/navigation_utils.ts +4 -2
  179. package/src/util/number_format.ts +46 -0
  180. package/src/util/previews.ts +8 -55
  181. package/src/util/property_utils.tsx +6 -1
  182. package/src/util/view_constants.ts +27 -0
  183. package/src/util/view_mode.ts +36 -0
  184. package/dist/PropertyEditView-W8I3YV89.js.map +0 -1
  185. package/dist/RouterCollectionsStudioView-BsLTNoqX.js.map +0 -1
  186. package/dist/export-J79rMoNj.js.map +0 -1
  187. package/dist/import-BvYl--U4.js.map +0 -1
  188. package/dist/util-GiwPgxnL.js.map +0 -1
@@ -6,10 +6,10 @@ import { DetailViewBinding } from "../components/DetailViewBinding";
6
6
  import { useCallback, useEffect, useRef, useState } from "react";
7
7
  import { useNavigate } from "react-router";
8
8
  import { CollectionViewBinding } from "../components/CollectionViewBinding/CollectionViewBinding";
9
- import { NotFoundPage, useUserConfigurationPersistence, useComponentOverride, useNavigationBlocker } from "@rebasepro/app";
9
+ import { ErrorView, NotFoundPage, useUserConfigurationPersistence, useComponentOverride, useNavigationBlocker } from "@rebasepro/app";
10
10
  import { resolveOpenEntityMode, resolveViewMode } from "../util/view_mode";
11
11
  import { UnsavedChangesDialog } from "@rebasepro/app";
12
- import { CircularProgressCenter } from "@rebasepro/ui";
12
+ import { CenteredView, CircularProgressCenter } from "@rebasepro/ui";
13
13
  import { NavigationViewCollectionInternal, NavigationViewEntityCustomInternal, NavigationViewInternal } from "@rebasepro/app";
14
14
  import { getNavigationEntriesFromPath } from "@rebasepro/app";
15
15
  import { toArray } from "@rebasepro/utils";
@@ -94,7 +94,7 @@ export function RebaseRoute() {
94
94
  if (!collectionRegistry.initialised) {
95
95
  return <CircularProgressCenter/>;
96
96
  }
97
- return null;
97
+ return <UnresolvedCollectionView path={navigationEntries[0].slug}/>;
98
98
  }
99
99
  return <ResolvedCollectionView
100
100
  key={`collection_view_${collection.slug}`}
@@ -117,7 +117,7 @@ export function RebaseRoute() {
117
117
  if (!collectionRegistry.initialised) {
118
118
  return <CircularProgressCenter/>;
119
119
  }
120
- return null;
120
+ return <UnresolvedCollectionView path={firstEntry.slug}/>;
121
121
  }
122
122
  return <ResolvedCollectionView
123
123
  key={`collection_view_${collection.slug}`}
@@ -233,6 +233,7 @@ function EntityFullScreenRoute({
233
233
  const urlController = useUrlController();
234
234
  const navigate = useNavigate();
235
235
  const location = useLocation();
236
+ const userConfigPersistence = useUserConfigurationPersistence();
236
237
 
237
238
  // defaultValues may be carried via location.state when openNewDocument() is called
238
239
  // for full-screen mode. We read it once on mount — after that, the form owns its state.
@@ -340,6 +341,30 @@ function EntityFullScreenRoute({
340
341
  // Determine if this is a detail-view-first collection showing the view page
341
342
  const isDetailMode = collection.defaultEntityAction === "view" && !isNew && !isCopy && entityId && !isEditRoute;
342
343
 
344
+ // A record is full screen either because its collection has no list to show
345
+ // beside it, or because the user folded that list away. Only the second is
346
+ // reversible, and it is reversible by dropping `#full` alone: this is the
347
+ // same URL the split route reads, so the same conditions decide it.
348
+ const splitListAvailable = !isNew && !isCopy && entityId !== undefined &&
349
+ navigationEntries[0]?.type === "collection" &&
350
+ (navigationEntries.length === 2 || navigationEntries.length === 3) &&
351
+ resolveOpenEntityMode({
352
+ collection,
353
+ viewMode: resolveViewMode({
354
+ collection,
355
+ search: location.search,
356
+ savedViewMode: userConfigPersistence?.getCollectionConfig(collection.slug)?.defaultViewMode as ViewMode | undefined
357
+ })
358
+ }) === "split";
359
+
360
+ const showList = splitListAvailable
361
+ ? () => navigate({
362
+ pathname,
363
+ search: location.search,
364
+ hash: ""
365
+ })
366
+ : undefined;
367
+
343
368
  if (isDetailMode) {
344
369
  return <>
345
370
  <DetailViewBinding
@@ -349,6 +374,7 @@ function EntityFullScreenRoute({
349
374
  layout={"full_screen"}
350
375
  path={collectionPath}
351
376
  selectedTab={selectedTab ?? undefined}
377
+ onShowList={showList}
352
378
  onEditClick={() => {
353
379
  const editUrl = urlController.buildUrlCollectionPath(`${collectionPath}/${entityId}`) + "/edit";
354
380
  navigate(editUrl + hash);
@@ -377,6 +403,7 @@ function EntityFullScreenRoute({
377
403
  path={collectionPath}
378
404
  copy={isCopy}
379
405
  selectedTab={selectedTab ?? undefined}
406
+ onShowList={showList}
380
407
  defaultValues={isNew ? defaultValues : undefined}
381
408
  onValuesModified={(modified) => blocked.current = modified}
382
409
  navigateBack={() => {
@@ -415,3 +442,34 @@ function EntityFullScreenRoute({
415
442
  body={"You have unsaved changes in this entity."}/>
416
443
  </>;
417
444
  }
445
+
446
+ /**
447
+ * Shown when a URL names a collection the registry cannot resolve.
448
+ *
449
+ * This used to be `return null`, which renders an empty pane inside the app
450
+ * chrome: the sidebar, the nav highlight and the breadcrumb all still work,
451
+ * because those read the collections array directly, while the view itself is
452
+ * blank. Nothing is thrown, and the only trace is a `console.debug` — so the
453
+ * panel looks like it lost its data rather than like it failed to find the
454
+ * collection, and there is no string to search for.
455
+ *
456
+ * A slug containing slashes went unresolvable this way and cost an afternoon to
457
+ * find. The lookup is fixed; this is the part that made it expensive.
458
+ */
459
+ function UnresolvedCollectionView({ path }: { path: string }) {
460
+ // Warn, not debug: this is a route that renders nothing useful, and it
461
+ // should be visible at the console's default level.
462
+ useEffect(() => {
463
+ console.warn(
464
+ `[rebase] No collection is registered for "${path}", so this route has nothing to render. ` +
465
+ "Check that the collection is in the collections array and that its slug matches the URL."
466
+ );
467
+ }, [path]);
468
+
469
+ return <CenteredView>
470
+ <ErrorView
471
+ title={"Collection not found"}
472
+ error={`Nothing is registered for "${path}".`}
473
+ tooltip={"The URL names a collection the panel does not know about. It may have been renamed or removed from the collections array, or the slug may not match the path."}/>
474
+ </CenteredView>;
475
+ }
@@ -61,4 +61,25 @@ export interface PropertyPreviewProps<P extends Property | Property, CustomProps
61
61
  */
62
62
  textOnly?: boolean;
63
63
 
64
+ /**
65
+ * The preview has one line and cannot grow — it fills a slot in a card, a
66
+ * chip, or a table cell of fixed height.
67
+ *
68
+ * Properties whose natural rendering is a block (Markdown, multi-line text,
69
+ * maps, arrays) render a one-line stand-in instead: an opening excerpt, or
70
+ * a count. Left unset, {@link PropertyPreview} infers it from the entity
71
+ * preview nesting depth, which is where the constraint actually comes from;
72
+ * pass it explicitly only to force the compact form somewhere the nesting
73
+ * does not say so.
74
+ */
75
+ compact?: boolean;
76
+
77
+ /**
78
+ * The caller has already labelled this value, so the preview must not
79
+ * repeat the property name. A boolean is the one that does: it renders its
80
+ * name beside the checkbox, which reads as a caption in a table cell and as
81
+ * a stutter under a field label — "VIP" over a checkbox saying "VIP".
82
+ */
83
+ hideLabel?: boolean;
84
+
64
85
  }
@@ -1,6 +1,6 @@
1
1
 
2
2
  import type { UrlController, SidePanelController, NavigateOptions, AdminCollection } from "@rebasepro/admin-types";
3
- import { withViewMode } from "./view_mode";
3
+ import { withListState, withViewMode } from "./view_mode";
4
4
 
5
5
  // Canonical path utilities — single source of truth in @rebasepro/common
6
6
  export { removeInitialAndTrailingSlashes, removeInitialSlash, removeTrailingSlash, addInitialSlash, getLastSegment, resolveCollectionPathIds, getCollectionBySlugWithin, getCollectionPathsCombinations } from "@rebasepro/app";
@@ -63,7 +63,9 @@ export function navigateToEntity({
63
63
  if (entityId && selectedTab) {
64
64
  to += `/${selectedTab}`;
65
65
  }
66
- to = withViewMode(to);
66
+ // The whole list state, not just the view mode: a record opened from a
67
+ // search must come back to that search.
68
+ to = withListState(to);
67
69
  if (!entityId) {
68
70
  to += "#new";
69
71
  }
@@ -0,0 +1,46 @@
1
+ import type { NumberFormatOptions } from "@rebasepro/admin-types";
2
+
3
+ /**
4
+ * Write a number out for reading, per a property's declared `admin.format`.
5
+ *
6
+ * Nothing is inferred here. With no options the number is returned as it came
7
+ * out of the database — `String(value)`, not a locale-grouped rendering — so a
8
+ * panel that declares nothing shows exactly the stored value, and an author who
9
+ * wants thousands separators asks for them.
10
+ *
11
+ * `Intl.NumberFormat` throws on a bad currency code or an out-of-range fraction
12
+ * count, and a record must not blank because one property is misconfigured, so a
13
+ * failure falls back to the raw value.
14
+ */
15
+ export function formatNumber(
16
+ value: number,
17
+ options: NumberFormatOptions | undefined,
18
+ fallbackLocale?: string
19
+ ): string {
20
+
21
+ if (!options) return String(value);
22
+
23
+ // A currency code is the whole declaration in the common case: writing
24
+ // `{ currency: "EUR" }` and getting a bare `185.11` because `style` was
25
+ // omitted is a trap, and there is nothing else `currency` could mean.
26
+ const style = options.style ?? (options.currency ? "currency" : "decimal");
27
+
28
+ if (style === "currency" && !options.currency) {
29
+ // `Intl` throws on this rather than picking a currency, which is the
30
+ // right call — there is no sensible default — but the record still has
31
+ // to render.
32
+ return String(value);
33
+ }
34
+
35
+ try {
36
+ return new Intl.NumberFormat(options.locale ?? fallbackLocale ?? undefined, {
37
+ style,
38
+ currency: options.currency,
39
+ minimumFractionDigits: options.minimumFractionDigits,
40
+ maximumFractionDigits: options.maximumFractionDigits,
41
+ notation: options.notation
42
+ }).format(value);
43
+ } catch {
44
+ return String(value);
45
+ }
46
+ }
@@ -1,64 +1,17 @@
1
1
  import type { Property } from "@rebasepro/types";
2
2
  import type { PropertyConfig, AdminCollection } from "@rebasepro/admin-types";
3
- import { AuthController } from "@rebasepro/admin-types";
4
- import { isPropertyBuilder } from "@rebasepro/common";
5
3
  import { getTitlePropertyKey, getTitlePropertyKeyForValues } from "@rebasepro/app";
6
- import { isReferenceProperty, isRelationProperty } from "./property_utils";
7
-
8
- function isHiddenProperty(property: Property | undefined): boolean {
9
- if (!property) return false;
10
- return Boolean(property.admin?.hideFromCollection);
11
- }
4
+ import { isPropertyBuilder } from "@rebasepro/common";
12
5
 
13
6
  /**
14
- * Returns true when the property holds file-storage content (single image,
15
- * array of images, generic upload, …). These properties are rendered by the
16
- * dedicated image-slot and should NOT appear as regular preview columns.
7
+ * Which properties fill a preview surface, best first.
8
+ *
9
+ * The implementation lives in `@rebasepro/app` so that the admin package and
10
+ * the app package cannot disagree about what a record looks like in a card —
11
+ * they had drifted, and only one of the two copies had learned to keep
12
+ * relations out of the list.
17
13
  */
18
- function isStorageProperty(property: Property | undefined): boolean {
19
- if (!property) return false;
20
- // Single string with storage config
21
- if (property.type === "string" && property.storage) return true;
22
- // String displayed as image URL
23
- if (property.type === "string" && property.admin?.urlPreview === "image") return true;
24
- // Array whose inner element has storage config or image URL
25
- if (property.type === "array" && property.of && !Array.isArray(property.of)) {
26
- const inner = property.of;
27
- if (inner.type === "string" && (inner.storage || inner.admin?.urlPreview === "image")) return true;
28
- }
29
- return false;
30
- }
31
-
32
- export function getEntityPreviewKeys(
33
- authController: AuthController,
34
- targetCollection: AdminCollection<any>,
35
- fields: Record<string, PropertyConfig>,
36
- previewProperties?: string[],
37
- limit = 3) {
38
- const allProperties = Object.keys(targetCollection.properties);
39
- let listProperties = previewProperties?.filter(p => allProperties.includes(p as string));
40
- if (!listProperties && targetCollection.previewProperties) {
41
- listProperties = targetCollection.previewProperties?.filter(p => allProperties.includes(p as string));
42
- }
43
- if (listProperties && listProperties.length > 0) {
44
- return listProperties;
45
- } else {
46
- const hasExplicitOrder = !!(targetCollection.propertiesOrder as string[] | undefined);
47
- listProperties = (targetCollection.propertiesOrder as string[]) || allProperties;
48
- return listProperties
49
- .filter(key => {
50
- const prop = targetCollection.properties[key];
51
- const isIdProp = prop && typeof prop === "object" && "isId" in prop && Boolean((prop as { isId?: boolean }).isId);
52
- return !isIdProp && key !== "id";
53
- })
54
- .filter(key => {
55
- const property = targetCollection.properties[key];
56
- return property && !isPropertyBuilder(property) && !isReferenceProperty(property) && !isHiddenProperty(property)
57
- && !isStorageProperty(property as Property)
58
- && (hasExplicitOrder || !isRelationProperty(property));
59
- }).slice(0, limit);
60
- }
61
- }
14
+ export { getEntityPreviewKeys } from "@rebasepro/app";
62
15
 
63
16
  /**
64
17
  * The property that fills the title slot for a collection.
@@ -159,8 +159,13 @@ export function getResolvedPropertyInPath(properties: Record<string, Property>,
159
159
  }
160
160
  if (path.includes(".")) {
161
161
  const pathSegments = path.split(".");
162
+ // A path whose root is not a property at all is simply not found.
163
+ // Reading `.type` off the miss threw a TypeError instead, which
164
+ // turned one unresolvable column key into a crash for whatever was
165
+ // walking the properties — see `propertiesToColumns`, where that is
166
+ // the whole table. `getPropertyInPath` above already guards this.
162
167
  const childProperty = properties[pathSegments[0]];
163
- if (childProperty.type === "map" && childProperty.properties) {
168
+ if (childProperty?.type === "map" && childProperty.properties) {
164
169
  return getResolvedPropertyInPath(childProperty.properties, pathSegments.slice(1).join("."))
165
170
  }
166
171
  }
@@ -4,3 +4,30 @@
4
4
  export const MAIN_TAB_VALUE = "__main_##Q$SC^#S6";
5
5
  export const JSON_TAB_VALUE = "__json";
6
6
  export const HISTORY_TAB_VALUE = "__rebase_history";
7
+
8
+ /**
9
+ * The tab row belongs to the header band above the record, not to the record
10
+ * itself, so it carries the same surface the identity bar over it and the form
11
+ * rail beside it carry. Against that, the active tab takes the record's own
12
+ * white and reads as connected to what it opens; on a white row it had to be
13
+ * the tinted one, which is the relationship backwards.
14
+ */
15
+ export const ENTITY_VIEW_TAB_BAR_CLASS = "bg-surface-50 dark:bg-surface-900";
16
+
17
+ /**
18
+ * Every tab in that row, a custom `tabComponent` included.
19
+ *
20
+ * `h-full` over the variant's `h-9`: the row is 40px and the tab 36px, so a
21
+ * centred tab floated ~2px clear of the row's bottom border and the active
22
+ * tab's underline stopped short of the record it opens. Paired with
23
+ * `items-stretch` on the Tabs root, which the variant leaves centred.
24
+ */
25
+ export const ENTITY_VIEW_TAB_CLASS = "h-full data-[state=active]:bg-white dark:data-[state=active]:bg-surface-800";
26
+
27
+ /**
28
+ * Added on top for the tabs this row labels itself — a custom `tabComponent`
29
+ * sizes its own contents. The main tab was the only one rendered without it,
30
+ * and so the only one left at the boxy variant's `text-xs` while its
31
+ * neighbours sat at `text-sm`.
32
+ */
33
+ export const ENTITY_VIEW_TAB_LABEL_CLASS = "text-sm min-w-[90px]";
@@ -58,6 +58,42 @@ export function withViewMode(url: string, search?: string): string {
58
58
  return `${base}${separator}${VIEW_MODE_PARAM}=${viewMode}${hash}`;
59
59
  }
60
60
 
61
+ /**
62
+ * Carry the whole of the collection list's URL state onto a navigation target.
63
+ *
64
+ * `withViewMode` exists because losing the view mode mid-session is jarring.
65
+ * Everything else the list writes to the URL — the search string, the filters,
66
+ * the sort — is state the user built up deliberately, and losing *that* is
67
+ * worse: opening a record from a search and pressing back returned an
68
+ * unfiltered list with an empty search box, so the search had to be retyped
69
+ * for every record they wanted to look at.
70
+ *
71
+ * Copies every param the list currently has rather than an allowlist, because
72
+ * filters are encoded under the field's own name — there is no fixed set to
73
+ * list. Params already on the target win: the caller is more specific.
74
+ *
75
+ * @param url the URL to decorate. May carry a query string and/or a hash.
76
+ * @param search query string to read from. Defaults to the browser location.
77
+ */
78
+ export function withListState(url: string, search?: string): string {
79
+ const source = search ?? (typeof window !== "undefined" ? window.location.search : "");
80
+ const carried = new URLSearchParams(source);
81
+ if ([...carried].length === 0) return url;
82
+
83
+ const hashIndex = url.indexOf("#");
84
+ const hash = hashIndex >= 0 ? url.substring(hashIndex) : "";
85
+ const base = hashIndex >= 0 ? url.substring(0, hashIndex) : url;
86
+
87
+ const [pathname, existing] = base.split("?");
88
+ const target = new URLSearchParams(existing ?? "");
89
+ for (const [key, value] of carried) {
90
+ if (!target.has(key)) target.set(key, value);
91
+ }
92
+
93
+ const query = target.toString();
94
+ return query ? `${pathname}?${query}${hash}` : `${pathname}${hash}`;
95
+ }
96
+
61
97
  /**
62
98
  * Resolve the view mode for a collection, honouring the documented priority:
63
99
  * URL param > saved user config > collection default.