@rebasepro/admin 0.13.0 → 0.13.1-canary.g249daa1

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 (154) hide show
  1. package/dist/{CollectionEditorDialog-CjXkWZ64.js → CollectionEditorDialog-DgfCYDmF.js} +3 -3
  2. package/dist/{CollectionEditorDialog-CjXkWZ64.js.map → CollectionEditorDialog-DgfCYDmF.js.map} +1 -1
  3. package/dist/{PropertyEditView-W8I3YV89.js → PropertyEditView-CCrruunW.js} +2 -2
  4. package/dist/{PropertyEditView-W8I3YV89.js.map → PropertyEditView-CCrruunW.js.map} +1 -1
  5. package/dist/{RouterCollectionsStudioView-BsLTNoqX.js → RouterCollectionsStudioView-C_EwDSi1.js} +9 -13
  6. package/dist/RouterCollectionsStudioView-C_EwDSi1.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/CollectionViewBinding/CollectionViewBinding.d.ts +16 -0
  11. package/dist/components/CollectionViewBinding/EntityCardBinding.d.ts +3 -1
  12. package/dist/components/CollectionViewBinding/SearchExplanation.d.ts +48 -0
  13. package/dist/components/CollectionViewBinding/SearchHighlight.d.ts +92 -0
  14. package/dist/components/CollectionViewBinding/SlotValue.d.ts +63 -0
  15. package/dist/components/CollectionViewBinding/SplitListCloseButton.d.ts +17 -4
  16. package/dist/components/CollectionViewBinding/SplitListShowButton.d.ts +12 -0
  17. package/dist/components/CollectionViewBinding/usePreviewSlots.d.ts +55 -5
  18. package/dist/components/DetailViewBinding.d.ts +15 -0
  19. package/dist/components/DrawerNavigationGroup.d.ts +9 -6
  20. package/dist/components/DrawerNavigationItem.d.ts +16 -6
  21. package/dist/components/EditViewBinding.d.ts +30 -1
  22. package/dist/components/EntityDisplayHeader.d.ts +53 -0
  23. package/dist/components/EntityIdentityBar.d.ts +43 -1
  24. package/dist/components/EntityViewBinding.d.ts +60 -6
  25. package/dist/components/RebaseLayout.d.ts +1 -1
  26. package/dist/components/RebaseShell.d.ts +1 -1
  27. package/dist/components/RelationSelector.d.ts +8 -1
  28. package/dist/components/UserSelector.d.ts +8 -1
  29. package/dist/components/app/Scaffold.d.ts +9 -3
  30. package/dist/components/index.d.ts +2 -0
  31. package/dist/{export-J79rMoNj.js → export-CV-qQfxK.js} +2 -2
  32. package/dist/{export-J79rMoNj.js.map → export-CV-qQfxK.js.map} +1 -1
  33. package/dist/form/components/FieldBlock.d.ts +58 -4
  34. package/dist/form/components/FormRail.d.ts +13 -0
  35. package/dist/form/components/FormSections.d.ts +15 -1
  36. package/dist/form/components/useRailVisible.d.ts +5 -4
  37. package/dist/form/form_utils.d.ts +38 -1
  38. package/dist/{history-D7L_JlLO.js → history-By7QT3V0.js} +4 -4
  39. package/dist/{history-D7L_JlLO.js.map → history-By7QT3V0.js.map} +1 -1
  40. package/dist/hooks/useEntityDisplay.d.ts +102 -0
  41. package/dist/hooks/useEntityDisplayTitle.d.ts +18 -9
  42. package/dist/{import-BvYl--U4.js → import-CYbvYa_x.js} +3 -3
  43. package/dist/{import-BvYl--U4.js.map → import-CYbvYa_x.js.map} +1 -1
  44. package/dist/index.d.ts +2 -2
  45. package/dist/index.js +152 -57
  46. package/dist/index.js.map +1 -1
  47. package/dist/preview/compact_text.d.ts +23 -0
  48. package/dist/preview/components/BooleanPreview.d.ts +3 -1
  49. package/dist/preview/property_previews/ArrayOfStringsPreview.d.ts +1 -1
  50. package/dist/preview/property_previews/ArrayOneOfPreview.d.ts +1 -1
  51. package/dist/preview/property_previews/ArrayPropertyPreview.d.ts +1 -1
  52. package/dist/preview/property_previews/MapPropertyPreview.d.ts +1 -1
  53. package/dist/preview/property_previews/StringPropertyPreview.d.ts +1 -1
  54. package/dist/routes/RebaseRoute.d.ts +1 -1
  55. package/dist/types/components/PropertyPreviewProps.d.ts +19 -0
  56. package/dist/util/number_format.d.ts +14 -0
  57. package/dist/util/previews.d.ts +9 -2
  58. package/dist/util/view_constants.d.ts +24 -0
  59. package/dist/util/view_mode.d.ts +18 -0
  60. package/dist/{util-GiwPgxnL.js → util-B8hurkxt.js} +3569 -1692
  61. package/dist/util-B8hurkxt.js.map +1 -0
  62. package/package.json +9 -9
  63. package/src/collection_editor/serializable_types.ts +1 -0
  64. package/src/collection_editor/ui/collection_editor/CollectionsStudioView.tsx +9 -11
  65. package/src/components/AdditionalFieldValue.tsx +68 -0
  66. package/src/components/CollectionTableBinding/CollectionTableBinding.tsx +7 -8
  67. package/src/components/CollectionTableBinding/PropertyTableCell.tsx +2 -2
  68. package/src/components/CollectionTableBinding/column_utils.tsx +15 -6
  69. package/src/components/CollectionTableBinding/internal/popup_field/PopupFormField.tsx +2 -2
  70. package/src/components/CollectionViewBinding/BoardCardBinding.tsx +26 -27
  71. package/src/components/CollectionViewBinding/CollectionCardViewBinding.tsx +2 -1
  72. package/src/components/CollectionViewBinding/CollectionListViewBinding.tsx +822 -354
  73. package/src/components/CollectionViewBinding/CollectionViewActions.tsx +1 -1
  74. package/src/components/CollectionViewBinding/CollectionViewBinding.tsx +51 -12
  75. package/src/components/CollectionViewBinding/EntityCardBinding.tsx +51 -39
  76. package/src/components/CollectionViewBinding/SearchExplanation.tsx +106 -0
  77. package/src/components/CollectionViewBinding/SearchHighlight.tsx +285 -0
  78. package/src/components/CollectionViewBinding/SlotValue.tsx +156 -0
  79. package/src/components/CollectionViewBinding/SplitListCloseButton.tsx +23 -7
  80. package/src/components/CollectionViewBinding/SplitListShowButton.tsx +28 -0
  81. package/src/components/CollectionViewBinding/SplitListView.tsx +47 -7
  82. package/src/components/CollectionViewBinding/useBoardDataController.tsx +12 -3
  83. package/src/components/CollectionViewBinding/usePreviewSlots.ts +183 -64
  84. package/src/components/CollectionViewBinding/utils.ts +7 -4
  85. package/src/components/DefaultDrawer.tsx +19 -23
  86. package/src/components/DetailViewBinding.tsx +109 -62
  87. package/src/components/DrawerNavigationGroup.tsx +16 -39
  88. package/src/components/DrawerNavigationItem.tsx +44 -16
  89. package/src/components/EditViewBinding.tsx +233 -50
  90. package/src/components/EntityDisplayHeader.tsx +170 -0
  91. package/src/components/EntityIdentityBar.tsx +163 -23
  92. package/src/components/EntityInspector.tsx +24 -6
  93. package/src/components/EntityPreviewBinding.tsx +58 -18
  94. package/src/components/EntityViewBinding.tsx +344 -41
  95. package/src/components/InlineEntityPreview.tsx +10 -10
  96. package/src/components/RebaseLayout.tsx +2 -2
  97. package/src/components/RebaseShell.tsx +2 -2
  98. package/src/components/ReferenceTable/SelectionTableBinding.tsx +4 -2
  99. package/src/components/RelationSelector.tsx +23 -9
  100. package/src/components/SideDialogs.tsx +15 -3
  101. package/src/components/SidePanelBinding.tsx +122 -66
  102. package/src/components/UserSelector.tsx +20 -8
  103. package/src/components/app/Scaffold.tsx +112 -10
  104. package/src/components/history/LastEditedByIndicator.tsx +7 -12
  105. package/src/components/index.ts +3 -0
  106. package/src/data_import/utils/data.ts +9 -1
  107. package/src/form/EntityForm.tsx +74 -53
  108. package/src/form/EntityFormBinding.tsx +24 -11
  109. package/src/form/PropertyFieldBinding.tsx +2 -2
  110. package/src/form/components/FieldBlock.tsx +80 -10
  111. package/src/form/components/FormRail.tsx +19 -2
  112. package/src/form/components/FormSections.tsx +32 -5
  113. package/src/form/components/StorageUploadProgress.tsx +9 -3
  114. package/src/form/components/useRailVisible.tsx +5 -4
  115. package/src/form/field_bindings/ArrayCustomShapedFieldBinding.tsx +2 -2
  116. package/src/form/field_bindings/BinaryFieldBinding.tsx +10 -5
  117. package/src/form/field_bindings/BlockFieldBinding.tsx +3 -3
  118. package/src/form/field_bindings/GeopointFieldBinding.tsx +23 -16
  119. package/src/form/field_bindings/MapFieldBinding.tsx +2 -2
  120. package/src/form/field_bindings/MarkdownEditorFieldBinding.tsx +13 -7
  121. package/src/form/field_bindings/MultiSelectFieldBinding.tsx +7 -1
  122. package/src/form/field_bindings/ReadOnlyFieldBinding.tsx +21 -2
  123. package/src/form/field_bindings/ReferenceFieldBinding.tsx +1 -1
  124. package/src/form/field_bindings/RelationFieldBinding.tsx +5 -2
  125. package/src/form/field_bindings/RepeatFieldBinding.tsx +3 -3
  126. package/src/form/field_bindings/StorageUploadFieldBinding.tsx +2 -2
  127. package/src/form/field_bindings/UserSelectFieldBinding.tsx +4 -1
  128. package/src/form/field_bindings/VectorFieldBinding.tsx +6 -4
  129. package/src/form/form_utils.ts +78 -0
  130. package/src/hooks/navigation/useBuildCollectionRegistryController.tsx +29 -5
  131. package/src/hooks/useEntityDisplay.tsx +262 -0
  132. package/src/hooks/useEntityDisplayTitle.tsx +27 -38
  133. package/src/index.ts +9 -1
  134. package/src/preview/PropertyPreview.tsx +25 -5
  135. package/src/preview/compact_text.ts +68 -0
  136. package/src/preview/components/BooleanPreview.tsx +5 -2
  137. package/src/preview/components/StorageThumbnail.tsx +5 -2
  138. package/src/preview/property_previews/ArrayOfRelationsPreview.tsx +6 -3
  139. package/src/preview/property_previews/ArrayOfStringsPreview.tsx +26 -1
  140. package/src/preview/property_previews/ArrayOneOfPreview.tsx +10 -1
  141. package/src/preview/property_previews/ArrayPropertyPreview.tsx +10 -1
  142. package/src/preview/property_previews/MapPropertyPreview.tsx +52 -1
  143. package/src/preview/property_previews/NumberPropertyPreview.tsx +9 -1
  144. package/src/preview/property_previews/StringPropertyPreview.tsx +8 -5
  145. package/src/routes/RebaseRoute.tsx +62 -4
  146. package/src/types/components/PropertyPreviewProps.tsx +21 -0
  147. package/src/util/navigation_utils.ts +4 -2
  148. package/src/util/number_format.ts +46 -0
  149. package/src/util/previews.ts +8 -55
  150. package/src/util/property_utils.tsx +6 -1
  151. package/src/util/view_constants.ts +27 -0
  152. package/src/util/view_mode.ts +36 -0
  153. package/dist/RouterCollectionsStudioView-BsLTNoqX.js.map +0 -1
  154. package/dist/util-GiwPgxnL.js.map +0 -1
@@ -128,27 +128,24 @@ export function GeopointFieldBinding({
128
128
  : undefined;
129
129
 
130
130
  return (
131
- <div className="flex flex-col gap-2 w-full mt-2">
132
- <div className="flex items-center justify-between mb-1">
133
- {!hideLabel && <LabelWithIcon
131
+ <div className="flex flex-col gap-2 w-full">
132
+ {/* Label only, and only when there is one: a row that survives
133
+ `hideLabel` still spends its `mb-1` and the column's `gap-2`, so
134
+ the pair of inputs sat below the control next to them even
135
+ though the two labels were level. */}
136
+ {!hideLabel && <div className="flex items-center justify-between mb-1">
137
+ <LabelWithIcon
134
138
  icon={getIconForProperty(property, "small")}
135
139
  required={property.validation?.required}
136
140
  title={property.name ?? propertyKey}
137
- />}
138
- {!disabled && (latText || lngText) && (
139
- <IconButton
140
- size="small"
141
- onClick={handleClear}
142
- className="text-text-secondary hover:text-red-500"
143
- aria-label="Clear location"
144
- >
145
- <Trash2Icon size={14}/>
146
- </IconButton>
147
- )}
148
- </div>
141
+ />
142
+ </div>}
149
143
 
150
144
  <PropertyIdCopyTooltip propertyKey={propertyKey}>
151
- <div className="flex gap-4 w-full">
145
+ {/* Clear sits beside the inputs rather than above them, so
146
+ entering a location does not grow the field by a row and
147
+ knock it out of line with its neighbour. */}
148
+ <div className="flex gap-4 w-full items-center">
152
149
  <TextField
153
150
  className="flex-1"
154
151
  size={size}
@@ -178,6 +175,16 @@ export function GeopointFieldBinding({
178
175
  placeholder="e.g. 2.1686"
179
176
  inputClassName="font-mono"
180
177
  />
178
+ {!disabled && (latText || lngText) && (
179
+ <IconButton
180
+ size="small"
181
+ onClick={handleClear}
182
+ className="shrink-0 text-text-secondary hover:text-red-500"
183
+ aria-label="Clear location"
184
+ >
185
+ <Trash2Icon size={14}/>
186
+ </IconButton>
187
+ )}
181
188
  </div>
182
189
  </PropertyIdCopyTooltip>
183
190
 
@@ -6,7 +6,7 @@ import { useTranslation } from "@rebasepro/app";
6
6
 
7
7
  import { ErrorBoundary } from "@rebasepro/ui";
8
8
  import { getIconForProperty } from "../../util/property_utils";
9
- import { isHidden, isReadOnly } from "@rebasepro/app";
9
+ import { isDisabled, isHidden, isReadOnly } from "@rebasepro/app";
10
10
  import { FieldHelperText } from "../components/FieldHelperText";
11
11
  import { LabelWithIconAndTooltip } from "../components/LabelWithIconAndTooltip";
12
12
  import { PropertyFieldBinding } from "../PropertyFieldBinding";
@@ -50,7 +50,7 @@ export function MapFieldBinding({
50
50
  {Object.entries(mapProperties)
51
51
  .filter(([_, property]) => !isHidden(property))
52
52
  .map(([entryKey, childProperty], index) => {
53
- const thisDisabled = isReadOnly(childProperty) || Boolean(childProperty.admin?.disabled);
53
+ const thisDisabled = isReadOnly(childProperty) || isDisabled(childProperty);
54
54
  const fieldBindingProps: PropertyFieldBindingProps<Record<string, unknown>> = {
55
55
  propertyKey: `${propertyKey}.${entryKey}`,
56
56
  disabled: disabled || thisDisabled,
@@ -216,17 +216,26 @@ export function MarkdownEditorFieldBinding({
216
216
 
217
217
  return (
218
218
  <>
219
- <div className="flex items-center w-full">
220
- {!hideLabel && <LabelWithIconAndTooltip
219
+ {!hideLabel && <div className="flex items-center w-full">
220
+ <LabelWithIconAndTooltip
221
221
  propertyKey={propertyKey}
222
222
  icon={getIconForProperty(property, "small")}
223
223
  required={property.validation?.required}
224
224
  title={property.name ?? propertyKey}
225
- className={"h-8 text-text-secondary dark:text-text-secondary-dark ml-3.5"}/>}
226
- <div className="flex-grow"/>
225
+ className={"h-8 text-text-secondary dark:text-text-secondary-dark ml-3.5"}/>
226
+ </div>}
227
+ {/* `relative` so Clear can float inside the box. It used to sit in
228
+ the label row, which meant a clearable field with its label
229
+ lifted out by the form still drew a row of chrome above the box
230
+ and started lower than the field beside it.
231
+ Bottom-right, not top: the editor puts its own mode toggle at
232
+ `top-2 right-2` and the two would sit on each other. */}
233
+ <div
234
+ className={cls("relative rounded-md", fieldBackgroundMixin, disabled ? fieldBackgroundDisabledMixin : fieldBackgroundHoverMixin)}>
227
235
  {property.admin?.clearable && !disabled && (
228
236
  <IconButton
229
237
  size="small"
238
+ className="absolute bottom-1 right-1 z-10"
230
239
  onClick={(e) => {
231
240
  e.stopPropagation();
232
241
  e.preventDefault();
@@ -236,9 +245,6 @@ export function MarkdownEditorFieldBinding({
236
245
  <XIcon/>
237
246
  </IconButton>
238
247
  )}
239
- </div>
240
- <div
241
- className={cls("rounded-md", fieldBackgroundMixin, disabled ? fieldBackgroundDisabledMixin : fieldBackgroundHoverMixin)}>
242
248
  {editor}
243
249
  </div>
244
250
  <FieldHelperText includeDescription={includeDescription}
@@ -87,7 +87,13 @@ export function MultiSelectFieldBinding({
87
87
  return (
88
88
  <>
89
89
  <MultiSelect
90
- className={"w-full mt-2"}
90
+ // No top margin: `className` lands on the box itself, not on
91
+ // the label above it, so `mt-2` was 8px of gap between the
92
+ // form's label and this control and none between the label and
93
+ // the text field beside it. A binding renders a control, not a
94
+ // labelled control — the top of what it renders is the top of
95
+ // the box, or it will not line up with its neighbour.
96
+ className={"w-full"}
91
97
  size={size}
92
98
  value={validValue ? value.map((v) => v?.toString()) : []}
93
99
  disabled={disabled}
@@ -45,7 +45,7 @@ export function ReadOnlyFieldBinding({
45
45
  icon={getIconForProperty(property, "small")}
46
46
  required={property.validation?.required}
47
47
  title={property.name ?? propertyKey}
48
- className={"h-8 text-text-secondary dark:text-text-secondary-dark ml-3.5"}/>
48
+ className={"h-8 text-text-secondary dark:text-text-secondary-dark ml-3"}/>
49
49
  }
50
50
 
51
51
  <div
@@ -53,13 +53,32 @@ export function ReadOnlyFieldBinding({
53
53
  "w-full overflow-x-scroll no-scrollbar flex items-center",
54
54
  skipCardWrapper
55
55
  ? ""
56
- : "rounded-lg border border-surface-200 dark:border-surface-700 px-4 md:px-6 min-h-10 opacity-80"
56
+ // `min-h-12` is `TextField`'s own `large` height, which is
57
+ // what every editable field in the form is. At 40px a
58
+ // read-only field sat 8px short of the input beside it,
59
+ // so a row holding one of each had its two boxes'
60
+ // baselines and bottom edges disagree — the field looked
61
+ // misaligned rather than uneditable.
62
+ // `px-3` is what `TextField` gives its input, so a
63
+ // read-only value starts on the same vertical line as
64
+ // the text beside it. It was `px-4 md:px-6`, which in a
65
+ // one-column span spent a sixth of the box on empty
66
+ // margin and pushed short values towards the middle.
67
+ : "rounded-lg border border-surface-200 dark:border-surface-700 px-3 min-h-12 opacity-80"
57
68
  )}>
58
69
 
59
70
  <ErrorBoundary>
71
+ {/* The field is labelled either just above by this
72
+ component, or by the `FieldBlock` that asked for
73
+ `hideLabel` — never by the preview. `BooleanPreview`
74
+ prints the property name beside its checkbox, so a
75
+ read-only boolean read as "Procesado por LLM" over a
76
+ checkbox saying "Procesado por LLM", wrapped over two
77
+ lines in a one-column span. */}
60
78
  <PropertyPreview propertyKey={propertyKey}
61
79
  value={value}
62
80
  property={property}
81
+ hideLabel={hideLabel || !minimalistView}
63
82
  size={"medium"}/>
64
83
  </ErrorBoundary>
65
84
 
@@ -121,7 +121,7 @@ function ReferenceFieldBindingInternal({
121
121
  />}
122
122
 
123
123
  {!refValue && <div className="justify-center text-left">
124
- <EntityPreviewContainer className={cls("px-6 h-16 text-sm font-medium flex items-center gap-6",
124
+ <EntityPreviewContainer className={cls("px-6 h-12 text-sm font-medium flex items-center gap-6",
125
125
  disabled || isSubmitting
126
126
  ? "text-surface-accent-500"
127
127
  : "cursor-pointer text-surface-accent-700 dark:text-surface-accent-300 hover:bg-surface-accent-50 dark:hover:bg-surface-800 group-hover:bg-surface-accent-50 dark:group-hover:bg-surface-800")}
@@ -72,7 +72,10 @@ function RelationSelectorBinding({
72
72
  const singleValue = normalizedSingle ?? null;
73
73
  const multipleValue = (manyRelation && Array.isArray(value)) ? value : [];
74
74
 
75
- const selectorSize: "small" | "medium" | undefined = size === "large" ? "medium" : size;
75
+ // Passed straight through. Mapping the form's `large` onto the selector's
76
+ // `medium` produced a 56px box next to 48px text fields — the labels lined
77
+ // up and the boxes did not.
78
+ const selectorSize = size;
76
79
 
77
80
  return (
78
81
  <div className="">
@@ -176,7 +179,7 @@ function SingleRelationFieldBinding({
176
179
  />}
177
180
 
178
181
  {!value && <div className="justify-center text-left">
179
- <EntityPreviewContainer className={cls("px-6 h-16 text-sm font-medium flex items-center gap-6",
182
+ <EntityPreviewContainer className={cls("px-6 h-12 text-sm font-medium flex items-center gap-6",
180
183
  disabled || isSubmitting
181
184
  ? "text-surface-accent-500"
182
185
  : "cursor-pointer text-surface-accent-700 dark:text-surface-accent-300 hover:bg-surface-accent-50 dark:hover:bg-surface-800 group-hover:bg-surface-accent-50 dark:group-hover:bg-surface-800")}
@@ -11,7 +11,7 @@ import { getIconForProperty } from "../../util/property_utils";
11
11
  import { PropertyFieldBinding } from "../PropertyFieldBinding";
12
12
  import { ExpandablePanel, Typography } from "@rebasepro/ui";
13
13
  import { useClearRestoreValue } from "../useClearRestoreValue";
14
- import { useAuthController, useTranslation } from "@rebasepro/app";
14
+ import { isDisabled, useAuthController, useTranslation } from "@rebasepro/app";
15
15
  import { mergeDeep } from "@rebasepro/utils";
16
16
 
17
17
  /**
@@ -96,14 +96,14 @@ export function RepeatFieldBinding({
96
96
  </ErrorBoundary>;
97
97
  };
98
98
 
99
- const canAddElements = !property.admin?.disabled && !isSubmitting && !disabled && (property.admin?.canAddElements || property.admin?.canAddElements === undefined);
99
+ const canAddElements = !isDisabled(property) && !isSubmitting && !disabled && (property.admin?.canAddElements || property.admin?.canAddElements === undefined);
100
100
  const sortable = property.admin?.sortable === undefined ? true : property.admin.sortable;
101
101
  const arrayContainer = <ArrayContainer droppableId={propertyKey}
102
102
  addLabel={property.name ? t("add_to_field", { fieldName: property.name }) : t("add_entry")}
103
103
  value={(value as unknown[]) ?? []}
104
104
  buildEntry={buildEntry}
105
105
  onInternalIdAdded={setLastAddedId}
106
- disabled={isSubmitting || Boolean(property.admin?.disabled)}
106
+ disabled={isSubmitting || isDisabled(property)}
107
107
  canAddElements={canAddElements}
108
108
  sortable={sortable}
109
109
  newDefaultEntry={getDefaultValueFor(property.of)}
@@ -8,7 +8,7 @@ import { useDropzone } from "react-dropzone";
8
8
  import { FieldHelperText } from "../components/FieldHelperText";
9
9
  import { LabelWithIconAndTooltip } from "../components/LabelWithIconAndTooltip";
10
10
 
11
- import { isReadOnly } from "@rebasepro/app";
11
+ import { isDisabled, isReadOnly } from "@rebasepro/app";
12
12
  import { getIconForProperty } from "../../util/property_utils";
13
13
  import { useAuthController, useSnackbarController, useStorageSource, useTranslation } from "@rebasepro/app";
14
14
  import { StorageSourceContext } from "@rebasepro/app";
@@ -74,7 +74,7 @@ export function StorageUploadFieldBinding({
74
74
  const authController = useAuthController();
75
75
 
76
76
  const storageSource = useStorageSource();
77
- const disabled = isReadOnly(property) || !!property.admin?.disabled || isSubmitting || context.disabled;
77
+ const disabled = isReadOnly(property) || isDisabled(property) || isSubmitting || context.disabled;
78
78
 
79
79
  const {
80
80
  internalValue,
@@ -34,7 +34,10 @@ export function UserSelectFieldBinding({
34
34
  size = "large"
35
35
  }: UserSelectProps) {
36
36
 
37
- const selectorSize: "small" | "medium" | undefined = size === "large" ? "medium" : size;
37
+ // Passed straight through. Mapping the form's `large` onto the selector's
38
+ // `medium` produced a 56px box next to 48px text fields — the labels lined
39
+ // up and the boxes did not.
40
+ const selectorSize = size;
38
41
 
39
42
  return (
40
43
  <>
@@ -98,11 +98,13 @@ export function VectorFieldBinding({
98
98
  const isPopulated = arrayValue.length > 0;
99
99
 
100
100
  return (
101
- <div className="flex flex-col gap-2 w-full mt-2">
102
- {/* Field Label */}
103
- <div className="flex items-center justify-between mb-1">
101
+ <div className="flex flex-col gap-2 w-full">
102
+ {/* Only when there is a label to put in it — an empty row still
103
+ spends its `mb-1` and the column's `gap-2`, and the control ends
104
+ up below the one beside it under a label at the same height. */}
105
+ {label && <div className="flex items-center justify-between mb-1">
104
106
  {label}
105
- </div>
107
+ </div>}
106
108
 
107
109
  <PropertyIdCopyTooltip propertyKey={propertyKey}>
108
110
  <div className="w-full">
@@ -114,6 +114,84 @@ export function getChanges<T extends object>(source: Partial<T>, comparison: Par
114
114
  return changes;
115
115
  }
116
116
 
117
+ /**
118
+ * What the local-changes backup still has to offer, given what the form is
119
+ * already showing — the answer the "unsaved local changes" banner is asking for.
120
+ * `undefined` means there is nothing left to apply, and no banner.
121
+ *
122
+ * Measured against what the form *opens showing*, not against the stored
123
+ * record. The banner exists to offer a draft the form is not already displaying,
124
+ * and an edit carried across a change of layout is in both the backup and the
125
+ * form: measuring against the stored values alone offered to restore the values
126
+ * already on screen, over an edit the user had never left.
127
+ */
128
+ export function getUnappliedLocalChanges<M extends Record<string, unknown>>(
129
+ backupValues: Partial<M>,
130
+ openingValues: Partial<M>
131
+ ): Partial<M> | undefined {
132
+ const changes = getChanges(backupValues, openingValues);
133
+ const cleaned = removeEmptyContainers(changes);
134
+ if (!cleaned || typeof cleaned !== "object" || Object.keys(cleaned).length === 0) {
135
+ return undefined;
136
+ }
137
+ return cleaned as Partial<M>;
138
+ }
139
+
140
+ /**
141
+ * What travels when the same record changes layout — the split's "hide list",
142
+ * full screen's "show list", the side panel's "open full screen". One mounted
143
+ * form is replaced by another showing the same record, so the edit in progress
144
+ * has to be handed over; left to the local-changes backup alone, the new form
145
+ * met it as a draft from a closed tab and raised the "unsaved local changes"
146
+ * banner over changes the user had made a second earlier and never left.
147
+ *
148
+ * Returns `undefined` when there is nothing to carry, which is its own answer: a
149
+ * record nobody edited must open in the next layout exactly as clean as it was.
150
+ *
151
+ * Only what was edited here travels, keyed by `touched` — the same subset the
152
+ * backup stores. Carrying the whole record instead marks every field touched in
153
+ * the receiving form, which lights up every empty required field as an error and
154
+ * writes the entire record back out as a "local change".
155
+ */
156
+ export function getEditHandoffValues<M extends Record<string, unknown>>({
157
+ status,
158
+ dirty,
159
+ values,
160
+ touched,
161
+ storedValues
162
+ }: {
163
+ status: EntityStatus;
164
+ /** Whether `values` differ from what is stored. */
165
+ dirty: boolean;
166
+ values: Partial<M>;
167
+ touched: Record<string, boolean>;
168
+ /** The stored record, for the fallback below. Absent for a new one. */
169
+ storedValues?: Partial<M>;
170
+ }): Partial<M> | undefined {
171
+
172
+ if (!values) return undefined;
173
+
174
+ // A record with nothing stored behind it carries whole: there is no
175
+ // baseline to fall back to, so anything not carried is simply lost.
176
+ if (status === "new" || status === "copy") {
177
+ return Object.keys(values).length > 0 ? values : undefined;
178
+ }
179
+
180
+ if (!dirty) return undefined;
181
+
182
+ const touchedValues = removeEmptyContainers(extractTouchedValues(values, touched ?? {})) as Partial<M> | undefined;
183
+ if (touchedValues && Object.keys(touchedValues).length > 0) {
184
+ return touchedValues;
185
+ }
186
+
187
+ // Dirty with nothing touched — a value set programmatically, by a plugin or
188
+ // a custom field that writes through `setFieldValue` without the blur that
189
+ // marks it. Diffing against the stored record still finds it, and dropping
190
+ // it here would hand over a form that opens clean over an edit.
191
+ const changes = getChanges(values, storedValues ?? {});
192
+ return Object.keys(changes).length > 0 ? changes : undefined;
193
+ }
194
+
117
195
  export function getInitialEntityValues<M extends Record<string, unknown>>(
118
196
  authController: AuthController,
119
197
  collection: AdminCollection,
@@ -41,11 +41,35 @@ export function useBuildCollectionRegistryController(props: {
41
41
  if (!collectionPath) return undefined;
42
42
 
43
43
  let collection: AdminCollection | undefined;
44
- try {
45
- collection = registry.resolvePathToCollections(collectionPath).finalCollection;
46
- } catch (e) {
47
- console.debug(`Could not resolve path to collection: ${collectionPath}`, e);
48
- return undefined;
44
+
45
+ // An exact slug match first, because a slug is allowed to contain
46
+ // slashes and several drivers need it to: a Firestore collection
47
+ // partitioned by locale is declared as `content/de-DE/podcasts`, and
48
+ // that string is its *name*, not a path to walk.
49
+ //
50
+ // Without this, such a slug went straight to `resolvePathToCollections`,
51
+ // which reads any `a/b/c` as collection/entity/subcollection, looked for
52
+ // a root collection called `content`, found none and threw — and the
53
+ // catch below turned that into `undefined`. `RebaseRoute` renders
54
+ // `null` for an unresolved collection, so the whole view came out blank
55
+ // with nothing in the console above `debug`. Thirty-five collections in
56
+ // one app were unreachable that way.
57
+ // `collectionPath`, not `cleanedPath`: an even segment count means the
58
+ // path ends at a record, and the id has already been trimmed off above.
59
+ // Matching the raw path instead would only ever hit for a collection
60
+ // route, leaving `content/de-DE/podcasts/abc123` back on the walker.
61
+ collection = registry.get(collectionPath) as AdminCollection | undefined;
62
+
63
+ if (!collection) {
64
+ try {
65
+ collection = registry.resolvePathToCollections(collectionPath).finalCollection;
66
+ } catch (e) {
67
+ // Warn, not debug. An unresolved collection ends as a route with
68
+ // nothing in it, and at `debug` there was no way to tell that
69
+ // from a collection that legitimately has no rows.
70
+ console.warn(`[rebase] Could not resolve a collection for "${collectionPath}"`, e);
71
+ return undefined;
72
+ }
49
73
  }
50
74
 
51
75
  if (!collection) {