@rebasepro/admin 0.14.1-canary.g7e666eb → 0.14.1

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 (22) hide show
  1. package/dist/{CollectionEditorDialog-BykOIZH4.js → CollectionEditorDialog-BeS4lxGh.js} +3 -3
  2. package/dist/{CollectionEditorDialog-BykOIZH4.js.map → CollectionEditorDialog-BeS4lxGh.js.map} +1 -1
  3. package/dist/{PropertyEditView-C9Der_Ej.js → PropertyEditView-BFJ6H9-Q.js} +2 -2
  4. package/dist/{PropertyEditView-C9Der_Ej.js.map → PropertyEditView-BFJ6H9-Q.js.map} +1 -1
  5. package/dist/{RouterCollectionsStudioView-CfTHbBCA.js → RouterCollectionsStudioView-sfsUpEJz.js} +4 -4
  6. package/dist/{RouterCollectionsStudioView-CfTHbBCA.js.map → RouterCollectionsStudioView-sfsUpEJz.js.map} +1 -1
  7. package/dist/collection_editor_ui.js +4 -4
  8. package/dist/{export-BDynF1En.js → export-KEf57Jso.js} +2 -2
  9. package/dist/{export-BDynF1En.js.map → export-KEf57Jso.js.map} +1 -1
  10. package/dist/{history-DS2iXiGi.js → history-Ctd2cRn_.js} +2 -2
  11. package/dist/{history-DS2iXiGi.js.map → history-Ctd2cRn_.js.map} +1 -1
  12. package/dist/{import-J5vb86xj.js → import-BujhycMq.js} +2 -2
  13. package/dist/{import-J5vb86xj.js.map → import-BujhycMq.js.map} +1 -1
  14. package/dist/index.js +6 -6
  15. package/dist/{util-WQP-ljKW.js → util-bL-fk6C6.js} +46 -37
  16. package/dist/util-bL-fk6C6.js.map +1 -0
  17. package/package.json +9 -9
  18. package/src/components/CollectionViewBinding/CollectionListViewBinding.tsx +25 -1
  19. package/src/components/CollectionViewBinding/CollectionViewStartActions.tsx +11 -8
  20. package/src/components/CollectionViewBinding/SortButton.tsx +82 -41
  21. package/src/components/DetailViewBinding.tsx +6 -1
  22. package/dist/util-WQP-ljKW.js.map +0 -1
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@rebasepro/admin",
3
3
  "type": "module",
4
- "version": "0.14.1-canary.g7e666eb",
4
+ "version": "0.14.1",
5
5
  "description": "Rebase CMS — content management views, forms, and routing",
6
6
  "funding": {
7
7
  "url": "https://github.com/sponsors/rebaseco"
@@ -83,14 +83,14 @@
83
83
  "react-dropzone": "^19.1.1",
84
84
  "react-use-measure": "^2.1.7",
85
85
  "zod": "^4.4.3",
86
- "@rebasepro/common": "0.14.1-canary.g7e666eb",
87
- "@rebasepro/app": "0.14.1-canary.g7e666eb",
88
- "@rebasepro/forms": "0.14.1-canary.g7e666eb",
89
- "@rebasepro/inference": "0.14.1-canary.g7e666eb",
90
- "@rebasepro/ui": "0.14.1-canary.g7e666eb",
91
- "@rebasepro/utils": "0.14.1-canary.g7e666eb",
92
- "@rebasepro/admin-types": "0.14.1-canary.g7e666eb",
93
- "@rebasepro/types": "0.14.1-canary.g7e666eb"
86
+ "@rebasepro/admin-types": "0.14.1",
87
+ "@rebasepro/forms": "0.14.1",
88
+ "@rebasepro/app": "0.14.1",
89
+ "@rebasepro/common": "0.14.1",
90
+ "@rebasepro/inference": "0.14.1",
91
+ "@rebasepro/types": "0.14.1",
92
+ "@rebasepro/ui": "0.14.1",
93
+ "@rebasepro/utils": "0.14.1"
94
94
  },
95
95
  "peerDependencies": {
96
96
  "react": ">=19.2.7",
@@ -24,6 +24,7 @@ import { useAnalyticsController } from "@rebasepro/app";
24
24
  import { IconForView } from "@rebasepro/app";
25
25
  import { getIcon } from "@rebasepro/app";
26
26
  import { hasDeclaredDisplay } from "@rebasepro/app";
27
+ import { useTranslation } from "@rebasepro/app";
27
28
  import { formatRelativeTime, getValueInPath } from "@rebasepro/utils";
28
29
  import { useCollectionSlotKeys, useEntitySlots, type CollectionSlotKeys, type EntityPreviewSlots } from "./usePreviewSlots";
29
30
  import { SlotValue, TagChips } from "./SlotValue";
@@ -1318,6 +1319,11 @@ function ListHeader({
1318
1319
  />
1319
1320
  );
1320
1321
 
1322
+ // No `role="columnheader"` / `aria-sort` here, tempting as they are: both
1323
+ // are only valid inside a `row` inside a `table`/`grid`, and neither this
1324
+ // header nor the virtualized `ListView` under it declares one. An orphaned
1325
+ // columnheader is worse than none. The sort state and the rank ride on the
1326
+ // header button's `aria-label` instead — see `ListHeaderLabel`.
1321
1327
  return (
1322
1328
  <div className={cls(
1323
1329
  "flex items-center gap-4 px-5 py-1.5 select-none border-b bg-surface-50 dark:bg-surface-900",
@@ -1371,6 +1377,8 @@ function ListHeaderLabel({
1371
1377
  position?: number;
1372
1378
  onSort?: (additive: boolean) => void;
1373
1379
  }) {
1380
+ const { t } = useTranslation();
1381
+
1374
1382
  const content = (
1375
1383
  <>
1376
1384
  <span className="truncate">{column.label}</span>
@@ -1393,10 +1401,26 @@ function ListHeaderLabel({
1393
1401
  return <span className={cls(base, tone)}>{content}</span>;
1394
1402
  }
1395
1403
 
1404
+ // What the *next* click does, which is the only thing a user needs from a
1405
+ // control with three states. This said "Sort by <column>" in every state,
1406
+ // so on a descending column the tooltip promised a sort where the click
1407
+ // removed one — and it said it in English regardless of the panel's
1408
+ // language, alone among the toolbar's sort controls.
1409
+ const nextAction = direction === "asc"
1410
+ ? t("sort_descending")
1411
+ : direction === "desc"
1412
+ ? t("sort_remove")
1413
+ : t("sort_ascending");
1414
+ const rank = direction && position !== undefined
1415
+ ? ` (${t("sort_key_position", { position: position + 1 })})`
1416
+ : "";
1417
+ const label = `${column.label}${rank} — ${nextAction}. ${t("sort_shift_click_hint")}`;
1418
+
1396
1419
  return (
1397
1420
  <button
1398
1421
  type="button"
1399
- title={`Sort by ${column.label} — shift-click to add it under the current sort`}
1422
+ title={label}
1423
+ aria-label={label}
1400
1424
  onClick={(event) => onSort(event.shiftKey)}
1401
1425
  className={cls(base, tone, "cursor-pointer hover:text-surface-700 dark:hover:text-surface-200 transition-colors")}
1402
1426
  >
@@ -141,15 +141,18 @@ parentEntityIds,
141
141
  </Tooltip>
142
142
  );
143
143
 
144
- // Ordering the collection is a click on a column header which only the
145
- // table has. The list and card views showed rows in whatever order the
146
- // query returned with no way to change it, so they get the control in the
147
- // toolbar instead.
144
+ // Everywhere a `sortBy` means something which is every view but the
145
+ // board, whose columns are ordered by their own order key and which ignores
146
+ // `sortBy` entirely, so the control would appear to do nothing.
148
147
  //
149
- // Not the table, where the headers already do this and say which column is
150
- // sorted; and not the board, which orders its columns by its own order key
151
- // and ignores `sortBy` entirely, so the control would appear to do nothing.
152
- const sortButton = resolvedProperties && (viewMode === "list" || viewMode === "cards") && (
148
+ // Including the table, whose headers were once thought to cover it. They
149
+ // build a multi-key sort (shift-click) and they show its ranks, but they
150
+ // cannot *re-rank* it: promoting the third key above the first means
151
+ // clearing the sort and shift-clicking all three back in the new order.
152
+ // Nor can they remove a middle key without cycling it through descending
153
+ // first. This popover is the only place either is one click, and a table
154
+ // with a three-key sort needs it more than a card grid does, not less.
155
+ const sortButton = resolvedProperties && viewMode !== "kanban" && (
153
156
  <SortButton
154
157
  key={"sort_button"}
155
158
  tableController={tableController}
@@ -10,6 +10,7 @@ import {
10
10
  ChevronDownIcon,
11
11
  ChevronUpIcon,
12
12
  cls,
13
+ defaultBorderMixin,
13
14
  iconSize,
14
15
  IconButton,
15
16
  Popover,
@@ -114,6 +115,12 @@ position });
114
115
  applySort(keys.filter(([existing]) => existing !== key));
115
116
  }, [applySort, keys]);
116
117
 
118
+ /** What a click on an active key will do to it, for the tooltip and the label. */
119
+ const flipLabel = useCallback(
120
+ (direction: "asc" | "desc") => direction === "asc" ? t("sort_descending") : t("sort_ascending"),
121
+ [t]
122
+ );
123
+
117
124
  /** Swap a key with its neighbour, which is what changes which one decides. */
118
125
  const moveKey = useCallback((position: number, delta: -1 | 1) => {
119
126
  const target = position + delta;
@@ -128,11 +135,18 @@ position });
128
135
  }
129
136
 
130
137
  const primary = keys[0];
131
- const primaryOption = primary ? options.find(option => option.key === primary[0]) : undefined;
132
- const label = primaryOption
138
+ // The key's own name when it has no option to be titled by — a sort can
139
+ // outlive the column it names (a property hidden since, a `collection.sort`
140
+ // on something the panel does not render). Falling back to "Sort" there
141
+ // showed the neutral icon and the inactive label over a collection that
142
+ // *was* sorted, which is the one thing this control exists to report.
143
+ const primaryTitle = primary
144
+ ? options.find(option => option.key === primary[0])?.title ?? primary[0]
145
+ : undefined;
146
+ const label = primaryTitle
133
147
  ? keys.length > 1
134
- ? `${t("sort_by")}: ${primaryOption.title} +${keys.length - 1}`
135
- : `${t("sort_by")}: ${primaryOption.title}`
148
+ ? `${t("sort_by")}: ${primaryTitle} +${keys.length - 1}`
149
+ : `${t("sort_by")}: ${primaryTitle}`
136
150
  : t("sort");
137
151
 
138
152
  // Icon only, at every width. The arrow says which way the order runs; which
@@ -152,7 +166,7 @@ position });
152
166
  size={"small"}
153
167
  aria-label={label}
154
168
  title={label}
155
- className={cls(primaryOption && "text-primary")}
169
+ className={cls(primaryTitle && "text-primary")}
156
170
  >
157
171
  {/* Same icon size as the filter controls it sits between: 16 in the
158
172
  split view's narrow toolbar, 20 everywhere else. It was a flat
@@ -186,7 +200,11 @@ position });
186
200
  </Typography>
187
201
 
188
202
  {keys.length > 0 && (
189
- <div className="flex flex-col">
203
+ // Bounded for the same reason the option list below is: the
204
+ // active keys are as many as the collection has sortable
205
+ // columns, and an unbounded column pushed "Clear sort" off
206
+ // the bottom of the screen on a wide collection.
207
+ <div className="flex flex-col max-h-[200px] overflow-y-auto">
190
208
  {keys.map(([key, direction], position) => {
191
209
  const option = options.find(o => o.key === key);
192
210
  return (
@@ -202,7 +220,8 @@ position });
202
220
  <button
203
221
  type="button"
204
222
  onClick={() => onOptionClick(key)}
205
- title={direction === "asc" ? t("sort_descending") : t("sort_ascending")}
223
+ title={flipLabel(direction)}
224
+ aria-label={`${option?.title ?? key} — ${t("sort_key_position", { position: position + 1 })}. ${flipLabel(direction)}`}
206
225
  className="flex items-center gap-2 flex-grow min-w-0 text-left cursor-pointer"
207
226
  >
208
227
  {option && (
@@ -217,49 +236,65 @@ position });
217
236
  : <ArrowUpIcon size={iconSize.smallest}/>}
218
237
  </span>
219
238
  </button>
220
- {keys.length > 1 && (
221
- <>
222
- <IconButton
223
- size="smallest"
224
- aria-label={t("sort_move_up")}
225
- title={t("sort_move_up")}
226
- disabled={position === 0}
227
- onClick={() => moveKey(position, -1)}
228
- >
229
- <ChevronUpIcon size={iconSize.smallest}/>
230
- </IconButton>
231
- <IconButton
232
- size="smallest"
233
- aria-label={t("sort_move_down")}
234
- title={t("sort_move_down")}
235
- disabled={position === keys.length - 1}
236
- onClick={() => moveKey(position, 1)}
237
- >
238
- <ChevronDownIcon size={iconSize.smallest}/>
239
- </IconButton>
240
- </>
241
- )}
242
- <IconButton
243
- size="smallest"
244
- aria-label={t("sort_remove_key")}
245
- title={t("sort_remove_key")}
246
- onClick={() => removeKey(key)}
247
- >
248
- <XIcon size={iconSize.smallest}/>
249
- </IconButton>
239
+ {/* Ruled off from the key itself. The row
240
+ carries two arrows that mean different
241
+ things — the direction this key runs, and
242
+ which key outranks which — and side by
243
+ side at 14px they read as one control
244
+ with a stutter. The rule says where the
245
+ key ends and the controls on it begin. */}
246
+ <div className={cls(
247
+ "flex-shrink-0 flex items-center gap-0.5 ml-1 pl-1 border-l",
248
+ defaultBorderMixin
249
+ )}>
250
+ {keys.length > 1 && (
251
+ <>
252
+ <IconButton
253
+ size="smallest"
254
+ aria-label={t("sort_move_up")}
255
+ title={t("sort_move_up")}
256
+ disabled={position === 0}
257
+ onClick={() => moveKey(position, -1)}
258
+ >
259
+ <ChevronUpIcon size={iconSize.smallest}/>
260
+ </IconButton>
261
+ <IconButton
262
+ size="smallest"
263
+ aria-label={t("sort_move_down")}
264
+ title={t("sort_move_down")}
265
+ disabled={position === keys.length - 1}
266
+ onClick={() => moveKey(position, 1)}
267
+ >
268
+ <ChevronDownIcon size={iconSize.smallest}/>
269
+ </IconButton>
270
+ </>
271
+ )}
272
+ <IconButton
273
+ size="smallest"
274
+ aria-label={t("sort_remove_key")}
275
+ title={t("sort_remove_key")}
276
+ onClick={() => removeKey(key)}
277
+ >
278
+ <XIcon size={iconSize.smallest}/>
279
+ </IconButton>
280
+ </div>
250
281
  </div>
251
282
  );
252
283
  })}
253
284
  </div>
254
285
  )}
255
286
 
256
- {inactiveOptions.length > 0 && (
287
+ {/* Only once there is a key to come *after*. With none, the
288
+ "Sort by" heading above already names the list, and an empty
289
+ caption here left a band of padding that read as a divider
290
+ between two groups where there was only one. */}
291
+ {keys.length > 0 && inactiveOptions.length > 0 && (
257
292
  <Typography
258
293
  variant="caption"
259
294
  color="secondary"
260
295
  className="font-medium uppercase tracking-wider px-3 pt-2 pb-1"
261
296
  >
262
- {keys.length > 0 ? t("sort_then_by") : ""}
297
+ {t("sort_then_by")}
263
298
  </Typography>
264
299
  )}
265
300
 
@@ -276,8 +311,14 @@ position });
276
311
  <button
277
312
  key={key}
278
313
  type="button"
279
- role="menuitemradio"
280
- aria-checked={false}
314
+ // Not `menuitemradio`: a sort is a *list* of
315
+ // keys, so picking one does not unpick another,
316
+ // and the role announced the opposite. There is
317
+ // no `role="menu"` around this either, which
318
+ // left the radio orphaned as well as wrong.
319
+ aria-label={keys.length > 0
320
+ ? `${t("sort_then_by")} ${title}`
321
+ : `${t("sort_by")} ${title}`}
281
322
  disabled={disabled}
282
323
  onClick={() => onOptionClick(key)}
283
324
  className={cls(
@@ -383,7 +383,12 @@ entityId }
383
383
  openEntityMode={layout} />
384
384
  : <div className="flex items-center justify-center w-full h-full p-3">
385
385
  <Typography variant={"label"}>
386
- {t("save_entity_before_subcollections") ?? "You need to save your entity before adding additional collections"}
386
+ {/* No `?? "…"` here: `t` returns the key itself
387
+ when it cannot find one, never null, so the
388
+ fallback never ran and this rendered the raw
389
+ `save_entity_before_subcollections`. The key
390
+ exists now, in all seven locales. */}
391
+ {t("save_entity_before_subcollections")}
387
392
  </Typography>
388
393
  </div>)
389
394
  }