@rebasepro/admin 0.13.1-canary.gef9608c → 0.14.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 (148) hide show
  1. package/dist/{CollectionEditorDialog-7b-kSLno.js → CollectionEditorDialog-CSheud_E.js} +19 -9
  2. package/dist/CollectionEditorDialog-CSheud_E.js.map +1 -0
  3. package/dist/{PropertyEditView-C-31uvpG.js → PropertyEditView-CWZlC1gq.js} +5 -5
  4. package/dist/PropertyEditView-CWZlC1gq.js.map +1 -0
  5. package/dist/{RouterCollectionsStudioView-BJylABq3.js → RouterCollectionsStudioView-CwTpdzWF.js} +9 -13
  6. package/dist/RouterCollectionsStudioView-CwTpdzWF.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/ClearFilterSortButton.d.ts +7 -1
  10. package/dist/components/CollectionViewBinding/EntityCardBinding.d.ts +3 -1
  11. package/dist/components/CollectionViewBinding/SearchExplanation.d.ts +48 -0
  12. package/dist/components/CollectionViewBinding/SearchHighlight.d.ts +101 -0
  13. package/dist/components/CollectionViewBinding/SlotValue.d.ts +26 -0
  14. package/dist/components/CollectionViewBinding/SortButton.d.ts +7 -1
  15. package/dist/components/CollectionViewBinding/SplitListCloseButton.d.ts +17 -6
  16. package/dist/components/CollectionViewBinding/SplitListShowButton.d.ts +1 -1
  17. package/dist/components/CollectionViewBinding/usePreviewSlots.d.ts +26 -1
  18. package/dist/components/DetailViewBinding.d.ts +7 -0
  19. package/dist/components/DrawerNavigationGroup.d.ts +4 -2
  20. package/dist/components/DrawerNavigationItem.d.ts +13 -5
  21. package/dist/components/EditViewBinding.d.ts +22 -7
  22. package/dist/components/EntityIdentityBar.d.ts +24 -1
  23. package/dist/components/RelationSelector.d.ts +13 -0
  24. package/dist/data_export/export/export.d.ts +7 -0
  25. package/dist/data_export/export/fetch_export_data.d.ts +48 -0
  26. package/dist/data_export/export/index.d.ts +1 -0
  27. package/dist/data_import/utils/csv.d.ts +30 -0
  28. package/dist/data_import/utils/index.d.ts +2 -0
  29. package/dist/data_import/utils/save_entities.d.ts +38 -0
  30. package/dist/data_import/utils/transforms.d.ts +9 -0
  31. package/dist/editor.js +3 -0
  32. package/dist/editor.js.map +1 -1
  33. package/dist/{export-C-XG3aRh.js → export-L53WektO.js} +175 -39
  34. package/dist/export-L53WektO.js.map +1 -0
  35. package/dist/form/form_utils.d.ts +38 -1
  36. package/dist/{history-DfMuvPfr.js → history-D5SKygpJ.js} +2 -2
  37. package/dist/{history-DfMuvPfr.js.map → history-D5SKygpJ.js.map} +1 -1
  38. package/dist/{import-B0GyllB0.js → import-C_4edkoD.js} +5 -4
  39. package/dist/import-C_4edkoD.js.map +1 -0
  40. package/dist/index.d.ts +2 -2
  41. package/dist/index.js +96 -80
  42. package/dist/index.js.map +1 -1
  43. package/dist/preview/components/date_fns_locales.d.ts +9 -0
  44. package/dist/util/view_constants.d.ts +24 -0
  45. package/dist/util/view_mode.d.ts +18 -0
  46. package/dist/{util-DHNXodBo.js → util-C-D5yD2n.js} +2173 -673
  47. package/dist/util-C-D5yD2n.js.map +1 -0
  48. package/package.json +10 -9
  49. package/src/collection_editor/serializable_types.ts +1 -0
  50. package/src/collection_editor/ui/collection_editor/AICollectionGeneratorPopover.tsx +8 -3
  51. package/src/collection_editor/ui/collection_editor/CollectionDetailsForm.tsx +5 -2
  52. package/src/collection_editor/ui/collection_editor/CollectionJsonImportDialog.tsx +1 -0
  53. package/src/collection_editor/ui/collection_editor/CollectionRLSTab.tsx +9 -8
  54. package/src/collection_editor/ui/collection_editor/CollectionsStudioView.tsx +9 -11
  55. package/src/collection_editor/ui/collection_editor/GeneralSettingsForm.tsx +5 -2
  56. package/src/collection_editor/ui/collection_editor/properties/MapPropertyField.tsx +1 -1
  57. package/src/collection_editor/ui/collection_editor/properties/advanced/AdvancedPropertyValidation.tsx +5 -2
  58. package/src/collection_editor/ui/collection_editor/properties/conditions/ConditionsEditor.tsx +1 -0
  59. package/src/collection_editor/useJsonCollectionsConfigController.tsx +92 -84
  60. package/src/collection_editor/useLocalCollectionsConfigController.tsx +10 -3
  61. package/src/components/ClearFilterSortButton.tsx +16 -7
  62. package/src/components/CollectionTableBinding/CollectionTableBinding.tsx +2 -2
  63. package/src/components/CollectionTableBinding/PropertyTableCell.tsx +2 -2
  64. package/src/components/CollectionTableBinding/column_utils.tsx +15 -6
  65. package/src/components/CollectionTableBinding/internal/popup_field/PopupFormField.tsx +2 -2
  66. package/src/components/CollectionTableBinding/table_bindings.tsx +7 -1
  67. package/src/components/CollectionViewBinding/BoardCardBinding.tsx +18 -1
  68. package/src/components/CollectionViewBinding/CollectionBoardViewBinding.tsx +13 -7
  69. package/src/components/CollectionViewBinding/CollectionCardViewBinding.tsx +2 -1
  70. package/src/components/CollectionViewBinding/CollectionListViewBinding.tsx +816 -335
  71. package/src/components/CollectionViewBinding/CollectionViewActions.tsx +1 -1
  72. package/src/components/CollectionViewBinding/CollectionViewBinding.tsx +26 -6
  73. package/src/components/CollectionViewBinding/CollectionViewStartActions.tsx +9 -0
  74. package/src/components/CollectionViewBinding/EntityCardBinding.tsx +45 -10
  75. package/src/components/CollectionViewBinding/SearchExplanation.tsx +106 -0
  76. package/src/components/CollectionViewBinding/SearchHighlight.tsx +311 -0
  77. package/src/components/CollectionViewBinding/SlotValue.tsx +96 -0
  78. package/src/components/CollectionViewBinding/SortButton.tsx +16 -4
  79. package/src/components/CollectionViewBinding/SplitListCloseButton.tsx +23 -9
  80. package/src/components/CollectionViewBinding/SplitListShowButton.tsx +3 -3
  81. package/src/components/CollectionViewBinding/SplitListView.tsx +36 -9
  82. package/src/components/CollectionViewBinding/ViewModeToggle.tsx +10 -5
  83. package/src/components/CollectionViewBinding/useBoardDataController.tsx +11 -3
  84. package/src/components/CollectionViewBinding/usePreviewSlots.ts +76 -24
  85. package/src/components/CollectionViewBinding/utils.ts +7 -4
  86. package/src/components/DefaultDrawer.tsx +7 -0
  87. package/src/components/DetailViewBinding.tsx +33 -9
  88. package/src/components/DrawerNavigationGroup.tsx +12 -32
  89. package/src/components/DrawerNavigationItem.tsx +33 -12
  90. package/src/components/EditViewBinding.tsx +175 -28
  91. package/src/components/EntityIdentityBar.tsx +109 -17
  92. package/src/components/EntityInspector.tsx +24 -6
  93. package/src/components/EntityViewBinding.tsx +4 -2
  94. package/src/components/ReferenceTable/SelectionTableBinding.tsx +4 -2
  95. package/src/components/RelationSelector.tsx +61 -11
  96. package/src/components/SearchIconsView.tsx +14 -33
  97. package/src/components/SelectableTable/filters/StringNumberFilterField.tsx +1 -0
  98. package/src/components/SidePanelBinding.tsx +36 -25
  99. package/src/components/UserSelector.tsx +52 -9
  100. package/src/components/history/LastEditedByIndicator.tsx +7 -12
  101. package/src/data_export/export/ExportCollectionAction.tsx +101 -48
  102. package/src/data_export/export/export.ts +63 -16
  103. package/src/data_export/export/fetch_export_data.ts +90 -0
  104. package/src/data_export/export/index.ts +1 -0
  105. package/src/data_import/components/DataNewPropertiesMapping.tsx +20 -3
  106. package/src/data_import/components/ImportNewPropertyFieldPreview.tsx +1 -0
  107. package/src/data_import/components/ImportSaveInProgress.tsx +35 -36
  108. package/src/data_import/import/ImportCollectionAction.tsx +13 -2
  109. package/src/data_import/utils/csv.ts +153 -0
  110. package/src/data_import/utils/data.ts +16 -2
  111. package/src/data_import/utils/file_to_json.ts +77 -6
  112. package/src/data_import/utils/index.ts +2 -0
  113. package/src/data_import/utils/save_entities.ts +138 -0
  114. package/src/data_import/utils/transforms.ts +23 -0
  115. package/src/editor/components/image-bubble.tsx +2 -0
  116. package/src/editor/selectors/link-selector.tsx +1 -0
  117. package/src/form/EntityForm.tsx +12 -3
  118. package/src/form/EntityFormBinding.tsx +8 -12
  119. package/src/form/PropertyFieldBinding.tsx +2 -2
  120. package/src/form/field_bindings/ArrayCustomShapedFieldBinding.tsx +2 -2
  121. package/src/form/field_bindings/BinaryFieldBinding.tsx +1 -0
  122. package/src/form/field_bindings/BlockFieldBinding.tsx +3 -3
  123. package/src/form/field_bindings/DateTimeFieldBinding.tsx +4 -0
  124. package/src/form/field_bindings/KeyValueFieldBinding.tsx +3 -0
  125. package/src/form/field_bindings/MapFieldBinding.tsx +2 -2
  126. package/src/form/field_bindings/ReadOnlyFieldBinding.tsx +15 -2
  127. package/src/form/field_bindings/RepeatFieldBinding.tsx +3 -3
  128. package/src/form/field_bindings/StorageUploadFieldBinding.tsx +2 -2
  129. package/src/form/field_bindings/TextFieldBinding.tsx +18 -0
  130. package/src/form/field_bindings/VectorFieldBinding.tsx +1 -0
  131. package/src/form/form_utils.ts +78 -0
  132. package/src/index.ts +9 -1
  133. package/src/preview/PropertyPreview.tsx +7 -3
  134. package/src/preview/components/DatePreview.tsx +15 -2
  135. package/src/preview/components/ReferencePreview.tsx +5 -8
  136. package/src/preview/components/RelationPreview.tsx +8 -7
  137. package/src/preview/components/date_fns_locales.ts +125 -0
  138. package/src/preview/property_previews/ArrayOfRelationsPreview.tsx +6 -3
  139. package/src/util/navigation_utils.ts +4 -2
  140. package/src/util/property_utils.tsx +6 -1
  141. package/src/util/view_constants.ts +27 -0
  142. package/src/util/view_mode.ts +36 -0
  143. package/dist/CollectionEditorDialog-7b-kSLno.js.map +0 -1
  144. package/dist/PropertyEditView-C-31uvpG.js.map +0 -1
  145. package/dist/RouterCollectionsStudioView-BJylABq3.js.map +0 -1
  146. package/dist/export-C-XG3aRh.js.map +0 -1
  147. package/dist/import-B0GyllB0.js.map +0 -1
  148. package/dist/util-DHNXodBo.js.map +0 -1
@@ -4,7 +4,7 @@ import { deepEqual as equal } from "fast-equals"
4
4
 
5
5
  import { EntityReference, EntityRelation } from "@rebasepro/types";
6
6
  import type { PropertyPreviewProps } from "../types/components/PropertyPreviewProps";
7
- import { resolveProperty, normalizeToEntityRelation } from "@rebasepro/common";
7
+ import { resolveProperty, normalizeToEntityRelation, getRelationTargetPath } from "@rebasepro/common";
8
8
  import { useAuthController, useCustomizationController, resolveComponentRef } from "@rebasepro/app";
9
9
  import { EmptyValue } from "./components/EmptyValue";
10
10
  import { UrlComponentPreview } from "./components/UrlComponentPreview";
@@ -234,12 +234,16 @@ export const PropertyPreview = React.memo(function PropertyPreview<P extends Pro
234
234
  }
235
235
 
236
236
  } else if (property.type === "relation") {
237
+ // A relation column arrives either hydrated or as the bare foreign key,
238
+ // depending on the fetch path. The declared target resolves the latter;
239
+ // see `normalizeToEntityRelation`.
240
+ const relationTargetPath = getRelationTargetPath(property);
237
241
  if (!value) {
238
242
  content = <EmptyValue/>;
239
243
  } else if (Array.isArray(value)) {
240
244
  // Many-cardinality relation: value is an array of EntityRelation (or plain objects)
241
245
  const relations = (value as unknown[])
242
- .map(item => normalizeToEntityRelation(item, "relation"))
246
+ .map(item => normalizeToEntityRelation(item, "relation", relationTargetPath))
243
247
  .filter(Boolean) as EntityRelation[];
244
248
  const renderRelation = (entityRelation: EntityRelation, index: number) => <RelationPreview
245
249
  key={`preview_rel_${propertyKey}_${index}`}
@@ -268,7 +272,7 @@ export const PropertyPreview = React.memo(function PropertyPreview<P extends Pro
268
272
  );
269
273
  } else {
270
274
  // Single-cardinality relation
271
- const relationValue = normalizeToEntityRelation(value, "relation");
275
+ const relationValue = normalizeToEntityRelation(value, "relation", relationTargetPath);
272
276
  if (relationValue) {
273
277
  content = <RelationPreview
274
278
  disabled={!property.relation}
@@ -1,9 +1,9 @@
1
1
  import React from "react";
2
2
 
3
3
  import { format } from "date-fns";
4
- import * as locales from "date-fns/locale";
5
4
  import { useCustomizationController } from "@rebasepro/app";
6
5
  import { defaultDateFormat } from "@rebasepro/utils";
6
+ import { getLoadedDateFnsLocale, loadDateFnsLocale } from "./date_fns_locales";
7
7
 
8
8
  export interface DatePreviewProps {
9
9
  date: Date;
@@ -28,7 +28,20 @@ export function DatePreview({
28
28
  }: DatePreviewProps): React.ReactElement {
29
29
 
30
30
  const customizationController = useCustomizationController();
31
- const dateUtilsLocale = customizationController?.locale ? (locales as Record<string, locales.Locale>)[customizationController.locale] : undefined;
31
+ const configuredLocale = customizationController?.locale;
32
+
33
+ // The locale arrives asynchronously now (see `date_fns_locales`). Seed from
34
+ // the cache so only the very first date on a page renders un-localised: by
35
+ // the time a table of them mounts, the module is already resolved and there
36
+ // is no second pass.
37
+ const [dateUtilsLocale, setDateUtilsLocale] = React.useState(() => getLoadedDateFnsLocale(configuredLocale));
38
+ React.useEffect(() => {
39
+ let cancelled = false;
40
+ loadDateFnsLocale(configuredLocale)
41
+ .then(loaded => { if (!cancelled) setDateUtilsLocale(loaded); })
42
+ .catch(error => console.error(`[rebase] could not load the "${configuredLocale}" date locale`, error));
43
+ return () => { cancelled = true; };
44
+ }, [configuredLocale]);
32
45
 
33
46
  if (!date || !(date instanceof Date) || isNaN(date.getTime())) {
34
47
  return <></>;
@@ -1,7 +1,7 @@
1
1
 
2
2
  import * as React from "react";
3
3
 
4
- import { Entity, EntityReference } from "@rebasepro/types";
4
+ import { EntityReference } from "@rebasepro/types";
5
5
  import type { PreviewSize } from "../../types/components/PropertyPreviewProps";
6
6
  import { useCustomizationController, useFetch, useComponentOverride, CollectionScopeProvider } from "@rebasepro/app";
7
7
  import { Skeleton } from "@rebasepro/ui";
@@ -157,11 +157,10 @@ function ReferencePreviewExisting<M extends Record<string, unknown> = Record<str
157
157
  useCache: true
158
158
  });
159
159
 
160
- if (entity) {
161
- referencesCache.set(reference.pathWithId, entity);
162
- }
163
-
164
- const usedEntity = entity ?? referencesCache.get(reference.pathWithId);
160
+ // Only what the fetch currently reports — see the note in RelationPreview:
161
+ // the module-level Map that used to back this kept a deleted row on screen
162
+ // for the rest of the session, and across a sign-out.
163
+ const usedEntity = entity;
165
164
 
166
165
  let body: React.ReactNode;
167
166
 
@@ -234,5 +233,3 @@ function ReferencePreviewExisting<M extends Record<string, unknown> = Record<str
234
233
  hover={hover}/>;
235
234
 
236
235
  }
237
-
238
- const referencesCache = new Map<string, Entity<any>>();
@@ -189,11 +189,14 @@ function RelationPreviewExisting<M extends Record<string, unknown> = Record<stri
189
189
  useCache: true
190
190
  });
191
191
 
192
- if (entity) {
193
- relationsCache.set(relation.pathWithId, entity);
194
- }
195
-
196
- const usedEntity = passedEntity ?? entity ?? relationsCache.get(relation.pathWithId);
192
+ // Only what the fetch currently reports. There used to be a module-level
193
+ // Map behind this, written on every successful fetch and never invalidated:
194
+ // when the target row was deleted `useFetch` correctly reported it gone and
195
+ // the card carried on rendering the copy in the Map, so the "missing"
196
+ // branches below were unreachable for anything previewed this session — and
197
+ // the Map outlived a sign-out. `useFetch` keeps its own cache, seeds the
198
+ // first render from it, and clears the entry when the row disappears.
199
+ const usedEntity = passedEntity ?? entity;
197
200
 
198
201
  let body: React.ReactNode;
199
202
 
@@ -266,5 +269,3 @@ function RelationPreviewExisting<M extends Record<string, unknown> = Record<stri
266
269
  hover={hover}/>;
267
270
 
268
271
  }
269
-
270
- const relationsCache = new Map<string, Entity<any>>();
@@ -0,0 +1,125 @@
1
+ import type { Locale as LocaleKey } from "@rebasepro/admin-types";
2
+ import type { Locale as DateFnsLocale } from "date-fns";
3
+
4
+ /**
5
+ * One date-fns locale, fetched when the admin is actually configured to use it.
6
+ *
7
+ * `DatePreview` used to open with:
8
+ *
9
+ * import * as locales from "date-fns/locale";
10
+ *
11
+ * and then index that namespace with the configured locale. A namespace import
12
+ * of a barrel is a reference to every member of it, so all ~77 locales were
13
+ * bundled — 640 kB, in the entry chunk's static graph, to format dates in the
14
+ * one locale a given deployment has configured (and `undefined`, meaning
15
+ * en-US, for nearly all of them).
16
+ *
17
+ * A map of thunks is written out rather than derived from the locale key,
18
+ * because `import(`date-fns/locale/${name}`)` is not statically analysable:
19
+ * the bundler cannot see which modules it must emit, and the import fails at
20
+ * runtime in a build. Each entry below is its own async chunk, and exactly one
21
+ * of them is ever fetched.
22
+ *
23
+ * `fil` is in the `Locale` union and has no date-fns locale to import; it
24
+ * resolved to `undefined` through the namespace too, so it still does.
25
+ */
26
+ const dateFnsLocaleLoaders: Partial<Record<LocaleKey, () => Promise<DateFnsLocale>>> = {
27
+ af: () => import("date-fns/locale/af").then(m => m.af),
28
+ ar: () => import("date-fns/locale/ar").then(m => m.ar),
29
+ arDZ: () => import("date-fns/locale/ar-DZ").then(m => m.arDZ),
30
+ arMA: () => import("date-fns/locale/ar-MA").then(m => m.arMA),
31
+ arSA: () => import("date-fns/locale/ar-SA").then(m => m.arSA),
32
+ az: () => import("date-fns/locale/az").then(m => m.az),
33
+ be: () => import("date-fns/locale/be").then(m => m.be),
34
+ bg: () => import("date-fns/locale/bg").then(m => m.bg),
35
+ bn: () => import("date-fns/locale/bn").then(m => m.bn),
36
+ ca: () => import("date-fns/locale/ca").then(m => m.ca),
37
+ cs: () => import("date-fns/locale/cs").then(m => m.cs),
38
+ cy: () => import("date-fns/locale/cy").then(m => m.cy),
39
+ da: () => import("date-fns/locale/da").then(m => m.da),
40
+ de: () => import("date-fns/locale/de").then(m => m.de),
41
+ el: () => import("date-fns/locale/el").then(m => m.el),
42
+ enAU: () => import("date-fns/locale/en-AU").then(m => m.enAU),
43
+ enCA: () => import("date-fns/locale/en-CA").then(m => m.enCA),
44
+ enGB: () => import("date-fns/locale/en-GB").then(m => m.enGB),
45
+ enIN: () => import("date-fns/locale/en-IN").then(m => m.enIN),
46
+ enNZ: () => import("date-fns/locale/en-NZ").then(m => m.enNZ),
47
+ enUS: () => import("date-fns/locale/en-US").then(m => m.enUS),
48
+ eo: () => import("date-fns/locale/eo").then(m => m.eo),
49
+ es: () => import("date-fns/locale/es").then(m => m.es),
50
+ et: () => import("date-fns/locale/et").then(m => m.et),
51
+ eu: () => import("date-fns/locale/eu").then(m => m.eu),
52
+ faIR: () => import("date-fns/locale/fa-IR").then(m => m.faIR),
53
+ fi: () => import("date-fns/locale/fi").then(m => m.fi),
54
+ fr: () => import("date-fns/locale/fr").then(m => m.fr),
55
+ frCA: () => import("date-fns/locale/fr-CA").then(m => m.frCA),
56
+ frCH: () => import("date-fns/locale/fr-CH").then(m => m.frCH),
57
+ gd: () => import("date-fns/locale/gd").then(m => m.gd),
58
+ gl: () => import("date-fns/locale/gl").then(m => m.gl),
59
+ gu: () => import("date-fns/locale/gu").then(m => m.gu),
60
+ he: () => import("date-fns/locale/he").then(m => m.he),
61
+ hi: () => import("date-fns/locale/hi").then(m => m.hi),
62
+ hr: () => import("date-fns/locale/hr").then(m => m.hr),
63
+ hu: () => import("date-fns/locale/hu").then(m => m.hu),
64
+ hy: () => import("date-fns/locale/hy").then(m => m.hy),
65
+ id: () => import("date-fns/locale/id").then(m => m.id),
66
+ is: () => import("date-fns/locale/is").then(m => m.is),
67
+ it: () => import("date-fns/locale/it").then(m => m.it),
68
+ ja: () => import("date-fns/locale/ja").then(m => m.ja),
69
+ ka: () => import("date-fns/locale/ka").then(m => m.ka),
70
+ kk: () => import("date-fns/locale/kk").then(m => m.kk),
71
+ kn: () => import("date-fns/locale/kn").then(m => m.kn),
72
+ ko: () => import("date-fns/locale/ko").then(m => m.ko),
73
+ lb: () => import("date-fns/locale/lb").then(m => m.lb),
74
+ lt: () => import("date-fns/locale/lt").then(m => m.lt),
75
+ lv: () => import("date-fns/locale/lv").then(m => m.lv),
76
+ mk: () => import("date-fns/locale/mk").then(m => m.mk),
77
+ ms: () => import("date-fns/locale/ms").then(m => m.ms),
78
+ mt: () => import("date-fns/locale/mt").then(m => m.mt),
79
+ nb: () => import("date-fns/locale/nb").then(m => m.nb),
80
+ nl: () => import("date-fns/locale/nl").then(m => m.nl),
81
+ nlBE: () => import("date-fns/locale/nl-BE").then(m => m.nlBE),
82
+ nn: () => import("date-fns/locale/nn").then(m => m.nn),
83
+ pl: () => import("date-fns/locale/pl").then(m => m.pl),
84
+ pt: () => import("date-fns/locale/pt").then(m => m.pt),
85
+ ptBR: () => import("date-fns/locale/pt-BR").then(m => m.ptBR),
86
+ ro: () => import("date-fns/locale/ro").then(m => m.ro),
87
+ ru: () => import("date-fns/locale/ru").then(m => m.ru),
88
+ sk: () => import("date-fns/locale/sk").then(m => m.sk),
89
+ sl: () => import("date-fns/locale/sl").then(m => m.sl),
90
+ sr: () => import("date-fns/locale/sr").then(m => m.sr),
91
+ srLatn: () => import("date-fns/locale/sr-Latn").then(m => m.srLatn),
92
+ sv: () => import("date-fns/locale/sv").then(m => m.sv),
93
+ ta: () => import("date-fns/locale/ta").then(m => m.ta),
94
+ te: () => import("date-fns/locale/te").then(m => m.te),
95
+ th: () => import("date-fns/locale/th").then(m => m.th),
96
+ tr: () => import("date-fns/locale/tr").then(m => m.tr),
97
+ ug: () => import("date-fns/locale/ug").then(m => m.ug),
98
+ uk: () => import("date-fns/locale/uk").then(m => m.uk),
99
+ uz: () => import("date-fns/locale/uz").then(m => m.uz),
100
+ vi: () => import("date-fns/locale/vi").then(m => m.vi),
101
+ zhCN: () => import("date-fns/locale/zh-CN").then(m => m.zhCN),
102
+ zhTW: () => import("date-fns/locale/zh-TW").then(m => m.zhTW),
103
+ };
104
+
105
+ const cache = new Map<LocaleKey, DateFnsLocale>();
106
+
107
+ /** The locale if it has already been fetched, otherwise `undefined`. Never fetches. */
108
+ export function getLoadedDateFnsLocale(locale: LocaleKey | undefined): DateFnsLocale | undefined {
109
+ return locale ? cache.get(locale) : undefined;
110
+ }
111
+
112
+ /**
113
+ * Fetch a date-fns locale. Resolves to `undefined` for a locale date-fns does
114
+ * not ship, which is what indexing the namespace used to yield.
115
+ */
116
+ export async function loadDateFnsLocale(locale: LocaleKey | undefined): Promise<DateFnsLocale | undefined> {
117
+ if (!locale) return undefined;
118
+ const cached = cache.get(locale);
119
+ if (cached) return cached;
120
+ const loader = dateFnsLocaleLoaders[locale];
121
+ if (!loader) return undefined;
122
+ const loaded = await loader();
123
+ cache.set(locale, loaded);
124
+ return loaded;
125
+ }
@@ -1,7 +1,7 @@
1
1
  import type { ArrayProperty, RelationProperty } from "@rebasepro/types";
2
2
  import { EntityRelation } from "@rebasepro/types";
3
3
  import type { PropertyPreviewProps } from "../../types/components/PropertyPreviewProps";
4
- import { normalizeToEntityRelation } from "@rebasepro/common";
4
+ import { normalizeToEntityRelation, getRelationTargetPath } from "@rebasepro/common";
5
5
  import { RelationPreview } from "../components/RelationPreview";
6
6
  import { useIsNestedEntityPreview } from "../../components/EntityPreviewNesting";
7
7
  import { InlineEntityListPreview } from "../components/InlineEntityListPreview";
@@ -27,11 +27,14 @@ export function ArrayOfRelationsPreview({
27
27
  throw Error("Picked wrong preview component ArrayOfRelationsPreview");
28
28
 
29
29
  const ofProperty = property.of as RelationProperty;
30
+ // Elements arrive hydrated or as bare foreign keys depending on the fetch
31
+ // path; the declared target resolves the latter rather than dropping them.
32
+ const relationTargetPath = getRelationTargetPath(ofProperty);
30
33
 
31
34
  // See ArrayOfReferencesPreview: nested lists stay on one wrapping line.
32
35
  if (nested || textOnly) {
33
36
  const relations = ((value ?? []) as unknown[])
34
- .map(relation => normalizeToEntityRelation(relation))
37
+ .map(relation => normalizeToEntityRelation(relation, "relation", relationTargetPath))
35
38
  .filter((relation): relation is EntityRelation => Boolean(relation));
36
39
  return <InlineEntityListPreview
37
40
  items={relations}
@@ -51,7 +54,7 @@ export function ArrayOfRelationsPreview({
51
54
  <div className="flex flex-col w-full gap-0.5">
52
55
  {value ?
53
56
  (value as unknown[]).map((relation: unknown, index: number) => {
54
- const entityRelation = normalizeToEntityRelation(relation);
57
+ const entityRelation = normalizeToEntityRelation(relation, "relation", relationTargetPath);
55
58
 
56
59
  if (!entityRelation) return null;
57
60
 
@@ -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
  }
@@ -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.