@byline/host-tanstack-start 1.10.3 → 1.11.0
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/admin-shell/chrome/th-sortable_module.css +6 -0
- package/dist/admin-shell/collections/list.d.ts +11 -1
- package/dist/admin-shell/collections/list.js +176 -5
- package/dist/admin-shell/collections/list.module.js +4 -0
- package/dist/admin-shell/collections/list_module.css +74 -0
- package/dist/routes/create-collection-list-route.js +13 -2
- package/dist/server-fns/collections/index.d.ts +1 -0
- package/dist/server-fns/collections/index.js +1 -0
- package/dist/server-fns/collections/list.js +9 -4
- package/dist/server-fns/collections/reorder.d.ts +21 -0
- package/dist/server-fns/collections/reorder.js +97 -0
- package/package.json +9 -7
- package/src/admin-shell/chrome/th-sortable.module.css +10 -0
- package/src/admin-shell/collections/list.module.css +72 -0
- package/src/admin-shell/collections/list.tsx +332 -70
- package/src/routes/create-collection-list-route.tsx +15 -1
- package/src/server-fns/collections/index.ts +1 -0
- package/src/server-fns/collections/list.ts +12 -1
- package/src/server-fns/collections/reorder.ts +166 -0
|
@@ -7,9 +7,19 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import type { ColumnDefinition, WorkflowStatus } from '@byline/core';
|
|
9
9
|
import type { AnyCollectionSchemaTypes } from '@byline/core/zod-schemas';
|
|
10
|
-
|
|
10
|
+
type ReorderFn = (params: {
|
|
11
|
+
documentId: string;
|
|
12
|
+
beforeDocumentId: string | null;
|
|
13
|
+
afterDocumentId: string | null;
|
|
14
|
+
}) => Promise<unknown>;
|
|
15
|
+
export declare const ListView: ({ data, columns, workflowStatuses, useAsTitle, orderable, onReorder, }: {
|
|
11
16
|
data: AnyCollectionSchemaTypes["ListType"];
|
|
12
17
|
columns: ColumnDefinition[];
|
|
13
18
|
workflowStatuses?: WorkflowStatus[];
|
|
14
19
|
useAsTitle?: string;
|
|
20
|
+
/** When true, render a drag handle column and enable drag-to-reorder. */
|
|
21
|
+
orderable?: boolean;
|
|
22
|
+
/** Persists a single-row reorder via the host's reorder server fn. */
|
|
23
|
+
onReorder?: ReorderFn;
|
|
15
24
|
}) => import("react").JSX.Element;
|
|
25
|
+
export {};
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useMemo, useState } from "react";
|
|
3
|
-
import { useRouterState } from "@tanstack/react-router";
|
|
4
|
-
import { Container, IconButton, LoaderRing, PlusIcon, Search, Section, Select, StatusBadge, Table, renderFormatted } from "@byline/ui/react";
|
|
2
|
+
import { useEffect, useMemo, useState } from "react";
|
|
3
|
+
import { useRouter, useRouterState } from "@tanstack/react-router";
|
|
4
|
+
import { Container, GripperVerticalIcon, IconButton, LoaderRing, PlusIcon, Search, Section, Select, StatusBadge, Table, renderFormatted, useToastManager } from "@byline/ui/react";
|
|
5
|
+
import { DndContext, KeyboardSensor, PointerSensor, useSensor, useSensors } from "@dnd-kit/core";
|
|
6
|
+
import { SortableContext, sortableKeyboardCoordinates, useSortable, verticalListSortingStrategy } from "@dnd-kit/sortable";
|
|
5
7
|
import classnames from "classnames";
|
|
6
8
|
import { Link, useNavigate } from "../chrome/loose-router.js";
|
|
7
9
|
import { RouterPager } from "../chrome/router-pager.js";
|
|
10
|
+
import { SortAscendingIcon } from "../chrome/sort-icons.js";
|
|
8
11
|
import { TableHeadingCellSortable } from "../chrome/th-sortable.js";
|
|
9
12
|
import { formatNumber } from "../chrome/utils.js";
|
|
10
13
|
import list_module from "./list.module.js";
|
|
@@ -32,11 +35,104 @@ function padRows(value) {
|
|
|
32
35
|
children: "\xa0"
|
|
33
36
|
}, `empty-row-${index}`));
|
|
34
37
|
}
|
|
35
|
-
|
|
38
|
+
function SortableTableRow({ id, disabled, children }) {
|
|
39
|
+
const { setNodeRef, attributes, listeners, transform, transition, isDragging } = useSortable({
|
|
40
|
+
id,
|
|
41
|
+
disabled
|
|
42
|
+
});
|
|
43
|
+
const style = {
|
|
44
|
+
transform: transform ? `translate3d(${transform.x}px, ${transform.y}px, 0)` : void 0,
|
|
45
|
+
transition,
|
|
46
|
+
position: 'relative',
|
|
47
|
+
zIndex: isDragging ? 10 : 'auto',
|
|
48
|
+
opacity: isDragging ? 0.6 : 1
|
|
49
|
+
};
|
|
50
|
+
return /*#__PURE__*/ jsxs("tr", {
|
|
51
|
+
ref: setNodeRef,
|
|
52
|
+
className: "byline-table-row",
|
|
53
|
+
style: style,
|
|
54
|
+
children: [
|
|
55
|
+
/*#__PURE__*/ jsx("td", {
|
|
56
|
+
className: classnames('byline-coll-list-drag-cell', list_module.dragCell),
|
|
57
|
+
children: /*#__PURE__*/ jsx("button", {
|
|
58
|
+
type: "button",
|
|
59
|
+
className: classnames('byline-coll-list-drag-handle', list_module.dragHandle),
|
|
60
|
+
"aria-label": disabled ? 'Drag disabled while filters or search are active' : 'Drag to reorder',
|
|
61
|
+
disabled: disabled,
|
|
62
|
+
...attributes,
|
|
63
|
+
...listeners,
|
|
64
|
+
children: /*#__PURE__*/ jsx(GripperVerticalIcon, {})
|
|
65
|
+
})
|
|
66
|
+
}),
|
|
67
|
+
children
|
|
68
|
+
]
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
const ListView = ({ data, columns, workflowStatuses, useAsTitle, orderable = false, onReorder })=>{
|
|
36
72
|
const navigate = useNavigate();
|
|
73
|
+
const router = useRouter();
|
|
74
|
+
const toastManager = useToastManager();
|
|
37
75
|
const location = useRouterState({
|
|
38
76
|
select: (s)=>s.location
|
|
39
77
|
});
|
|
78
|
+
const [localDocs, setLocalDocs] = useState(data.docs);
|
|
79
|
+
const [isReordering, setIsReordering] = useState(false);
|
|
80
|
+
useEffect(()=>{
|
|
81
|
+
if (!isReordering) setLocalDocs(data.docs);
|
|
82
|
+
}, [
|
|
83
|
+
data.docs,
|
|
84
|
+
isReordering
|
|
85
|
+
]);
|
|
86
|
+
const searchParams = location.search;
|
|
87
|
+
const isCanonicalView = !searchParams.order && !searchParams.desc && !searchParams.query && !searchParams.status;
|
|
88
|
+
const dragEnabled = orderable && isCanonicalView && !!onReorder;
|
|
89
|
+
const sensors = useSensors(useSensor(PointerSensor, {
|
|
90
|
+
activationConstraint: {
|
|
91
|
+
distance: 5
|
|
92
|
+
}
|
|
93
|
+
}), useSensor(KeyboardSensor, {
|
|
94
|
+
coordinateGetter: sortableKeyboardCoordinates
|
|
95
|
+
}));
|
|
96
|
+
const handleDragEnd = async (event)=>{
|
|
97
|
+
const { active, over } = event;
|
|
98
|
+
if (!over || active.id === over.id || !onReorder) return;
|
|
99
|
+
const oldIndex = localDocs.findIndex((d)=>d.id === active.id);
|
|
100
|
+
const newIndex = localDocs.findIndex((d)=>d.id === over.id);
|
|
101
|
+
if (oldIndex < 0 || newIndex < 0 || oldIndex === newIndex) return;
|
|
102
|
+
const previousDocs = localDocs;
|
|
103
|
+
const next = [
|
|
104
|
+
...localDocs
|
|
105
|
+
];
|
|
106
|
+
const [moved] = next.splice(oldIndex, 1);
|
|
107
|
+
if (!moved) return;
|
|
108
|
+
next.splice(newIndex, 0, moved);
|
|
109
|
+
setLocalDocs(next);
|
|
110
|
+
setIsReordering(true);
|
|
111
|
+
const before = next[newIndex - 1]?.id ?? null;
|
|
112
|
+
const after = next[newIndex + 1]?.id ?? null;
|
|
113
|
+
try {
|
|
114
|
+
await onReorder({
|
|
115
|
+
documentId: String(active.id),
|
|
116
|
+
beforeDocumentId: before,
|
|
117
|
+
afterDocumentId: after
|
|
118
|
+
});
|
|
119
|
+
await router.invalidate();
|
|
120
|
+
} catch (_err) {
|
|
121
|
+
setLocalDocs(previousDocs);
|
|
122
|
+
toastManager.add({
|
|
123
|
+
title: 'Could not save the new order',
|
|
124
|
+
description: 'Please try again.',
|
|
125
|
+
data: {
|
|
126
|
+
intent: 'danger',
|
|
127
|
+
iconType: 'danger',
|
|
128
|
+
icon: true,
|
|
129
|
+
close: true
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
} finally{
|
|
133
|
+
setIsReordering(false);
|
|
134
|
+
}
|
|
135
|
+
};
|
|
40
136
|
const statusItems = useMemo(()=>[
|
|
41
137
|
{
|
|
42
138
|
value: '_all',
|
|
@@ -162,7 +258,82 @@ const ListView = ({ data, columns, workflowStatuses, useAsTitle })=>{
|
|
|
162
258
|
/*#__PURE__*/ jsxs(Table.Container, {
|
|
163
259
|
className: classnames('byline-coll-list-table-wrap', list_module.tableWrap),
|
|
164
260
|
children: [
|
|
165
|
-
/*#__PURE__*/
|
|
261
|
+
orderable ? /*#__PURE__*/ jsx(DndContext, {
|
|
262
|
+
sensors: sensors,
|
|
263
|
+
onDragEnd: handleDragEnd,
|
|
264
|
+
children: /*#__PURE__*/ jsx(SortableContext, {
|
|
265
|
+
items: localDocs.map((d)=>d.id),
|
|
266
|
+
strategy: verticalListSortingStrategy,
|
|
267
|
+
children: /*#__PURE__*/ jsxs(Table, {
|
|
268
|
+
children: [
|
|
269
|
+
/*#__PURE__*/ jsx(Table.Header, {
|
|
270
|
+
children: /*#__PURE__*/ jsxs(Table.Row, {
|
|
271
|
+
children: [
|
|
272
|
+
/*#__PURE__*/ jsx("th", {
|
|
273
|
+
scope: "col",
|
|
274
|
+
className: classnames('byline-coll-list-drag-cell', list_module.dragCell),
|
|
275
|
+
children: /*#__PURE__*/ jsx("button", {
|
|
276
|
+
type: "button",
|
|
277
|
+
className: classnames('byline-coll-list-order-header', list_module.orderHeader, isCanonicalView && [
|
|
278
|
+
'byline-coll-list-order-header-active',
|
|
279
|
+
list_module.orderHeaderActive
|
|
280
|
+
]),
|
|
281
|
+
onClick: ()=>{
|
|
282
|
+
const params = structuredClone(location.search);
|
|
283
|
+
delete params.page;
|
|
284
|
+
delete params.order;
|
|
285
|
+
delete params.desc;
|
|
286
|
+
navigate({
|
|
287
|
+
to: location.pathname,
|
|
288
|
+
search: params
|
|
289
|
+
});
|
|
290
|
+
},
|
|
291
|
+
"aria-label": "Sort by manual order",
|
|
292
|
+
title: "Sort by manual order",
|
|
293
|
+
children: /*#__PURE__*/ jsx(SortAscendingIcon, {})
|
|
294
|
+
})
|
|
295
|
+
}),
|
|
296
|
+
columns.map((column)=>/*#__PURE__*/ jsx(TableHeadingCellSortable, {
|
|
297
|
+
fieldName: String(column.fieldName),
|
|
298
|
+
label: column.label,
|
|
299
|
+
sortable: column.sortable,
|
|
300
|
+
scope: "col",
|
|
301
|
+
align: column.align,
|
|
302
|
+
className: column.className
|
|
303
|
+
}, String(column.fieldName)))
|
|
304
|
+
]
|
|
305
|
+
})
|
|
306
|
+
}),
|
|
307
|
+
/*#__PURE__*/ jsx(Table.Body, {
|
|
308
|
+
children: localDocs.map((document)=>/*#__PURE__*/ jsx(SortableTableRow, {
|
|
309
|
+
id: document.id,
|
|
310
|
+
disabled: !dragEnabled,
|
|
311
|
+
children: columns.map((column)=>/*#__PURE__*/ jsx(Table.Cell, {
|
|
312
|
+
className: classnames({
|
|
313
|
+
'byline-coll-list-cell-right': 'right' === column.align,
|
|
314
|
+
[list_module.cellRight]: 'right' === column.align,
|
|
315
|
+
'byline-coll-list-cell-center': 'center' === column.align,
|
|
316
|
+
[list_module.cellCenter]: 'center' === column.align
|
|
317
|
+
}),
|
|
318
|
+
children: useAsTitle && column.fieldName === useAsTitle ? /*#__PURE__*/ jsx(Link, {
|
|
319
|
+
to: '/admin/collections/$collection/$id',
|
|
320
|
+
params: {
|
|
321
|
+
collection: data.included.collection.path,
|
|
322
|
+
id: document.id
|
|
323
|
+
},
|
|
324
|
+
children: column.formatter ? renderFormatted(getColumnValue(document, column.fieldName), document, column.formatter) : getColumnValue(document, column.fieldName) ?? '------'
|
|
325
|
+
}) : column.formatter ? renderFormatted(getColumnValue(document, column.fieldName), document, column.formatter) : 'status' === column.fieldName && workflowStatuses ? /*#__PURE__*/ jsx(StatusBadge, {
|
|
326
|
+
status: document.status,
|
|
327
|
+
workflowStatuses: workflowStatuses,
|
|
328
|
+
hasPublishedVersion: document.hasPublishedVersion
|
|
329
|
+
}) : String(getColumnValue(document, column.fieldName) ?? '')
|
|
330
|
+
}, String(column.fieldName)))
|
|
331
|
+
}, document.id))
|
|
332
|
+
})
|
|
333
|
+
]
|
|
334
|
+
})
|
|
335
|
+
})
|
|
336
|
+
}) : /*#__PURE__*/ jsxs(Table, {
|
|
166
337
|
children: [
|
|
167
338
|
/*#__PURE__*/ jsx(Table.Header, {
|
|
168
339
|
children: /*#__PURE__*/ jsx(Table.Row, {
|
|
@@ -11,6 +11,10 @@ const list_module = {
|
|
|
11
11
|
cellRight: "cellRight-KPlIvX",
|
|
12
12
|
cellCenter: "cellCenter-NlTSyy",
|
|
13
13
|
padRow: "padRow-mCWMHC",
|
|
14
|
+
dragCell: "dragCell-o9UFuB",
|
|
15
|
+
dragHandle: "dragHandle-kAIpKJ",
|
|
16
|
+
orderHeader: "orderHeader-wsz9WO",
|
|
17
|
+
orderHeaderActive: "orderHeaderActive-VUBEa8",
|
|
14
18
|
pageSize: "pageSize-ojmZKR"
|
|
15
19
|
};
|
|
16
20
|
export default list_module;
|
|
@@ -80,6 +80,80 @@
|
|
|
80
80
|
height: 32px;
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
:is(.dragCell-o9UFuB, .byline-coll-list-drag-cell) {
|
|
84
|
+
vertical-align: middle;
|
|
85
|
+
white-space: nowrap;
|
|
86
|
+
width: 1%;
|
|
87
|
+
padding: 0 .5rem 0 .25rem;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
:is(.dragHandle-kAIpKJ, .byline-coll-list-drag-handle) {
|
|
91
|
+
width: 20px;
|
|
92
|
+
height: 20px;
|
|
93
|
+
color: inherit;
|
|
94
|
+
cursor: grab;
|
|
95
|
+
opacity: .45;
|
|
96
|
+
touch-action: none;
|
|
97
|
+
background: none;
|
|
98
|
+
border: 0;
|
|
99
|
+
justify-content: center;
|
|
100
|
+
align-items: center;
|
|
101
|
+
padding: 0;
|
|
102
|
+
display: inline-flex;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.dragHandle-kAIpKJ:hover {
|
|
106
|
+
opacity: 1;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.byline-coll-list-drag-handle:hover {
|
|
110
|
+
opacity: 1;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.dragHandle-kAIpKJ:disabled {
|
|
114
|
+
cursor: not-allowed;
|
|
115
|
+
opacity: .2;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.byline-coll-list-drag-handle:disabled {
|
|
119
|
+
cursor: not-allowed;
|
|
120
|
+
opacity: .2;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.dragHandle-kAIpKJ:active {
|
|
124
|
+
cursor: grabbing;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.byline-coll-list-drag-handle:active {
|
|
128
|
+
cursor: grabbing;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
:is(.orderHeader-wsz9WO, .byline-coll-list-order-header) {
|
|
132
|
+
width: 20px;
|
|
133
|
+
height: 20px;
|
|
134
|
+
color: inherit;
|
|
135
|
+
cursor: pointer;
|
|
136
|
+
opacity: .45;
|
|
137
|
+
background: none;
|
|
138
|
+
border: 0;
|
|
139
|
+
justify-content: center;
|
|
140
|
+
align-items: center;
|
|
141
|
+
padding: 0;
|
|
142
|
+
display: inline-flex;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.orderHeader-wsz9WO:hover {
|
|
146
|
+
opacity: .85;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.byline-coll-list-order-header:hover {
|
|
150
|
+
opacity: .85;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
:is(.orderHeaderActive-VUBEa8, .byline-coll-list-order-header-active) {
|
|
154
|
+
opacity: 1;
|
|
155
|
+
}
|
|
156
|
+
|
|
83
157
|
@media (min-width: 40rem) {
|
|
84
158
|
:is(.pageSize-ojmZKR, .byline-coll-list-page-size) {
|
|
85
159
|
margin-left: auto;
|
|
@@ -7,7 +7,7 @@ import { z } from "zod";
|
|
|
7
7
|
import { BreadcrumbsClient } from "../admin-shell/chrome/breadcrumbs/breadcrumbs-client.js";
|
|
8
8
|
import { useNavigate } from "../admin-shell/chrome/loose-router.js";
|
|
9
9
|
import { ListView } from "../admin-shell/collections/list.js";
|
|
10
|
-
import { getCollectionDocuments } from "../server-fns/collections/index.js";
|
|
10
|
+
import { getCollectionDocuments, reorderCollectionDocument } from "../server-fns/collections/index.js";
|
|
11
11
|
const searchSchema = z.object({
|
|
12
12
|
page: z.coerce.number().min(1).optional(),
|
|
13
13
|
page_size: z.coerce.number().max(100).optional(),
|
|
@@ -119,7 +119,18 @@ function createCollectionListRoute(path) {
|
|
|
119
119
|
data: data,
|
|
120
120
|
columns: columns,
|
|
121
121
|
workflowStatuses: workflowStatuses,
|
|
122
|
-
useAsTitle: collectionDef.useAsTitle
|
|
122
|
+
useAsTitle: collectionDef.useAsTitle,
|
|
123
|
+
orderable: adminConfig?.orderable === true,
|
|
124
|
+
onReorder: async ({ documentId, beforeDocumentId, afterDocumentId })=>{
|
|
125
|
+
await reorderCollectionDocument({
|
|
126
|
+
data: {
|
|
127
|
+
collection,
|
|
128
|
+
documentId,
|
|
129
|
+
beforeDocumentId,
|
|
130
|
+
afterDocumentId
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
}
|
|
123
134
|
})
|
|
124
135
|
]
|
|
125
136
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createServerFn } from "@tanstack/react-start";
|
|
2
|
-
import { ERR_NOT_FOUND, getCollectionSchemasForPath, getLogger, getServerConfig } from "@byline/core";
|
|
2
|
+
import { ERR_NOT_FOUND, getCollectionAdminConfig, getCollectionSchemasForPath, getLogger, getServerConfig } from "@byline/core";
|
|
3
3
|
import { ensureCollection } from "../../integrations/api-utils.js";
|
|
4
4
|
import { getAdminBylineClient } from "../../integrations/byline-client.js";
|
|
5
5
|
import { serialise } from "./utils.js";
|
|
@@ -20,11 +20,16 @@ const getCollectionDocuments = createServerFn({
|
|
|
20
20
|
const where = {};
|
|
21
21
|
if (params.status) where.status = params.status;
|
|
22
22
|
if (params.query) where.query = params.query;
|
|
23
|
+
const adminConfig = getCollectionAdminConfig(path);
|
|
24
|
+
const defaultSort = adminConfig?.orderable === true ? {
|
|
25
|
+
order_key: 'asc'
|
|
26
|
+
} : void 0;
|
|
27
|
+
const sortSpec = params.order ? {
|
|
28
|
+
[params.order]: false === params.desc ? 'asc' : 'desc'
|
|
29
|
+
} : defaultSort;
|
|
23
30
|
const result = await handle.find({
|
|
24
31
|
where: Object.keys(where).length > 0 ? where : void 0,
|
|
25
|
-
sort:
|
|
26
|
-
[params.order]: false === params.desc ? 'asc' : 'desc'
|
|
27
|
-
} : void 0,
|
|
32
|
+
sort: sortSpec,
|
|
28
33
|
locale: params.locale ?? 'en',
|
|
29
34
|
page: params.page,
|
|
30
35
|
pageSize,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This Source Code is subject to the terms of the Mozilla Public
|
|
3
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
5
|
+
*
|
|
6
|
+
* Copyright (c) Infonomic Company Limited
|
|
7
|
+
*/
|
|
8
|
+
export declare const reorderCollectionDocument: import("@tanstack/react-start").RequiredFetcher<undefined, (input: {
|
|
9
|
+
collection: string;
|
|
10
|
+
documentId: string;
|
|
11
|
+
beforeDocumentId?: string | null;
|
|
12
|
+
afterDocumentId?: string | null;
|
|
13
|
+
}) => {
|
|
14
|
+
collection: string;
|
|
15
|
+
documentId: string;
|
|
16
|
+
beforeDocumentId?: string | null;
|
|
17
|
+
afterDocumentId?: string | null;
|
|
18
|
+
}, Promise<{
|
|
19
|
+
status: "ok";
|
|
20
|
+
orderKey: string;
|
|
21
|
+
}>>;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { createServerFn } from "@tanstack/react-start";
|
|
2
|
+
import { ERR_NOT_FOUND, ERR_VALIDATION, assertActorCanPerform, generateKeyBetween, generateNKeysBetween, getCollectionAdminConfig, getLogger, getServerConfig } from "@byline/core";
|
|
3
|
+
import { getAdminRequestContext } from "../../auth/auth-context.js";
|
|
4
|
+
import { ensureCollection } from "../../integrations/api-utils.js";
|
|
5
|
+
const reorderCollectionDocument = createServerFn({
|
|
6
|
+
method: 'POST'
|
|
7
|
+
}).inputValidator((input)=>input).handler(async ({ data: input })=>{
|
|
8
|
+
const { collection: path, documentId, beforeDocumentId, afterDocumentId } = input;
|
|
9
|
+
const logger = getLogger();
|
|
10
|
+
const config = await ensureCollection(path);
|
|
11
|
+
if (!config) throw ERR_NOT_FOUND({
|
|
12
|
+
message: 'Collection not found',
|
|
13
|
+
details: {
|
|
14
|
+
collectionPath: path
|
|
15
|
+
}
|
|
16
|
+
}).log(logger);
|
|
17
|
+
const adminConfig = getCollectionAdminConfig(path);
|
|
18
|
+
if (adminConfig?.orderable !== true) throw ERR_VALIDATION({
|
|
19
|
+
message: `collection '${path}' is not orderable; set \`orderable: true\` on its admin config to enable reordering`,
|
|
20
|
+
details: {
|
|
21
|
+
collectionPath: path
|
|
22
|
+
}
|
|
23
|
+
}).log(logger);
|
|
24
|
+
const requestContext = await getAdminRequestContext();
|
|
25
|
+
assertActorCanPerform(requestContext, path, 'update');
|
|
26
|
+
const serverConfig = getServerConfig();
|
|
27
|
+
const collectionId = config.collection.id;
|
|
28
|
+
const canonical = await serverConfig.db.queries.documents.getCanonicalDocumentOrder({
|
|
29
|
+
collection_id: collectionId
|
|
30
|
+
});
|
|
31
|
+
let corrupted = false;
|
|
32
|
+
{
|
|
33
|
+
const seen = new Set();
|
|
34
|
+
let lastKey = null;
|
|
35
|
+
for (const doc of canonical)if (null != doc.order_key) {
|
|
36
|
+
if (seen.has(doc.order_key)) {
|
|
37
|
+
corrupted = true;
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
if (null != lastKey && doc.order_key <= lastKey) {
|
|
41
|
+
corrupted = true;
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
seen.add(doc.order_key);
|
|
45
|
+
lastKey = doc.order_key;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (corrupted) {
|
|
49
|
+
const allKeys = generateNKeysBetween(null, null, canonical.length);
|
|
50
|
+
for(let i = 0; i < canonical.length; i++)await serverConfig.db.commands.documents.setOrderKey({
|
|
51
|
+
document_id: canonical[i]?.id,
|
|
52
|
+
order_key: allKeys[i]
|
|
53
|
+
});
|
|
54
|
+
} else {
|
|
55
|
+
const firstNullIdx = canonical.findIndex((d)=>null == d.order_key);
|
|
56
|
+
if (-1 !== firstNullIdx) {
|
|
57
|
+
const nullDocs = canonical.slice(firstNullIdx);
|
|
58
|
+
const lastExistingKey = 0 === firstNullIdx ? null : canonical[firstNullIdx - 1]?.order_key;
|
|
59
|
+
const newKeys = generateNKeysBetween(lastExistingKey, null, nullDocs.length);
|
|
60
|
+
for(let i = 0; i < nullDocs.length; i++)await serverConfig.db.commands.documents.setOrderKey({
|
|
61
|
+
document_id: nullDocs[i]?.id,
|
|
62
|
+
order_key: newKeys[i]
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const { left, right } = await serverConfig.db.queries.documents.getNeighborOrderKeys({
|
|
67
|
+
collection_id: collectionId,
|
|
68
|
+
before_document_id: beforeDocumentId ?? null,
|
|
69
|
+
after_document_id: afterDocumentId ?? null
|
|
70
|
+
});
|
|
71
|
+
let newKey;
|
|
72
|
+
try {
|
|
73
|
+
newKey = generateKeyBetween(left, right);
|
|
74
|
+
} catch (err) {
|
|
75
|
+
throw ERR_VALIDATION({
|
|
76
|
+
message: 'cannot generate order_key between supplied neighbors',
|
|
77
|
+
details: {
|
|
78
|
+
collectionPath: path,
|
|
79
|
+
documentId,
|
|
80
|
+
beforeDocumentId,
|
|
81
|
+
afterDocumentId,
|
|
82
|
+
left,
|
|
83
|
+
right,
|
|
84
|
+
cause: err instanceof Error ? err.message : String(err)
|
|
85
|
+
}
|
|
86
|
+
}).log(logger);
|
|
87
|
+
}
|
|
88
|
+
await serverConfig.db.commands.documents.setOrderKey({
|
|
89
|
+
document_id: documentId,
|
|
90
|
+
order_key: newKey
|
|
91
|
+
});
|
|
92
|
+
return {
|
|
93
|
+
status: 'ok',
|
|
94
|
+
orderKey: newKey
|
|
95
|
+
};
|
|
96
|
+
});
|
|
97
|
+
export { reorderCollectionDocument };
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"private": false,
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MPL-2.0",
|
|
6
|
-
"version": "1.
|
|
6
|
+
"version": "1.11.0",
|
|
7
7
|
"engines": {
|
|
8
8
|
"node": ">=20.9.0"
|
|
9
9
|
},
|
|
@@ -99,18 +99,20 @@
|
|
|
99
99
|
}
|
|
100
100
|
},
|
|
101
101
|
"dependencies": {
|
|
102
|
+
"@dnd-kit/core": "^6.3.1",
|
|
103
|
+
"@dnd-kit/sortable": "^10.0.0",
|
|
102
104
|
"classnames": "^2.5.1",
|
|
103
105
|
"npm-run-all": "^4.1.5",
|
|
104
106
|
"react-json-view-lite": "^2.5.0",
|
|
105
107
|
"react-swipeable": "^7.0.2",
|
|
106
108
|
"uuid": "^14.0.0",
|
|
107
109
|
"zod": "^4.4.3",
|
|
108
|
-
"@byline/admin": "1.
|
|
109
|
-
"@byline/auth": "1.
|
|
110
|
-
"@byline/ai": "1.
|
|
111
|
-
"@byline/client": "1.
|
|
112
|
-
"@byline/core": "1.
|
|
113
|
-
"@byline/ui": "1.
|
|
110
|
+
"@byline/admin": "1.11.0",
|
|
111
|
+
"@byline/auth": "1.11.0",
|
|
112
|
+
"@byline/ai": "1.11.0",
|
|
113
|
+
"@byline/client": "1.11.0",
|
|
114
|
+
"@byline/core": "1.11.0",
|
|
115
|
+
"@byline/ui": "1.11.0"
|
|
114
116
|
},
|
|
115
117
|
"peerDependencies": {
|
|
116
118
|
"@tanstack/react-router": "^1.167.0",
|
|
@@ -49,3 +49,13 @@
|
|
|
49
49
|
width: 1.5rem;
|
|
50
50
|
height: 1.5rem;
|
|
51
51
|
}
|
|
52
|
+
|
|
53
|
+
/* Sort indicator SVGs are rendered with class `byline-sort-icon` (see
|
|
54
|
+
sort-icons.tsx) inside a flex container; without explicit dimensions
|
|
55
|
+
they collapse to zero. Size them here so every consumer of the icons
|
|
56
|
+
picks the rule up automatically. */
|
|
57
|
+
:global(.byline-sort-icon) {
|
|
58
|
+
flex-shrink: 0;
|
|
59
|
+
width: 1.25rem;
|
|
60
|
+
height: 1.25rem;
|
|
61
|
+
}
|
|
@@ -111,6 +111,78 @@
|
|
|
111
111
|
border: 0;
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
+
.dragCell,
|
|
115
|
+
:global(.byline-coll-list-drag-cell) {
|
|
116
|
+
/* "Shrink to content" — table layout distributes the row width across cells
|
|
117
|
+
without an explicit width; setting 1% with nowrap forces this column to
|
|
118
|
+
take only the space its inner button needs. */
|
|
119
|
+
width: 1%;
|
|
120
|
+
padding: 0 0.5rem 0 0.25rem;
|
|
121
|
+
vertical-align: middle;
|
|
122
|
+
white-space: nowrap;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.dragHandle,
|
|
126
|
+
:global(.byline-coll-list-drag-handle) {
|
|
127
|
+
display: inline-flex;
|
|
128
|
+
align-items: center;
|
|
129
|
+
justify-content: center;
|
|
130
|
+
width: 20px;
|
|
131
|
+
height: 20px;
|
|
132
|
+
padding: 0;
|
|
133
|
+
border: 0;
|
|
134
|
+
background: transparent;
|
|
135
|
+
color: inherit;
|
|
136
|
+
cursor: grab;
|
|
137
|
+
opacity: 0.45;
|
|
138
|
+
touch-action: none;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.dragHandle:hover,
|
|
142
|
+
:global(.byline-coll-list-drag-handle:hover) {
|
|
143
|
+
opacity: 1;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.dragHandle:disabled,
|
|
147
|
+
:global(.byline-coll-list-drag-handle:disabled) {
|
|
148
|
+
cursor: not-allowed;
|
|
149
|
+
opacity: 0.2;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.dragHandle:active,
|
|
153
|
+
:global(.byline-coll-list-drag-handle:active) {
|
|
154
|
+
cursor: grabbing;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/* Column-header button in the drag column. Clicking returns to canonical
|
|
158
|
+
`order_key` sort. Active when the current view is canonical so the user
|
|
159
|
+
sees the affordance is "selected" — matches the visual semantics of
|
|
160
|
+
the sortable column headers next to it. */
|
|
161
|
+
.orderHeader,
|
|
162
|
+
:global(.byline-coll-list-order-header) {
|
|
163
|
+
display: inline-flex;
|
|
164
|
+
align-items: center;
|
|
165
|
+
justify-content: center;
|
|
166
|
+
width: 20px;
|
|
167
|
+
height: 20px;
|
|
168
|
+
padding: 0;
|
|
169
|
+
border: 0;
|
|
170
|
+
background: transparent;
|
|
171
|
+
color: inherit;
|
|
172
|
+
cursor: pointer;
|
|
173
|
+
opacity: 0.45;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.orderHeader:hover,
|
|
177
|
+
:global(.byline-coll-list-order-header:hover) {
|
|
178
|
+
opacity: 0.85;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
.orderHeaderActive,
|
|
182
|
+
:global(.byline-coll-list-order-header-active) {
|
|
183
|
+
opacity: 1;
|
|
184
|
+
}
|
|
185
|
+
|
|
114
186
|
.pageSize,
|
|
115
187
|
:global(.byline-coll-list-page-size) {
|
|
116
188
|
/* fallback */
|