@rebasepro/admin 0.13.0 → 0.13.1-canary.g1822133

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 (210) hide show
  1. package/dist/{CollectionEditorDialog-CjXkWZ64.js → CollectionEditorDialog-CSheud_E.js} +19 -9
  2. package/dist/CollectionEditorDialog-CSheud_E.js.map +1 -0
  3. package/dist/{PropertyEditView-W8I3YV89.js → PropertyEditView-CWZlC1gq.js} +5 -5
  4. package/dist/PropertyEditView-CWZlC1gq.js.map +1 -0
  5. package/dist/{RouterCollectionsStudioView-BsLTNoqX.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/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 +21 -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/editor.js +3 -0
  41. package/dist/editor.js.map +1 -1
  42. package/dist/{export-J79rMoNj.js → export-L53WektO.js} +175 -39
  43. package/dist/export-L53WektO.js.map +1 -0
  44. package/dist/form/components/FieldBlock.d.ts +58 -4
  45. package/dist/form/components/FormRail.d.ts +13 -0
  46. package/dist/form/components/FormSections.d.ts +15 -1
  47. package/dist/form/components/useRailVisible.d.ts +5 -4
  48. package/dist/form/form_utils.d.ts +38 -1
  49. package/dist/{history-D7L_JlLO.js → history-D5SKygpJ.js} +4 -4
  50. package/dist/{history-D7L_JlLO.js.map → history-D5SKygpJ.js.map} +1 -1
  51. package/dist/hooks/useEntityDisplay.d.ts +102 -0
  52. package/dist/hooks/useEntityDisplayTitle.d.ts +18 -9
  53. package/dist/{import-BvYl--U4.js → import-C_4edkoD.js} +6 -5
  54. package/dist/import-C_4edkoD.js.map +1 -0
  55. package/dist/index.d.ts +2 -2
  56. package/dist/index.js +237 -123
  57. package/dist/index.js.map +1 -1
  58. package/dist/preview/compact_text.d.ts +23 -0
  59. package/dist/preview/components/BooleanPreview.d.ts +3 -1
  60. package/dist/preview/components/date_fns_locales.d.ts +9 -0
  61. package/dist/preview/property_previews/ArrayOfStringsPreview.d.ts +1 -1
  62. package/dist/preview/property_previews/ArrayOneOfPreview.d.ts +1 -1
  63. package/dist/preview/property_previews/ArrayPropertyPreview.d.ts +1 -1
  64. package/dist/preview/property_previews/MapPropertyPreview.d.ts +1 -1
  65. package/dist/preview/property_previews/StringPropertyPreview.d.ts +1 -1
  66. package/dist/routes/RebaseRoute.d.ts +1 -1
  67. package/dist/types/components/PropertyPreviewProps.d.ts +19 -0
  68. package/dist/util/number_format.d.ts +14 -0
  69. package/dist/util/previews.d.ts +9 -2
  70. package/dist/util/view_constants.d.ts +24 -0
  71. package/dist/util/view_mode.d.ts +18 -0
  72. package/dist/{util-GiwPgxnL.js → util-C-D5yD2n.js} +4161 -1809
  73. package/dist/util-C-D5yD2n.js.map +1 -0
  74. package/package.json +10 -9
  75. package/src/collection_editor/serializable_types.ts +1 -0
  76. package/src/collection_editor/ui/collection_editor/AICollectionGeneratorPopover.tsx +8 -3
  77. package/src/collection_editor/ui/collection_editor/CollectionDetailsForm.tsx +5 -2
  78. package/src/collection_editor/ui/collection_editor/CollectionJsonImportDialog.tsx +1 -0
  79. package/src/collection_editor/ui/collection_editor/CollectionRLSTab.tsx +9 -8
  80. package/src/collection_editor/ui/collection_editor/CollectionsStudioView.tsx +9 -11
  81. package/src/collection_editor/ui/collection_editor/GeneralSettingsForm.tsx +5 -2
  82. package/src/collection_editor/ui/collection_editor/properties/MapPropertyField.tsx +1 -1
  83. package/src/collection_editor/ui/collection_editor/properties/advanced/AdvancedPropertyValidation.tsx +5 -2
  84. package/src/collection_editor/ui/collection_editor/properties/conditions/ConditionsEditor.tsx +1 -0
  85. package/src/collection_editor/useJsonCollectionsConfigController.tsx +92 -84
  86. package/src/collection_editor/useLocalCollectionsConfigController.tsx +10 -3
  87. package/src/components/AdditionalFieldValue.tsx +68 -0
  88. package/src/components/ClearFilterSortButton.tsx +16 -7
  89. package/src/components/CollectionTableBinding/CollectionTableBinding.tsx +7 -8
  90. package/src/components/CollectionTableBinding/PropertyTableCell.tsx +2 -2
  91. package/src/components/CollectionTableBinding/column_utils.tsx +15 -6
  92. package/src/components/CollectionTableBinding/internal/popup_field/PopupFormField.tsx +2 -2
  93. package/src/components/CollectionTableBinding/table_bindings.tsx +7 -1
  94. package/src/components/CollectionViewBinding/BoardCardBinding.tsx +26 -27
  95. package/src/components/CollectionViewBinding/CollectionBoardViewBinding.tsx +13 -7
  96. package/src/components/CollectionViewBinding/CollectionCardViewBinding.tsx +2 -1
  97. package/src/components/CollectionViewBinding/CollectionListViewBinding.tsx +822 -354
  98. package/src/components/CollectionViewBinding/CollectionViewActions.tsx +1 -1
  99. package/src/components/CollectionViewBinding/CollectionViewBinding.tsx +52 -12
  100. package/src/components/CollectionViewBinding/CollectionViewStartActions.tsx +9 -0
  101. package/src/components/CollectionViewBinding/EntityCardBinding.tsx +51 -39
  102. package/src/components/CollectionViewBinding/SearchExplanation.tsx +106 -0
  103. package/src/components/CollectionViewBinding/SearchHighlight.tsx +311 -0
  104. package/src/components/CollectionViewBinding/SlotValue.tsx +156 -0
  105. package/src/components/CollectionViewBinding/SortButton.tsx +16 -4
  106. package/src/components/CollectionViewBinding/SplitListCloseButton.tsx +23 -7
  107. package/src/components/CollectionViewBinding/SplitListShowButton.tsx +28 -0
  108. package/src/components/CollectionViewBinding/SplitListView.tsx +47 -7
  109. package/src/components/CollectionViewBinding/ViewModeToggle.tsx +10 -5
  110. package/src/components/CollectionViewBinding/useBoardDataController.tsx +23 -6
  111. package/src/components/CollectionViewBinding/usePreviewSlots.ts +183 -64
  112. package/src/components/CollectionViewBinding/utils.ts +7 -4
  113. package/src/components/DefaultDrawer.tsx +26 -23
  114. package/src/components/DetailViewBinding.tsx +109 -62
  115. package/src/components/DrawerNavigationGroup.tsx +16 -39
  116. package/src/components/DrawerNavigationItem.tsx +56 -16
  117. package/src/components/EditViewBinding.tsx +233 -50
  118. package/src/components/EntityDisplayHeader.tsx +170 -0
  119. package/src/components/EntityIdentityBar.tsx +163 -23
  120. package/src/components/EntityInspector.tsx +24 -6
  121. package/src/components/EntityPreviewBinding.tsx +58 -18
  122. package/src/components/EntityViewBinding.tsx +344 -41
  123. package/src/components/InlineEntityPreview.tsx +10 -10
  124. package/src/components/RebaseLayout.tsx +2 -2
  125. package/src/components/RebaseShell.tsx +2 -2
  126. package/src/components/ReferenceTable/SelectionTableBinding.tsx +4 -2
  127. package/src/components/RelationSelector.tsx +76 -15
  128. package/src/components/SearchIconsView.tsx +14 -33
  129. package/src/components/SelectableTable/filters/StringNumberFilterField.tsx +1 -0
  130. package/src/components/SideDialogs.tsx +15 -3
  131. package/src/components/SidePanelBinding.tsx +122 -66
  132. package/src/components/UserSelector.tsx +67 -13
  133. package/src/components/app/Scaffold.tsx +112 -10
  134. package/src/components/history/LastEditedByIndicator.tsx +7 -12
  135. package/src/components/index.ts +3 -0
  136. package/src/data_export/export/ExportCollectionAction.tsx +101 -48
  137. package/src/data_export/export/export.ts +63 -16
  138. package/src/data_export/export/fetch_export_data.ts +90 -0
  139. package/src/data_export/export/index.ts +1 -0
  140. package/src/data_import/components/DataNewPropertiesMapping.tsx +20 -3
  141. package/src/data_import/components/ImportNewPropertyFieldPreview.tsx +1 -0
  142. package/src/data_import/components/ImportSaveInProgress.tsx +35 -36
  143. package/src/data_import/import/ImportCollectionAction.tsx +13 -2
  144. package/src/data_import/utils/csv.ts +153 -0
  145. package/src/data_import/utils/data.ts +16 -2
  146. package/src/data_import/utils/file_to_json.ts +77 -6
  147. package/src/data_import/utils/index.ts +2 -0
  148. package/src/data_import/utils/save_entities.ts +138 -0
  149. package/src/data_import/utils/transforms.ts +23 -0
  150. package/src/editor/components/image-bubble.tsx +2 -0
  151. package/src/editor/selectors/link-selector.tsx +1 -0
  152. package/src/form/EntityForm.tsx +74 -53
  153. package/src/form/EntityFormBinding.tsx +24 -11
  154. package/src/form/PropertyFieldBinding.tsx +2 -2
  155. package/src/form/components/FieldBlock.tsx +80 -10
  156. package/src/form/components/FormRail.tsx +19 -2
  157. package/src/form/components/FormSections.tsx +32 -5
  158. package/src/form/components/StorageUploadProgress.tsx +9 -3
  159. package/src/form/components/useRailVisible.tsx +5 -4
  160. package/src/form/field_bindings/ArrayCustomShapedFieldBinding.tsx +2 -2
  161. package/src/form/field_bindings/BinaryFieldBinding.tsx +11 -5
  162. package/src/form/field_bindings/BlockFieldBinding.tsx +3 -3
  163. package/src/form/field_bindings/DateTimeFieldBinding.tsx +4 -0
  164. package/src/form/field_bindings/GeopointFieldBinding.tsx +23 -16
  165. package/src/form/field_bindings/KeyValueFieldBinding.tsx +3 -0
  166. package/src/form/field_bindings/MapFieldBinding.tsx +2 -2
  167. package/src/form/field_bindings/MarkdownEditorFieldBinding.tsx +13 -7
  168. package/src/form/field_bindings/MultiSelectFieldBinding.tsx +7 -1
  169. package/src/form/field_bindings/ReadOnlyFieldBinding.tsx +21 -2
  170. package/src/form/field_bindings/ReferenceFieldBinding.tsx +1 -1
  171. package/src/form/field_bindings/RelationFieldBinding.tsx +5 -2
  172. package/src/form/field_bindings/RepeatFieldBinding.tsx +3 -3
  173. package/src/form/field_bindings/StorageUploadFieldBinding.tsx +2 -2
  174. package/src/form/field_bindings/TextFieldBinding.tsx +18 -0
  175. package/src/form/field_bindings/UserSelectFieldBinding.tsx +4 -1
  176. package/src/form/field_bindings/VectorFieldBinding.tsx +7 -4
  177. package/src/form/form_utils.ts +78 -0
  178. package/src/hooks/navigation/useBuildCollectionRegistryController.tsx +29 -5
  179. package/src/hooks/useEntityDisplay.tsx +262 -0
  180. package/src/hooks/useEntityDisplayTitle.tsx +27 -38
  181. package/src/index.ts +9 -1
  182. package/src/preview/PropertyPreview.tsx +25 -5
  183. package/src/preview/compact_text.ts +68 -0
  184. package/src/preview/components/BooleanPreview.tsx +5 -2
  185. package/src/preview/components/DatePreview.tsx +15 -2
  186. package/src/preview/components/ReferencePreview.tsx +5 -8
  187. package/src/preview/components/RelationPreview.tsx +8 -7
  188. package/src/preview/components/StorageThumbnail.tsx +5 -2
  189. package/src/preview/components/date_fns_locales.ts +125 -0
  190. package/src/preview/property_previews/ArrayOfRelationsPreview.tsx +6 -3
  191. package/src/preview/property_previews/ArrayOfStringsPreview.tsx +26 -1
  192. package/src/preview/property_previews/ArrayOneOfPreview.tsx +10 -1
  193. package/src/preview/property_previews/ArrayPropertyPreview.tsx +10 -1
  194. package/src/preview/property_previews/MapPropertyPreview.tsx +52 -1
  195. package/src/preview/property_previews/NumberPropertyPreview.tsx +9 -1
  196. package/src/preview/property_previews/StringPropertyPreview.tsx +8 -5
  197. package/src/routes/RebaseRoute.tsx +62 -4
  198. package/src/types/components/PropertyPreviewProps.tsx +21 -0
  199. package/src/util/navigation_utils.ts +4 -2
  200. package/src/util/number_format.ts +46 -0
  201. package/src/util/previews.ts +8 -55
  202. package/src/util/property_utils.tsx +6 -1
  203. package/src/util/view_constants.ts +27 -0
  204. package/src/util/view_mode.ts +36 -0
  205. package/dist/CollectionEditorDialog-CjXkWZ64.js.map +0 -1
  206. package/dist/PropertyEditView-W8I3YV89.js.map +0 -1
  207. package/dist/RouterCollectionsStudioView-BsLTNoqX.js.map +0 -1
  208. package/dist/export-J79rMoNj.js.map +0 -1
  209. package/dist/import-BvYl--U4.js.map +0 -1
  210. package/dist/util-GiwPgxnL.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";
@@ -27,6 +27,7 @@ import { ErrorView } from "@rebasepro/app";
27
27
  import { RelationPreview } from "./components/RelationPreview";
28
28
  import { InlineEntityListPreview } from "./components/InlineEntityListPreview";
29
29
  import { useIsNestedEntityPreview } from "../components/EntityPreviewNesting";
30
+ import { markdownToPlainText } from "./compact_text";
30
31
  import { UserPreview } from "./components/UserPreview";
31
32
 
32
33
  /**
@@ -50,6 +51,13 @@ export const PropertyPreview = React.memo(function PropertyPreview<P extends Pro
50
51
  fill
51
52
  } = props;
52
53
 
54
+ // A preview rendered inside an entity preview fills one line of a card and
55
+ // has no room to grow — see {@link PropertyPreviewProps.compact}. The card
56
+ // is the thing that knows this, and it says so by opening a nesting
57
+ // boundary, so the depth is the signal rather than a prop every caller
58
+ // between here and there would have to forward.
59
+ const compact = props.compact ?? (nested || Boolean(props.textOnly));
60
+
53
61
  const property = resolveProperty({
54
62
  propertyKey,
55
63
  property: inputProperty,
@@ -103,7 +111,9 @@ export const PropertyPreview = React.memo(function PropertyPreview<P extends Pro
103
111
  fill={fill}
104
112
  previewType={stringProperty.admin?.urlPreview}/>;
105
113
  } else if (stringProperty.admin?.markdown) {
106
- content = <Markdown source={value} size={"small"}/>;
114
+ content = compact
115
+ ? <span className={"text-sm"}>{markdownToPlainText(value)}</span>
116
+ : <Markdown source={value} size={"small"}/>;
107
117
  } else if (stringProperty.userSelect) {
108
118
  content = <UserPreview
109
119
  value={value}
@@ -113,6 +123,7 @@ export const PropertyPreview = React.memo(function PropertyPreview<P extends Pro
113
123
  />;
114
124
  } else {
115
125
  content = <StringPropertyPreview {...props}
126
+ compact={compact}
116
127
  property={stringProperty}
117
128
  value={value}/>;
118
129
  }
@@ -129,6 +140,7 @@ export const PropertyPreview = React.memo(function PropertyPreview<P extends Pro
129
140
  if (arrayProperty.of) {
130
141
  if (Array.isArray(arrayProperty.of)) {
131
142
  content = <ArrayPropertyPreview {...props}
143
+ compact={compact}
132
144
  value={value}
133
145
  property={arrayProperty}/>;
134
146
  } else if (arrayProperty.of.type === "reference") {
@@ -153,6 +165,7 @@ export const PropertyPreview = React.memo(function PropertyPreview<P extends Pro
153
165
  } else {
154
166
  content = <ArrayOfStringsPreview
155
167
  {...props}
168
+ compact={compact}
156
169
  property={property as ArrayProperty}
157
170
  value={value as string[]}/>;
158
171
  }
@@ -163,11 +176,13 @@ export const PropertyPreview = React.memo(function PropertyPreview<P extends Pro
163
176
  property={property as ArrayProperty}/>;
164
177
  } else {
165
178
  content = <ArrayPropertyPreview {...props}
179
+ compact={compact}
166
180
  value={value}
167
181
  property={property as ArrayProperty}/>;
168
182
  }
169
183
  } else if (arrayProperty.oneOf) {
170
184
  content = <ArrayOneOfPreview {...props}
185
+ compact={compact}
171
186
  value={value}
172
187
  property={property as ArrayProperty}/>;
173
188
  }
@@ -178,6 +193,7 @@ export const PropertyPreview = React.memo(function PropertyPreview<P extends Pro
178
193
  if (typeof value === "object") {
179
194
  content =
180
195
  <MapPropertyPreview {...props}
196
+ compact={compact}
181
197
  value={value as Record<string, any>}
182
198
  property={property as MapProperty}/>;
183
199
  } else {
@@ -218,12 +234,16 @@ export const PropertyPreview = React.memo(function PropertyPreview<P extends Pro
218
234
  }
219
235
 
220
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);
221
241
  if (!value) {
222
242
  content = <EmptyValue/>;
223
243
  } else if (Array.isArray(value)) {
224
244
  // Many-cardinality relation: value is an array of EntityRelation (or plain objects)
225
245
  const relations = (value as unknown[])
226
- .map(item => normalizeToEntityRelation(item, "relation"))
246
+ .map(item => normalizeToEntityRelation(item, "relation", relationTargetPath))
227
247
  .filter(Boolean) as EntityRelation[];
228
248
  const renderRelation = (entityRelation: EntityRelation, index: number) => <RelationPreview
229
249
  key={`preview_rel_${propertyKey}_${index}`}
@@ -252,7 +272,7 @@ export const PropertyPreview = React.memo(function PropertyPreview<P extends Pro
252
272
  );
253
273
  } else {
254
274
  // Single-cardinality relation
255
- const relationValue = normalizeToEntityRelation(value, "relation");
275
+ const relationValue = normalizeToEntityRelation(value, "relation", relationTargetPath);
256
276
  if (relationValue) {
257
277
  content = <RelationPreview
258
278
  disabled={!property.relation}
@@ -270,7 +290,7 @@ export const PropertyPreview = React.memo(function PropertyPreview<P extends Pro
270
290
 
271
291
  } else if (property.type === "boolean") {
272
292
  if (typeof value === "boolean") {
273
- content = <BooleanPreview value={value} size={size} property={property}/>;
293
+ content = <BooleanPreview value={value} size={size} property={property} hideLabel={props.hideLabel}/>;
274
294
  } else {
275
295
  content = buildWrongValueType(propertyKey, property.type, value);
276
296
  }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Turning block content into the one line a compact preview has room for.
3
+ *
4
+ * A preview slot inside a card is a single line that cannot grow. Rendering
5
+ * Markdown there does not produce a small version of the document — it
6
+ * produces the whole document, headings and lists included, inside a 44px box.
7
+ * These helpers take the opening line instead, which is what a person reads
8
+ * off a card anyway.
9
+ */
10
+
11
+ /** Longer than any card line; the cap only keeps the DOM node small. */
12
+ const DEFAULT_MAX_LENGTH = 280;
13
+
14
+ const FENCED_CODE = /```[\s\S]*?```|~~~[\s\S]*?~~~/g;
15
+ const HTML_TAG = /<[^>]*>/g;
16
+ const IMAGE = /!\[([^\]]*)\]\([^)]*\)/g;
17
+ const LINK = /\[([^\]]*)\]\([^)]*\)/g;
18
+ const REFERENCE_LINK = /\[([^\]]*)\]\[[^\]]*\]/g;
19
+ const HEADING = /^\s{0,3}#{1,6}\s+/gm;
20
+ const BLOCKQUOTE = /^\s{0,3}>\s?/gm;
21
+ const LIST_MARKER = /^\s*(?:[-*+]|\d+[.)])\s+/gm;
22
+ const THEMATIC_BREAK = /^\s{0,3}(?:[-*_]\s*){3,}$/gm;
23
+ const TABLE_PIPE = /[|]/g;
24
+ const EMPHASIS = /(\*\*|__|\*|_|~~|`)/g;
25
+
26
+ /**
27
+ * The readable text of a Markdown document, as one line.
28
+ *
29
+ * Deliberately a lexical strip rather than a parse: this runs once per preview
30
+ * cell in a table that can hold hundreds, and the result is truncated to a line
31
+ * regardless, so the cost of being exactly right is not worth paying. Anything
32
+ * it fails to strip degrades to a stray `*`, never to a broken layout.
33
+ */
34
+ export function markdownToPlainText(source: string, maxLength: number = DEFAULT_MAX_LENGTH): string {
35
+ if (!source) return "";
36
+
37
+ const text = source
38
+ .replace(FENCED_CODE, " ")
39
+ .replace(IMAGE, "$1")
40
+ .replace(LINK, "$1")
41
+ .replace(REFERENCE_LINK, "$1")
42
+ .replace(THEMATIC_BREAK, " ")
43
+ .replace(HEADING, "")
44
+ .replace(BLOCKQUOTE, "")
45
+ .replace(LIST_MARKER, "")
46
+ .replace(HTML_TAG, "")
47
+ .replace(TABLE_PIPE, " ")
48
+ .replace(EMPHASIS, "");
49
+
50
+ return collapseToSingleLine(text, maxLength);
51
+ }
52
+
53
+ /**
54
+ * Whitespace — including the newlines that would otherwise let a `truncate`
55
+ * container grow — collapsed to single spaces, capped to `maxLength`.
56
+ */
57
+ export function collapseToSingleLine(source: string, maxLength: number = DEFAULT_MAX_LENGTH): string {
58
+ if (!source) return "";
59
+ const collapsed = source.replace(/\s+/g, " ").trim();
60
+ if (collapsed.length <= maxLength) return collapsed;
61
+ // Cut at a word boundary when there is one nearby, so the line does not end
62
+ // mid-word for the sake of ten characters.
63
+ const cut = collapsed.slice(0, maxLength);
64
+ // The cut already landed between words: keep the last one whole.
65
+ if (collapsed[maxLength] === " ") return cut.trimEnd() + "…";
66
+ const lastSpace = cut.lastIndexOf(" ");
67
+ return (lastSpace > maxLength * 0.6 ? cut.slice(0, lastSpace) : cut).trimEnd() + "…";
68
+ }
@@ -8,18 +8,21 @@ import { PreviewSize } from "../../types/components/PropertyPreviewProps";
8
8
  export function BooleanPreview({
9
9
  value,
10
10
  size,
11
- property
11
+ property,
12
+ hideLabel
12
13
  }: {
13
14
  value: boolean,
14
15
  size: PreviewSize,
15
16
  property: Property,
17
+ /** The caller already shows the name — see `PropertyPreviewProps.hideLabel`. */
18
+ hideLabel?: boolean,
16
19
  }): React.ReactElement {
17
20
  return <div className={"flex flex-row gap-2 items-center"}>
18
21
  <Checkbox checked={value}
19
22
  padding={false}
20
23
  size={size}
21
24
  color={"secondary"}/>
22
- {property.name && <span
25
+ {!hideLabel && property.name && <span
23
26
  className={cls("text-text-secondary dark:text-text-secondary-dark", size === "small" ? "text-sm" : "")}>{property.name}</span>}
24
27
  </div>;
25
28
  }
@@ -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>>();
@@ -2,7 +2,7 @@ import React, { useEffect } from "react";
2
2
 
3
3
  import { renderSkeletonImageThumbnail } from "../property_previews/SkeletonPropertyComponent";
4
4
  import { UrlComponentPreview } from "./UrlComponentPreview";
5
- import { ErrorView, useStorageSource, useStorageSources } from "@rebasepro/app";
5
+ import { ErrorView, useStorageSource, useStorageSources, useTranslation } from "@rebasepro/app";
6
6
  import { resolveStorageSource } from "@rebasepro/common";
7
7
  import { DownloadConfig, FileType } from "@rebasepro/types";
8
8
  import type { PreviewSize } from "../../types/components/PropertyPreviewProps";
@@ -42,6 +42,7 @@ export function StorageThumbnailInternal({
42
42
  storageSourceKey
43
43
  }: StorageThumbnailProps) {
44
44
 
45
+ const { t } = useTranslation();
45
46
  const [error, setError] = React.useState<Error | undefined>(undefined);
46
47
  const defaultStorage = useStorageSource();
47
48
  const storageSources = useStorageSources();
@@ -85,7 +86,9 @@ export function StorageThumbnailInternal({
85
86
  : (filetype?.startsWith("audio") ? "audio" : "file"));
86
87
 
87
88
  if (downloadConfig?.fileNotFound)
88
- return <ErrorView error={"File not found"}></ErrorView>
89
+ // `file_not_found` is translated into seven locales and this rendered
90
+ // the English literal, so a German panel said "File not found".
91
+ return <ErrorView error={t("file_not_found")}></ErrorView>
89
92
 
90
93
  return downloadConfig?.url
91
94
  ? <UrlComponentPreview previewType={previewType}
@@ -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
 
@@ -12,7 +12,8 @@ export function ArrayOfStringsPreview({
12
12
  value,
13
13
  property: property,
14
14
  // entity,
15
- size
15
+ size,
16
+ compact
16
17
  }: PropertyPreviewProps<ArrayProperty>) {
17
18
 
18
19
  if (Array.isArray(property.of)) {
@@ -27,6 +28,30 @@ export function ArrayOfStringsPreview({
27
28
  const stringProperty = property.of as StringProperty;
28
29
  const arrayValues = value as string[];
29
30
 
31
+ // One line, so the items run along it separated by a dot rather than
32
+ // stacking. Each still renders through the string preview: a value that is
33
+ // a tag stays a tag, it just sits beside its neighbours.
34
+ if (compact) {
35
+ return (
36
+ <span className="inline-flex items-center gap-1 min-w-0 truncate">
37
+ {arrayValues &&
38
+ arrayValues.map((v, index: number) =>
39
+ <React.Fragment key={`preview_array_strings_${propertyKey}_${index}`}>
40
+ {index > 0 && <span className="opacity-40"
41
+ aria-hidden={true}>·</span>}
42
+ <ErrorBoundary>
43
+ <StringPropertyPreview propertyKey={propertyKey}
44
+ property={stringProperty}
45
+ value={v}
46
+ compact={true}
47
+ size={size}/>
48
+ </ErrorBoundary>
49
+ </React.Fragment>
50
+ )}
51
+ </span>
52
+ );
53
+ }
54
+
30
55
  return (
31
56
  <div className="flex flex-col gap-2">
32
57
  {arrayValues &&
@@ -14,7 +14,8 @@ export function ArrayOneOfPreview({
14
14
  propertyKey,
15
15
  value,
16
16
  property: property,
17
- size
17
+ size,
18
+ compact
18
19
  // entity
19
20
  }: PropertyPreviewProps<ArrayProperty>) {
20
21
 
@@ -37,6 +38,14 @@ export function ArrayOneOfPreview({
37
38
 
38
39
  if (!values) return null;
39
40
 
41
+ // As in {@link ArrayPropertyPreview}: blocks stacked vertically, so a
42
+ // single line can only report the count.
43
+ if (compact) {
44
+ return <span className={"text-sm truncate"}>
45
+ {values.length === 1 ? "1 block" : `${values.length} blocks`}
46
+ </span>;
47
+ }
48
+
40
49
  const childSize: PreviewSize = size === "medium" ? "medium" : "small";
41
50
 
42
51
  const typeField = property.oneOf.typeField ?? DEFAULT_ONE_OF_TYPE;
@@ -15,7 +15,8 @@ export function ArrayPropertyPreview({
15
15
  propertyKey,
16
16
  value,
17
17
  property: property,
18
- size
18
+ size,
19
+ compact
19
20
  }: PropertyPreviewProps<ArrayProperty>) {
20
21
 
21
22
  if (property.type !== "array")
@@ -38,6 +39,14 @@ export function ArrayPropertyPreview({
38
39
 
39
40
  if (!values) return null;
40
41
 
42
+ // Each entry is a block of its own — a bordered row, sometimes a whole
43
+ // sub-table. Stating how many there are is the only honest one-liner.
44
+ if (compact) {
45
+ return <span className={"text-sm truncate"}>
46
+ {values.length === 1 ? "1 item" : `${values.length} items`}
47
+ </span>;
48
+ }
49
+
41
50
  const childSize: PreviewSize = size === "medium" ? "medium" : "small";
42
51
 
43
52
  return (
@@ -14,7 +14,8 @@ export function MapPropertyPreview({
14
14
  propertyKey,
15
15
  value,
16
16
  property,
17
- size
17
+ size,
18
+ compact
18
19
  }: PropertyPreviewProps<MapProperty>) {
19
20
 
20
21
  if (property.type !== "map") {
@@ -23,6 +24,13 @@ export function MapPropertyPreview({
23
24
 
24
25
  const mapProperty = property as MapProperty;
25
26
 
27
+ // A map is a table — a row per sub-property — and a card line has room for
28
+ // neither the rows nor the borders between them.
29
+ if (compact) {
30
+ return <CompactMapPreview property={mapProperty}
31
+ value={value as Record<string, unknown> | undefined}/>;
32
+ }
33
+
26
34
  if (!mapProperty.properties || Object.keys(mapProperty.properties ?? {}).length === 0) {
27
35
  return (
28
36
  <KeyValuePreview value={value}/>
@@ -105,6 +113,49 @@ export function MapPropertyPreview({
105
113
 
106
114
  }
107
115
 
116
+ /** How many leaf values a one-line map summary shows before it gives up. */
117
+ const COMPACT_MAP_ENTRIES = 3;
118
+
119
+ /**
120
+ * A map in one line: its first few leaf values, labelled, joined by dots.
121
+ *
122
+ * Nested maps and arrays are skipped rather than flattened — a line that says
123
+ * `[object Object]` is worse than a line that says nothing about that field —
124
+ * and a map with no leaves at all falls back to stating its size, which is at
125
+ * least true.
126
+ */
127
+ function CompactMapPreview({
128
+ property,
129
+ value
130
+ }: { property: MapProperty, value: Record<string, unknown> | undefined }) {
131
+
132
+ if (!value || typeof value !== "object") return <EmptyValue/>;
133
+
134
+ const declared = property.properties;
135
+ const keys = declared ? Object.keys(declared) : Object.keys(value);
136
+
137
+ const parts: string[] = [];
138
+ for (const key of keys) {
139
+ if (parts.length >= COMPACT_MAP_ENTRIES) break;
140
+ const childValue = value[key];
141
+ if (childValue === undefined || childValue === null || childValue === "") continue;
142
+ if (typeof childValue === "object") continue;
143
+ const label = declared?.[key]?.name ?? key;
144
+ parts.push(`${label}: ${String(childValue)}`);
145
+ }
146
+
147
+ if (parts.length === 0) {
148
+ const count = Object.keys(value).length;
149
+ return <Typography variant={"caption"}
150
+ color={"secondary"}
151
+ className={"truncate"}>
152
+ {count === 1 ? "1 field" : `${count} fields`}
153
+ </Typography>;
154
+ }
155
+
156
+ return <span className={"truncate text-sm"}>{parts.join(" · ")}</span>;
157
+ }
158
+
108
159
  export function KeyValuePreview({ value }: { value: any }) {
109
160
  if (typeof value !== "object") return null;
110
161
  if (!value) return <EmptyValue/>;