@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
|
@@ -6,13 +6,14 @@
|
|
|
6
6
|
* Copyright (c) Infonomic Company Limited
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { useMemo, useState } from 'react'
|
|
10
|
-
import { useRouterState } from '@tanstack/react-router'
|
|
9
|
+
import { useEffect, useMemo, useState } from 'react'
|
|
10
|
+
import { useRouter, useRouterState } from '@tanstack/react-router'
|
|
11
11
|
|
|
12
12
|
import type { ColumnDefinition, WorkflowStatus } from '@byline/core'
|
|
13
13
|
import type { AnyCollectionSchemaTypes } from '@byline/core/zod-schemas'
|
|
14
14
|
import {
|
|
15
15
|
Container,
|
|
16
|
+
GripperVerticalIcon,
|
|
16
17
|
IconButton,
|
|
17
18
|
LoaderRing,
|
|
18
19
|
PlusIcon,
|
|
@@ -22,15 +23,37 @@ import {
|
|
|
22
23
|
Select,
|
|
23
24
|
StatusBadge,
|
|
24
25
|
Table,
|
|
26
|
+
useToastManager,
|
|
25
27
|
} from '@byline/ui/react'
|
|
28
|
+
import {
|
|
29
|
+
DndContext,
|
|
30
|
+
type DragEndEvent,
|
|
31
|
+
KeyboardSensor,
|
|
32
|
+
PointerSensor,
|
|
33
|
+
useSensor,
|
|
34
|
+
useSensors,
|
|
35
|
+
} from '@dnd-kit/core'
|
|
36
|
+
import {
|
|
37
|
+
SortableContext,
|
|
38
|
+
sortableKeyboardCoordinates,
|
|
39
|
+
useSortable,
|
|
40
|
+
verticalListSortingStrategy,
|
|
41
|
+
} from '@dnd-kit/sortable'
|
|
26
42
|
import cx from 'classnames'
|
|
27
43
|
|
|
28
44
|
import { Link, useNavigate } from '../chrome/loose-router.js'
|
|
29
45
|
import { RouterPager } from '../chrome/router-pager.js'
|
|
46
|
+
import { SortAscendingIcon } from '../chrome/sort-icons.js'
|
|
30
47
|
import { TableHeadingCellSortable } from '../chrome/th-sortable.js'
|
|
31
48
|
import { formatNumber } from '../chrome/utils.js'
|
|
32
49
|
import styles from './list.module.css'
|
|
33
50
|
|
|
51
|
+
type ReorderFn = (params: {
|
|
52
|
+
documentId: string
|
|
53
|
+
beforeDocumentId: string | null
|
|
54
|
+
afterDocumentId: string | null
|
|
55
|
+
}) => Promise<unknown>
|
|
56
|
+
|
|
34
57
|
/**
|
|
35
58
|
* Resolve a column value from a document, checking `fields` first (user-defined
|
|
36
59
|
* collection fields) then the root (metadata like status, updated_at).
|
|
@@ -76,20 +99,145 @@ function padRows(value: number) {
|
|
|
76
99
|
))
|
|
77
100
|
}
|
|
78
101
|
|
|
102
|
+
/**
|
|
103
|
+
* One `<tr>` participating in dnd-kit's vertical-list sort. Renders the row
|
|
104
|
+
* directly (not via `Table.Row`) because the row needs a function `ref`
|
|
105
|
+
* callback for dnd-kit; `Table.Row`'s typed `RefObject` prop wouldn't
|
|
106
|
+
* accept that.
|
|
107
|
+
*/
|
|
108
|
+
function SortableTableRow({
|
|
109
|
+
id,
|
|
110
|
+
disabled,
|
|
111
|
+
children,
|
|
112
|
+
}: {
|
|
113
|
+
id: string
|
|
114
|
+
disabled: boolean
|
|
115
|
+
children: React.ReactNode
|
|
116
|
+
}) {
|
|
117
|
+
const { setNodeRef, attributes, listeners, transform, transition, isDragging } = useSortable({
|
|
118
|
+
id,
|
|
119
|
+
disabled,
|
|
120
|
+
})
|
|
121
|
+
const style: React.CSSProperties = {
|
|
122
|
+
transform: transform ? `translate3d(${transform.x}px, ${transform.y}px, 0)` : undefined,
|
|
123
|
+
transition,
|
|
124
|
+
position: 'relative',
|
|
125
|
+
zIndex: isDragging ? 10 : 'auto',
|
|
126
|
+
opacity: isDragging ? 0.6 : 1,
|
|
127
|
+
}
|
|
128
|
+
return (
|
|
129
|
+
<tr ref={setNodeRef} className="byline-table-row" style={style}>
|
|
130
|
+
<td className={cx('byline-coll-list-drag-cell', styles.dragCell)}>
|
|
131
|
+
<button
|
|
132
|
+
type="button"
|
|
133
|
+
className={cx('byline-coll-list-drag-handle', styles.dragHandle)}
|
|
134
|
+
aria-label={
|
|
135
|
+
disabled ? 'Drag disabled while filters or search are active' : 'Drag to reorder'
|
|
136
|
+
}
|
|
137
|
+
disabled={disabled}
|
|
138
|
+
{...attributes}
|
|
139
|
+
{...listeners}
|
|
140
|
+
>
|
|
141
|
+
<GripperVerticalIcon />
|
|
142
|
+
</button>
|
|
143
|
+
</td>
|
|
144
|
+
{children}
|
|
145
|
+
</tr>
|
|
146
|
+
)
|
|
147
|
+
}
|
|
148
|
+
|
|
79
149
|
export const ListView = ({
|
|
80
150
|
data,
|
|
81
151
|
columns,
|
|
82
152
|
workflowStatuses,
|
|
83
153
|
useAsTitle,
|
|
154
|
+
orderable = false,
|
|
155
|
+
onReorder,
|
|
84
156
|
}: {
|
|
85
157
|
data: AnyCollectionSchemaTypes['ListType']
|
|
86
158
|
columns: ColumnDefinition[]
|
|
87
159
|
workflowStatuses?: WorkflowStatus[]
|
|
88
160
|
useAsTitle?: string
|
|
161
|
+
/** When true, render a drag handle column and enable drag-to-reorder. */
|
|
162
|
+
orderable?: boolean
|
|
163
|
+
/** Persists a single-row reorder via the host's reorder server fn. */
|
|
164
|
+
onReorder?: ReorderFn
|
|
89
165
|
}) => {
|
|
90
166
|
const navigate = useNavigate()
|
|
167
|
+
const router = useRouter()
|
|
168
|
+
const toastManager = useToastManager()
|
|
91
169
|
const location = useRouterState({ select: (s) => s.location })
|
|
92
170
|
|
|
171
|
+
// Local mirror of the loader docs so drag-and-drop can paint the new
|
|
172
|
+
// order optimistically before the server roundtrip completes. We resync
|
|
173
|
+
// from `data.docs` whenever fresh loader data arrives (after a
|
|
174
|
+
// `router.invalidate()`), unless a reorder is mid-flight — clobbering
|
|
175
|
+
// an in-flight optimistic state would flash the row back to its old
|
|
176
|
+
// position. See the dnd-kit + admin-roles list pattern for the same
|
|
177
|
+
// ordering invariant.
|
|
178
|
+
const [localDocs, setLocalDocs] = useState(data.docs)
|
|
179
|
+
const [isReordering, setIsReordering] = useState(false)
|
|
180
|
+
useEffect(() => {
|
|
181
|
+
if (!isReordering) {
|
|
182
|
+
setLocalDocs(data.docs)
|
|
183
|
+
}
|
|
184
|
+
}, [data.docs, isReordering])
|
|
185
|
+
|
|
186
|
+
// Drag is only meaningful in the canonical view: the default order_key
|
|
187
|
+
// sort, no search, no status filter. Otherwise the visible order isn't
|
|
188
|
+
// the stored order and "drop between A and B" maps onto the wrong
|
|
189
|
+
// neighbour ids.
|
|
190
|
+
const searchParams = location.search as {
|
|
191
|
+
order?: string
|
|
192
|
+
desc?: boolean
|
|
193
|
+
query?: string
|
|
194
|
+
status?: string
|
|
195
|
+
}
|
|
196
|
+
const isCanonicalView =
|
|
197
|
+
!searchParams.order && !searchParams.desc && !searchParams.query && !searchParams.status
|
|
198
|
+
const dragEnabled = orderable && isCanonicalView && !!onReorder
|
|
199
|
+
|
|
200
|
+
const sensors = useSensors(
|
|
201
|
+
useSensor(PointerSensor, { activationConstraint: { distance: 5 } }),
|
|
202
|
+
useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates })
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
const handleDragEnd = async (event: DragEndEvent) => {
|
|
206
|
+
const { active, over } = event
|
|
207
|
+
if (!over || active.id === over.id || !onReorder) return
|
|
208
|
+
const oldIndex = localDocs.findIndex((d) => d.id === active.id)
|
|
209
|
+
const newIndex = localDocs.findIndex((d) => d.id === over.id)
|
|
210
|
+
if (oldIndex < 0 || newIndex < 0 || oldIndex === newIndex) return
|
|
211
|
+
|
|
212
|
+
const previousDocs = localDocs
|
|
213
|
+
const next = [...localDocs]
|
|
214
|
+
const [moved] = next.splice(oldIndex, 1)
|
|
215
|
+
if (!moved) return
|
|
216
|
+
next.splice(newIndex, 0, moved)
|
|
217
|
+
setLocalDocs(next)
|
|
218
|
+
setIsReordering(true)
|
|
219
|
+
|
|
220
|
+
const before = next[newIndex - 1]?.id ?? null
|
|
221
|
+
const after = next[newIndex + 1]?.id ?? null
|
|
222
|
+
try {
|
|
223
|
+
await onReorder({
|
|
224
|
+
documentId: String(active.id),
|
|
225
|
+
beforeDocumentId: before,
|
|
226
|
+
afterDocumentId: after,
|
|
227
|
+
})
|
|
228
|
+
await router.invalidate()
|
|
229
|
+
} catch (_err) {
|
|
230
|
+
setLocalDocs(previousDocs)
|
|
231
|
+
toastManager.add({
|
|
232
|
+
title: 'Could not save the new order',
|
|
233
|
+
description: 'Please try again.',
|
|
234
|
+
data: { intent: 'danger', iconType: 'danger', icon: true, close: true },
|
|
235
|
+
})
|
|
236
|
+
} finally {
|
|
237
|
+
setIsReordering(false)
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
93
241
|
// Memoized so Base UI's SelectRoot doesn't see a fresh items identity on
|
|
94
242
|
// every render — a non-stable items array combined with a controlled value
|
|
95
243
|
// trips an internal store sync loop (manifests as "Maximum update depth
|
|
@@ -204,77 +352,191 @@ export const ListView = ({
|
|
|
204
352
|
/>
|
|
205
353
|
</div>
|
|
206
354
|
<Table.Container className={cx('byline-coll-list-table-wrap', styles.tableWrap)}>
|
|
207
|
-
|
|
208
|
-
<
|
|
209
|
-
<
|
|
210
|
-
{
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
355
|
+
{orderable ? (
|
|
356
|
+
<DndContext sensors={sensors} onDragEnd={handleDragEnd}>
|
|
357
|
+
<SortableContext
|
|
358
|
+
items={localDocs.map((d) => d.id)}
|
|
359
|
+
strategy={verticalListSortingStrategy}
|
|
360
|
+
>
|
|
361
|
+
<Table>
|
|
362
|
+
<Table.Header>
|
|
363
|
+
<Table.Row>
|
|
364
|
+
<th scope="col" className={cx('byline-coll-list-drag-cell', styles.dragCell)}>
|
|
365
|
+
<button
|
|
366
|
+
type="button"
|
|
367
|
+
className={cx(
|
|
368
|
+
'byline-coll-list-order-header',
|
|
369
|
+
styles.orderHeader,
|
|
370
|
+
isCanonicalView && [
|
|
371
|
+
'byline-coll-list-order-header-active',
|
|
372
|
+
styles.orderHeaderActive,
|
|
373
|
+
]
|
|
374
|
+
)}
|
|
375
|
+
onClick={() => {
|
|
376
|
+
// Clear any non-canonical sort and search-related
|
|
377
|
+
// state so the visible order matches what drag
|
|
378
|
+
// operations will mutate. Default sort then falls
|
|
379
|
+
// back to `order_key asc` server-side.
|
|
380
|
+
const params = structuredClone(
|
|
381
|
+
location.search as Record<string, unknown>
|
|
382
|
+
)
|
|
383
|
+
delete params.page
|
|
384
|
+
delete params.order
|
|
385
|
+
delete params.desc
|
|
386
|
+
navigate({
|
|
387
|
+
to: location.pathname as never,
|
|
388
|
+
search: params,
|
|
389
|
+
})
|
|
390
|
+
}}
|
|
391
|
+
aria-label="Sort by manual order"
|
|
392
|
+
title="Sort by manual order"
|
|
393
|
+
>
|
|
394
|
+
<SortAscendingIcon />
|
|
395
|
+
</button>
|
|
396
|
+
</th>
|
|
397
|
+
{columns.map((column) => {
|
|
398
|
+
return (
|
|
399
|
+
<TableHeadingCellSortable
|
|
400
|
+
key={String(column.fieldName)}
|
|
401
|
+
fieldName={String(column.fieldName)}
|
|
402
|
+
label={column.label}
|
|
403
|
+
sortable={column.sortable}
|
|
404
|
+
scope="col"
|
|
405
|
+
align={column.align}
|
|
406
|
+
className={column.className}
|
|
407
|
+
/>
|
|
408
|
+
)
|
|
409
|
+
})}
|
|
410
|
+
</Table.Row>
|
|
411
|
+
</Table.Header>
|
|
225
412
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
})}
|
|
239
|
-
>
|
|
240
|
-
{useAsTitle && column.fieldName === useAsTitle ? (
|
|
241
|
-
<Link
|
|
242
|
-
to={'/admin/collections/$collection/$id' as never}
|
|
243
|
-
params={{
|
|
244
|
-
collection: data.included.collection.path,
|
|
245
|
-
id: document.id,
|
|
246
|
-
}}
|
|
413
|
+
<Table.Body>
|
|
414
|
+
{localDocs.map((document) => (
|
|
415
|
+
<SortableTableRow key={document.id} id={document.id} disabled={!dragEnabled}>
|
|
416
|
+
{columns.map((column) => (
|
|
417
|
+
<Table.Cell
|
|
418
|
+
key={String(column.fieldName)}
|
|
419
|
+
className={cx({
|
|
420
|
+
'byline-coll-list-cell-right': column.align === 'right',
|
|
421
|
+
[styles.cellRight]: column.align === 'right',
|
|
422
|
+
'byline-coll-list-cell-center': column.align === 'center',
|
|
423
|
+
[styles.cellCenter]: column.align === 'center',
|
|
424
|
+
})}
|
|
247
425
|
>
|
|
248
|
-
{column.
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
426
|
+
{useAsTitle && column.fieldName === useAsTitle ? (
|
|
427
|
+
<Link
|
|
428
|
+
to={'/admin/collections/$collection/$id' as never}
|
|
429
|
+
params={{
|
|
430
|
+
collection: data.included.collection.path,
|
|
431
|
+
id: document.id,
|
|
432
|
+
}}
|
|
433
|
+
>
|
|
434
|
+
{column.formatter
|
|
435
|
+
? renderFormatted(
|
|
436
|
+
getColumnValue(document, column.fieldName as string),
|
|
437
|
+
document,
|
|
438
|
+
column.formatter
|
|
439
|
+
)
|
|
440
|
+
: (getColumnValue(document, column.fieldName as string) ??
|
|
441
|
+
'------')}
|
|
442
|
+
</Link>
|
|
443
|
+
) : column.formatter ? (
|
|
444
|
+
renderFormatted(
|
|
445
|
+
getColumnValue(document, column.fieldName as string),
|
|
446
|
+
document,
|
|
447
|
+
column.formatter
|
|
448
|
+
)
|
|
449
|
+
) : column.fieldName === 'status' && workflowStatuses ? (
|
|
450
|
+
<StatusBadge
|
|
451
|
+
status={document.status}
|
|
452
|
+
workflowStatuses={workflowStatuses}
|
|
453
|
+
hasPublishedVersion={document.hasPublishedVersion}
|
|
454
|
+
/>
|
|
455
|
+
) : (
|
|
456
|
+
String(getColumnValue(document, column.fieldName as string) ?? '')
|
|
457
|
+
)}
|
|
458
|
+
</Table.Cell>
|
|
459
|
+
))}
|
|
460
|
+
</SortableTableRow>
|
|
272
461
|
))}
|
|
273
|
-
</Table.
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
</
|
|
277
|
-
|
|
462
|
+
</Table.Body>
|
|
463
|
+
</Table>
|
|
464
|
+
</SortableContext>
|
|
465
|
+
</DndContext>
|
|
466
|
+
) : (
|
|
467
|
+
<Table>
|
|
468
|
+
<Table.Header>
|
|
469
|
+
<Table.Row>
|
|
470
|
+
{columns.map((column) => {
|
|
471
|
+
return (
|
|
472
|
+
<TableHeadingCellSortable
|
|
473
|
+
key={String(column.fieldName)}
|
|
474
|
+
fieldName={String(column.fieldName)}
|
|
475
|
+
label={column.label}
|
|
476
|
+
sortable={column.sortable}
|
|
477
|
+
scope="col"
|
|
478
|
+
align={column.align}
|
|
479
|
+
className={column.className}
|
|
480
|
+
/>
|
|
481
|
+
)
|
|
482
|
+
})}
|
|
483
|
+
</Table.Row>
|
|
484
|
+
</Table.Header>
|
|
485
|
+
|
|
486
|
+
<Table.Body>
|
|
487
|
+
{data?.docs?.map((document) => {
|
|
488
|
+
return (
|
|
489
|
+
<Table.Row key={document.id}>
|
|
490
|
+
{columns.map((column) => (
|
|
491
|
+
<Table.Cell
|
|
492
|
+
key={String(column.fieldName)}
|
|
493
|
+
className={cx({
|
|
494
|
+
'byline-coll-list-cell-right': column.align === 'right',
|
|
495
|
+
[styles.cellRight]: column.align === 'right',
|
|
496
|
+
'byline-coll-list-cell-center': column.align === 'center',
|
|
497
|
+
[styles.cellCenter]: column.align === 'center',
|
|
498
|
+
})}
|
|
499
|
+
>
|
|
500
|
+
{useAsTitle && column.fieldName === useAsTitle ? (
|
|
501
|
+
<Link
|
|
502
|
+
to={'/admin/collections/$collection/$id' as never}
|
|
503
|
+
params={{
|
|
504
|
+
collection: data.included.collection.path,
|
|
505
|
+
id: document.id,
|
|
506
|
+
}}
|
|
507
|
+
>
|
|
508
|
+
{column.formatter
|
|
509
|
+
? renderFormatted(
|
|
510
|
+
getColumnValue(document, column.fieldName as string),
|
|
511
|
+
document,
|
|
512
|
+
column.formatter
|
|
513
|
+
)
|
|
514
|
+
: (getColumnValue(document, column.fieldName as string) ??
|
|
515
|
+
'------')}
|
|
516
|
+
</Link>
|
|
517
|
+
) : column.formatter ? (
|
|
518
|
+
renderFormatted(
|
|
519
|
+
getColumnValue(document, column.fieldName as string),
|
|
520
|
+
document,
|
|
521
|
+
column.formatter
|
|
522
|
+
)
|
|
523
|
+
) : column.fieldName === 'status' && workflowStatuses ? (
|
|
524
|
+
<StatusBadge
|
|
525
|
+
status={document.status}
|
|
526
|
+
workflowStatuses={workflowStatuses}
|
|
527
|
+
hasPublishedVersion={document.hasPublishedVersion}
|
|
528
|
+
/>
|
|
529
|
+
) : (
|
|
530
|
+
String(getColumnValue(document, column.fieldName as string) ?? '')
|
|
531
|
+
)}
|
|
532
|
+
</Table.Cell>
|
|
533
|
+
))}
|
|
534
|
+
</Table.Row>
|
|
535
|
+
)
|
|
536
|
+
})}
|
|
537
|
+
</Table.Body>
|
|
538
|
+
</Table>
|
|
539
|
+
)}
|
|
278
540
|
{padRows(6 - (data?.docs?.length ?? 0))}
|
|
279
541
|
</Table.Container>
|
|
280
542
|
<div
|
|
@@ -21,7 +21,10 @@ import { z } from 'zod'
|
|
|
21
21
|
import { BreadcrumbsClient } from '../admin-shell/chrome/breadcrumbs/breadcrumbs-client.js'
|
|
22
22
|
import { useNavigate } from '../admin-shell/chrome/loose-router.js'
|
|
23
23
|
import { ListView } from '../admin-shell/collections/list.js'
|
|
24
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
getCollectionDocuments,
|
|
26
|
+
reorderCollectionDocument,
|
|
27
|
+
} from '../server-fns/collections/index.js'
|
|
25
28
|
|
|
26
29
|
const searchSchema = z.object({
|
|
27
30
|
page: z.coerce.number().min(1).optional(),
|
|
@@ -164,6 +167,17 @@ export function createCollectionListRoute(path: string) {
|
|
|
164
167
|
columns={columns}
|
|
165
168
|
workflowStatuses={workflowStatuses}
|
|
166
169
|
useAsTitle={collectionDef.useAsTitle}
|
|
170
|
+
orderable={adminConfig?.orderable === true}
|
|
171
|
+
onReorder={async ({ documentId, beforeDocumentId, afterDocumentId }) => {
|
|
172
|
+
await reorderCollectionDocument({
|
|
173
|
+
data: {
|
|
174
|
+
collection,
|
|
175
|
+
documentId,
|
|
176
|
+
beforeDocumentId,
|
|
177
|
+
afterDocumentId,
|
|
178
|
+
},
|
|
179
|
+
})
|
|
180
|
+
}}
|
|
167
181
|
/>
|
|
168
182
|
)}
|
|
169
183
|
</>
|
|
@@ -10,6 +10,7 @@ import { createServerFn } from '@tanstack/react-start'
|
|
|
10
10
|
|
|
11
11
|
import {
|
|
12
12
|
ERR_NOT_FOUND,
|
|
13
|
+
getCollectionAdminConfig,
|
|
13
14
|
getCollectionSchemasForPath,
|
|
14
15
|
getLogger,
|
|
15
16
|
getServerConfig,
|
|
@@ -65,9 +66,19 @@ export const getCollectionDocuments = createServerFn({ method: 'GET' })
|
|
|
65
66
|
if (params.status) where.status = params.status
|
|
66
67
|
if (params.query) where.query = params.query
|
|
67
68
|
|
|
69
|
+
// Default sort for `orderable: true` collections is the fractional
|
|
70
|
+
// `order_key` ascending. Caller's explicit `params.order` always wins
|
|
71
|
+
// so the admin can re-sort by other columns without surprise.
|
|
72
|
+
const adminConfig = getCollectionAdminConfig(path)
|
|
73
|
+
const defaultSort: Record<string, 'asc' | 'desc'> | undefined =
|
|
74
|
+
adminConfig?.orderable === true ? { order_key: 'asc' } : undefined
|
|
75
|
+
const sortSpec: Record<string, 'asc' | 'desc'> | undefined = params.order
|
|
76
|
+
? { [params.order]: params.desc === false ? 'asc' : 'desc' }
|
|
77
|
+
: defaultSort
|
|
78
|
+
|
|
68
79
|
const result = await handle.find({
|
|
69
80
|
where: Object.keys(where).length > 0 ? where : undefined,
|
|
70
|
-
sort:
|
|
81
|
+
sort: sortSpec,
|
|
71
82
|
locale: params.locale ?? 'en',
|
|
72
83
|
page: params.page,
|
|
73
84
|
pageSize,
|