@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.
- package/dist/{CollectionEditorDialog-BykOIZH4.js → CollectionEditorDialog-BeS4lxGh.js} +3 -3
- package/dist/{CollectionEditorDialog-BykOIZH4.js.map → CollectionEditorDialog-BeS4lxGh.js.map} +1 -1
- package/dist/{PropertyEditView-C9Der_Ej.js → PropertyEditView-BFJ6H9-Q.js} +2 -2
- package/dist/{PropertyEditView-C9Der_Ej.js.map → PropertyEditView-BFJ6H9-Q.js.map} +1 -1
- package/dist/{RouterCollectionsStudioView-CfTHbBCA.js → RouterCollectionsStudioView-sfsUpEJz.js} +4 -4
- package/dist/{RouterCollectionsStudioView-CfTHbBCA.js.map → RouterCollectionsStudioView-sfsUpEJz.js.map} +1 -1
- package/dist/collection_editor_ui.js +4 -4
- package/dist/{export-BDynF1En.js → export-KEf57Jso.js} +2 -2
- package/dist/{export-BDynF1En.js.map → export-KEf57Jso.js.map} +1 -1
- package/dist/{history-DS2iXiGi.js → history-Ctd2cRn_.js} +2 -2
- package/dist/{history-DS2iXiGi.js.map → history-Ctd2cRn_.js.map} +1 -1
- package/dist/{import-J5vb86xj.js → import-BujhycMq.js} +2 -2
- package/dist/{import-J5vb86xj.js.map → import-BujhycMq.js.map} +1 -1
- package/dist/index.js +6 -6
- package/dist/{util-WQP-ljKW.js → util-bL-fk6C6.js} +46 -37
- package/dist/util-bL-fk6C6.js.map +1 -0
- package/package.json +9 -9
- package/src/components/CollectionViewBinding/CollectionListViewBinding.tsx +25 -1
- package/src/components/CollectionViewBinding/CollectionViewStartActions.tsx +11 -8
- package/src/components/CollectionViewBinding/SortButton.tsx +82 -41
- package/src/components/DetailViewBinding.tsx +6 -1
- 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
|
|
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/
|
|
87
|
-
"@rebasepro/
|
|
88
|
-
"@rebasepro/
|
|
89
|
-
"@rebasepro/
|
|
90
|
-
"@rebasepro/
|
|
91
|
-
"@rebasepro/
|
|
92
|
-
"@rebasepro/
|
|
93
|
-
"@rebasepro/
|
|
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={
|
|
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
|
-
//
|
|
145
|
-
//
|
|
146
|
-
//
|
|
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
|
-
//
|
|
150
|
-
//
|
|
151
|
-
//
|
|
152
|
-
|
|
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
|
-
|
|
132
|
-
|
|
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")}: ${
|
|
135
|
-
: `${t("sort_by")}: ${
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
-
{
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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
|
-
|
|
280
|
-
|
|
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
|
-
{
|
|
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
|
}
|