@rebasepro/admin 0.14.0 → 0.14.1-canary.g7e666eb
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-CSheud_E.js → CollectionEditorDialog-BykOIZH4.js} +3 -3
- package/dist/{CollectionEditorDialog-CSheud_E.js.map → CollectionEditorDialog-BykOIZH4.js.map} +1 -1
- package/dist/{PropertyEditView-CWZlC1gq.js → PropertyEditView-C9Der_Ej.js} +2 -2
- package/dist/{PropertyEditView-CWZlC1gq.js.map → PropertyEditView-C9Der_Ej.js.map} +1 -1
- package/dist/{RouterCollectionsStudioView-CwTpdzWF.js → RouterCollectionsStudioView-CfTHbBCA.js} +4 -4
- package/dist/{RouterCollectionsStudioView-CwTpdzWF.js.map → RouterCollectionsStudioView-CfTHbBCA.js.map} +1 -1
- package/dist/collection_editor_ui.js +4 -4
- package/dist/components/CollectionViewBinding/CollectionViewBinding.d.ts +2 -1
- package/dist/components/CollectionViewBinding/SortButton.d.ts +14 -4
- package/dist/{export-L53WektO.js → export-BDynF1En.js} +2 -2
- package/dist/{export-L53WektO.js.map → export-BDynF1En.js.map} +1 -1
- package/dist/{history-D5SKygpJ.js → history-DS2iXiGi.js} +2 -2
- package/dist/{history-D5SKygpJ.js.map → history-DS2iXiGi.js.map} +1 -1
- package/dist/{import-C_4edkoD.js → import-J5vb86xj.js} +2 -2
- package/dist/{import-C_4edkoD.js.map → import-J5vb86xj.js.map} +1 -1
- package/dist/index.js +6 -6
- package/dist/{util-C-D5yD2n.js → util-WQP-ljKW.js} +206 -90
- package/dist/util-WQP-ljKW.js.map +1 -0
- package/package.json +9 -9
- package/src/collection_editor/ui/EditorCollectionActionStart.tsx +3 -2
- package/src/components/CollectionViewBinding/CollectionListViewBinding.tsx +80 -39
- package/src/components/CollectionViewBinding/CollectionViewBinding.tsx +19 -8
- package/src/components/CollectionViewBinding/FilterPresetsButton.tsx +6 -5
- package/src/components/CollectionViewBinding/SortButton.tsx +149 -37
- package/src/components/SelectableTable/SelectableTable.tsx +1 -1
- package/dist/util-C-D5yD2n.js.map +0 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Properties } from "@rebasepro/types";
|
|
1
|
+
import type { OrderByTuple, Properties } from "@rebasepro/types";
|
|
2
2
|
|
|
3
3
|
import React, { useCallback, useMemo, useState } from "react";
|
|
4
4
|
import { EntityTableController } from "@rebasepro/admin-types";
|
|
@@ -7,11 +7,14 @@ import {
|
|
|
7
7
|
ArrowUpDownIcon,
|
|
8
8
|
ArrowUpIcon,
|
|
9
9
|
Button,
|
|
10
|
+
ChevronDownIcon,
|
|
11
|
+
ChevronUpIcon,
|
|
10
12
|
cls,
|
|
11
13
|
iconSize,
|
|
12
14
|
IconButton,
|
|
13
15
|
Popover,
|
|
14
|
-
Typography
|
|
16
|
+
Typography,
|
|
17
|
+
XIcon
|
|
15
18
|
} from "@rebasepro/ui";
|
|
16
19
|
import { useTranslation } from "@rebasepro/app";
|
|
17
20
|
import { getSortablePropertyOptions } from "../CollectionTableBinding/column_utils";
|
|
@@ -33,16 +36,26 @@ export type SortButtonProps<M extends Record<string, unknown>> = {
|
|
|
33
36
|
};
|
|
34
37
|
|
|
35
38
|
/**
|
|
36
|
-
* Toolbar control that orders the collection
|
|
39
|
+
* Toolbar control that orders the collection.
|
|
37
40
|
*
|
|
38
41
|
* The table orders by clicking a column header, which leaves every other view
|
|
39
42
|
* mode — list, cards, and either of them inside a split view — with no way to
|
|
40
43
|
* order at all, even though the controller has always carried `sortBy` and the
|
|
41
44
|
* query has always honoured it. This binds a control to it.
|
|
42
45
|
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
+
* It edits the *whole* order rather than a single key. A sort can have several
|
|
47
|
+
* keys, applied in the order shown: the second decides between rows the first
|
|
48
|
+
* calls equal, the third between rows the first two do. So the active keys sit
|
|
49
|
+
* at the top, numbered and movable, and the properties below them are the ones
|
|
50
|
+
* that can still be added under the last. Shift-clicking a column header does
|
|
51
|
+
* the same thing from the table; this is the view for the modes that have no
|
|
52
|
+
* headers, and the only place the order between keys can be changed.
|
|
53
|
+
*
|
|
54
|
+
* Clicking an active key flips its direction, so ascending → descending is one
|
|
55
|
+
* click rather than a trip through a direction selector; clicking an inactive
|
|
56
|
+
* one appends it ascending. The popover stays open through all of it — building
|
|
57
|
+
* a two-key order out of a control that closes on every click is four round
|
|
58
|
+
* trips through a button.
|
|
46
59
|
*/
|
|
47
60
|
export function SortButton<M extends Record<string, unknown>>({
|
|
48
61
|
tableController,
|
|
@@ -64,11 +77,18 @@ export function SortButton<M extends Record<string, unknown>>({
|
|
|
64
77
|
checkFilterCombination
|
|
65
78
|
} = tableController;
|
|
66
79
|
|
|
67
|
-
const
|
|
68
|
-
const
|
|
80
|
+
const keys = useMemo(() => (sortBy ?? []) as OrderByTuple[], [sortBy]);
|
|
81
|
+
const activeIndex = useMemo(() => {
|
|
82
|
+
const index = new Map<string, { direction: "asc" | "desc"; position: number }>();
|
|
83
|
+
keys.forEach(([key, direction], position) => {
|
|
84
|
+
if (!index.has(key)) index.set(key, { direction,
|
|
85
|
+
position });
|
|
86
|
+
});
|
|
87
|
+
return index;
|
|
88
|
+
}, [keys]);
|
|
69
89
|
|
|
70
|
-
const applySort = useCallback((next?: [
|
|
71
|
-
setSortBy?.(next);
|
|
90
|
+
const applySort = useCallback((next?: OrderByTuple[]) => {
|
|
91
|
+
setSortBy?.((next && next.length > 0 ? next : undefined) as Parameters<NonNullable<typeof setSortBy>>[0]);
|
|
72
92
|
// Re-ordering a partially loaded collection is the same event as
|
|
73
93
|
// clicking a table header: the rows already fetched are no longer the
|
|
74
94
|
// ones the query answers with, so pagination starts over rather than
|
|
@@ -78,21 +98,41 @@ export function SortButton<M extends Record<string, unknown>>({
|
|
|
78
98
|
setItemCount?.(pageSize);
|
|
79
99
|
}, [setSortBy, setItemCount, pageSize]);
|
|
80
100
|
|
|
101
|
+
/** Flip an active key's direction, or append an inactive one ascending. */
|
|
81
102
|
const onOptionClick = useCallback((key: string) => {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
103
|
+
const active = activeIndex.get(key);
|
|
104
|
+
if (!active) {
|
|
105
|
+
applySort([...keys, [key, "asc"]]);
|
|
106
|
+
return;
|
|
86
107
|
}
|
|
87
|
-
|
|
108
|
+
applySort(keys.map(entry => entry[0] === key
|
|
109
|
+
? [key, active.direction === "asc" ? "desc" : "asc"] as OrderByTuple
|
|
110
|
+
: entry));
|
|
111
|
+
}, [activeIndex, applySort, keys]);
|
|
112
|
+
|
|
113
|
+
const removeKey = useCallback((key: string) => {
|
|
114
|
+
applySort(keys.filter(([existing]) => existing !== key));
|
|
115
|
+
}, [applySort, keys]);
|
|
116
|
+
|
|
117
|
+
/** Swap a key with its neighbour, which is what changes which one decides. */
|
|
118
|
+
const moveKey = useCallback((position: number, delta: -1 | 1) => {
|
|
119
|
+
const target = position + delta;
|
|
120
|
+
if (target < 0 || target >= keys.length) return;
|
|
121
|
+
const next = [...keys];
|
|
122
|
+
[next[position], next[target]] = [next[target], next[position]];
|
|
123
|
+
applySort(next);
|
|
124
|
+
}, [applySort, keys]);
|
|
88
125
|
|
|
89
126
|
if (!setSortBy || options.length === 0) {
|
|
90
127
|
return null;
|
|
91
128
|
}
|
|
92
129
|
|
|
93
|
-
const
|
|
94
|
-
const
|
|
95
|
-
|
|
130
|
+
const primary = keys[0];
|
|
131
|
+
const primaryOption = primary ? options.find(option => option.key === primary[0]) : undefined;
|
|
132
|
+
const label = primaryOption
|
|
133
|
+
? keys.length > 1
|
|
134
|
+
? `${t("sort_by")}: ${primaryOption.title} +${keys.length - 1}`
|
|
135
|
+
: `${t("sort_by")}: ${primaryOption.title}`
|
|
96
136
|
: t("sort");
|
|
97
137
|
|
|
98
138
|
// Icon only, at every width. The arrow says which way the order runs; which
|
|
@@ -105,25 +145,28 @@ export function SortButton<M extends Record<string, unknown>>({
|
|
|
105
145
|
// second time in the same click and the popover would never open; a
|
|
106
146
|
// `Tooltip` in between would swallow the trigger's ref.
|
|
107
147
|
const arrowSize = compact ? iconSize.smallest : iconSize.small;
|
|
148
|
+
const primaryDirection = primary?.[1];
|
|
108
149
|
|
|
109
150
|
const trigger = (
|
|
110
151
|
<IconButton
|
|
111
152
|
size={"small"}
|
|
112
153
|
aria-label={label}
|
|
113
154
|
title={label}
|
|
114
|
-
className={cls(
|
|
155
|
+
className={cls(primaryOption && "text-primary")}
|
|
115
156
|
>
|
|
116
157
|
{/* Same icon size as the filter controls it sits between: 16 in the
|
|
117
158
|
split view's narrow toolbar, 20 everywhere else. It was a flat
|
|
118
159
|
16, which read as a smaller button next to them. */}
|
|
119
|
-
{
|
|
160
|
+
{primaryDirection === "desc"
|
|
120
161
|
? <ArrowDownIcon size={arrowSize}/>
|
|
121
|
-
:
|
|
162
|
+
: primaryDirection === "asc"
|
|
122
163
|
? <ArrowUpIcon size={arrowSize}/>
|
|
123
164
|
: <ArrowUpDownIcon size={arrowSize}/>}
|
|
124
165
|
</IconButton>
|
|
125
166
|
);
|
|
126
167
|
|
|
168
|
+
const inactiveOptions = options.filter(option => !activeIndex.has(option.key));
|
|
169
|
+
|
|
127
170
|
return (
|
|
128
171
|
<Popover
|
|
129
172
|
open={open}
|
|
@@ -133,7 +176,7 @@ export function SortButton<M extends Record<string, unknown>>({
|
|
|
133
176
|
align="start"
|
|
134
177
|
trigger={trigger}
|
|
135
178
|
>
|
|
136
|
-
<div className="py-2 min-w-[
|
|
179
|
+
<div className="py-2 min-w-[280px] max-w-[360px] flex flex-col">
|
|
137
180
|
<Typography
|
|
138
181
|
variant="caption"
|
|
139
182
|
color="secondary"
|
|
@@ -142,43 +185,112 @@ export function SortButton<M extends Record<string, unknown>>({
|
|
|
142
185
|
{t("sort_by")}
|
|
143
186
|
</Typography>
|
|
144
187
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
188
|
+
{keys.length > 0 && (
|
|
189
|
+
<div className="flex flex-col">
|
|
190
|
+
{keys.map(([key, direction], position) => {
|
|
191
|
+
const option = options.find(o => o.key === key);
|
|
192
|
+
return (
|
|
193
|
+
<div
|
|
194
|
+
key={key}
|
|
195
|
+
className="flex items-center gap-1 pl-3 pr-2 py-1 text-sm text-primary"
|
|
196
|
+
>
|
|
197
|
+
{/* The rank, not decoration: it is the only
|
|
198
|
+
thing on screen saying which key wins. */}
|
|
199
|
+
<span className="flex-shrink-0 w-4 text-xs tabular-nums text-text-secondary dark:text-text-secondary-dark">
|
|
200
|
+
{position + 1}
|
|
201
|
+
</span>
|
|
202
|
+
<button
|
|
203
|
+
type="button"
|
|
204
|
+
onClick={() => onOptionClick(key)}
|
|
205
|
+
title={direction === "asc" ? t("sort_descending") : t("sort_ascending")}
|
|
206
|
+
className="flex items-center gap-2 flex-grow min-w-0 text-left cursor-pointer"
|
|
207
|
+
>
|
|
208
|
+
{option && (
|
|
209
|
+
<span className="flex-shrink-0 flex items-center">
|
|
210
|
+
{getIconForProperty(option.property, "smallest")}
|
|
211
|
+
</span>
|
|
212
|
+
)}
|
|
213
|
+
<span className="flex-grow truncate">{option?.title ?? key}</span>
|
|
214
|
+
<span className="flex-shrink-0 flex items-center">
|
|
215
|
+
{direction === "desc"
|
|
216
|
+
? <ArrowDownIcon size={iconSize.smallest}/>
|
|
217
|
+
: <ArrowUpIcon size={iconSize.smallest}/>}
|
|
218
|
+
</span>
|
|
219
|
+
</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>
|
|
250
|
+
</div>
|
|
251
|
+
);
|
|
252
|
+
})}
|
|
253
|
+
</div>
|
|
254
|
+
)}
|
|
255
|
+
|
|
256
|
+
{inactiveOptions.length > 0 && (
|
|
257
|
+
<Typography
|
|
258
|
+
variant="caption"
|
|
259
|
+
color="secondary"
|
|
260
|
+
className="font-medium uppercase tracking-wider px-3 pt-2 pb-1"
|
|
261
|
+
>
|
|
262
|
+
{keys.length > 0 ? t("sort_then_by") : ""}
|
|
263
|
+
</Typography>
|
|
264
|
+
)}
|
|
265
|
+
|
|
266
|
+
<div className="max-h-[280px] overflow-y-auto flex flex-col">
|
|
267
|
+
{inactiveOptions.map(({ key, title, property }) => {
|
|
148
268
|
// A driver may refuse a sort next to the filter that is
|
|
149
269
|
// already applied. Offering it would turn the click
|
|
150
270
|
// into a failed request, so it is disabled with the
|
|
151
271
|
// same authority the table headers ask.
|
|
152
272
|
const disabled = checkFilterCombination
|
|
153
|
-
? !checkFilterCombination(filterValues ?? {}, [key,
|
|
273
|
+
? !checkFilterCombination(filterValues ?? {}, [...keys, [key, "asc"]])
|
|
154
274
|
: false;
|
|
155
275
|
return (
|
|
156
276
|
<button
|
|
157
277
|
key={key}
|
|
158
278
|
type="button"
|
|
159
279
|
role="menuitemradio"
|
|
160
|
-
aria-checked={
|
|
280
|
+
aria-checked={false}
|
|
161
281
|
disabled={disabled}
|
|
162
282
|
onClick={() => onOptionClick(key)}
|
|
163
283
|
className={cls(
|
|
164
284
|
"flex items-center gap-2 px-3 py-1.5 text-sm text-left w-full",
|
|
165
285
|
disabled
|
|
166
286
|
? "opacity-50 cursor-not-allowed"
|
|
167
|
-
: "cursor-pointer hover:bg-surface-accent-100 dark:hover:bg-surface-accent-900"
|
|
168
|
-
active && "text-primary"
|
|
287
|
+
: "cursor-pointer hover:bg-surface-accent-100 dark:hover:bg-surface-accent-900"
|
|
169
288
|
)}
|
|
170
289
|
>
|
|
171
290
|
<span className="flex-shrink-0 flex items-center">
|
|
172
291
|
{getIconForProperty(property, "smallest")}
|
|
173
292
|
</span>
|
|
174
293
|
<span className="flex-grow truncate">{title}</span>
|
|
175
|
-
{active && (
|
|
176
|
-
<span className="flex-shrink-0 flex items-center">
|
|
177
|
-
{activeDirection === "desc"
|
|
178
|
-
? <ArrowDownIcon size={iconSize.smallest}/>
|
|
179
|
-
: <ArrowUpIcon size={iconSize.smallest}/>}
|
|
180
|
-
</span>
|
|
181
|
-
)}
|
|
182
294
|
</button>
|
|
183
295
|
);
|
|
184
296
|
})}
|
|
@@ -189,7 +301,7 @@ export function SortButton<M extends Record<string, unknown>>({
|
|
|
189
301
|
variant="text"
|
|
190
302
|
size="small"
|
|
191
303
|
className="w-full"
|
|
192
|
-
disabled={
|
|
304
|
+
disabled={keys.length === 0}
|
|
193
305
|
onClick={() => applySort(undefined)}
|
|
194
306
|
>
|
|
195
307
|
{t("clear_sort")}
|
|
@@ -284,7 +284,7 @@ export const SelectableTable = function SelectableTable<M extends Record<string,
|
|
|
284
284
|
filter={filterValues as any}
|
|
285
285
|
onFilterUpdate={setFilterValues ? onFilterUpdate : undefined}
|
|
286
286
|
sortBy={sortBy}
|
|
287
|
-
onSortByUpdate={setSortBy as ((sortBy?: [string, "asc" | "desc"]) => void)}
|
|
287
|
+
onSortByUpdate={setSortBy as ((sortBy?: [string, "asc" | "desc"][]) => void)}
|
|
288
288
|
hoverRow={hoverRow}
|
|
289
289
|
initialScroll={initialScroll}
|
|
290
290
|
onScroll={onScroll}
|