@byline/host-tanstack-start 3.12.2 → 3.13.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/collections/edit.js +1 -0
- package/dist/admin-shell/collections/tree-list-projection.d.ts +56 -0
- package/dist/admin-shell/collections/tree-list-projection.js +109 -0
- package/dist/admin-shell/collections/tree-list.d.ts +29 -0
- package/dist/admin-shell/collections/tree-list.js +272 -0
- package/dist/admin-shell/collections/tree-list.module.js +17 -0
- package/dist/admin-shell/collections/tree-list_module.css +117 -0
- package/dist/integrations/byline-field-services.js +25 -1
- package/dist/routes/create-collection-list-route.js +27 -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/tree.d.ts +84 -0
- package/dist/server-fns/collections/tree.js +144 -0
- package/package.json +8 -8
- package/src/admin-shell/collections/edit.tsx +1 -0
- package/src/admin-shell/collections/tree-list-projection.test.node.ts +116 -0
- package/src/admin-shell/collections/tree-list-projection.ts +198 -0
- package/src/admin-shell/collections/tree-list.module.css +140 -0
- package/src/admin-shell/collections/tree-list.tsx +354 -0
- package/src/integrations/byline-field-services.ts +29 -0
- package/src/routes/create-collection-list-route.tsx +39 -1
- package/src/server-fns/collections/index.ts +1 -0
- package/src/server-fns/collections/tree.ts +203 -0
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TreeListView — built-in list view for `tree: true` collections. Reuses the
|
|
3
|
+
* collection-list head/stats look; adds depth indentation, a branch glyph, and
|
|
4
|
+
* an "unplaced" group separator.
|
|
5
|
+
*
|
|
6
|
+
* Override handles:
|
|
7
|
+
* .byline-coll-list-head / -title / -stats — shared with the table list
|
|
8
|
+
* .byline-tree-list-table-wrap — table wrapper
|
|
9
|
+
* .byline-tree-list-title / -branch — indented title cell
|
|
10
|
+
* .byline-tree-list-group — "Unplaced" separator row
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
.head,
|
|
14
|
+
:global(.byline-coll-list-head) {
|
|
15
|
+
display: flex;
|
|
16
|
+
align-items: center;
|
|
17
|
+
gap: 0.75rem;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.title,
|
|
21
|
+
:global(.byline-coll-list-title) {
|
|
22
|
+
margin: 0;
|
|
23
|
+
padding-bottom: 2px;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.stats,
|
|
27
|
+
:global(.byline-coll-list-stats) {
|
|
28
|
+
display: flex;
|
|
29
|
+
align-items: center;
|
|
30
|
+
justify-content: center;
|
|
31
|
+
margin-right: auto;
|
|
32
|
+
margin-bottom: -4px;
|
|
33
|
+
height: 28px;
|
|
34
|
+
min-width: 28px;
|
|
35
|
+
padding: 5px 6px;
|
|
36
|
+
white-space: nowrap;
|
|
37
|
+
font-size: 0.875rem;
|
|
38
|
+
line-height: 0;
|
|
39
|
+
background-color: var(--gray-25);
|
|
40
|
+
border: var(--border-width-thin) var(--border-style-solid) var(--gray-200);
|
|
41
|
+
border-radius: 0.375rem;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
:is([data-theme="dark"], :global(.dark)) .stats,
|
|
45
|
+
:is([data-theme="dark"], :global(.dark)) :global(.byline-coll-list-stats) {
|
|
46
|
+
background-color: var(--canvas-700);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.tableWrap,
|
|
50
|
+
:global(.byline-tree-list-table-wrap) {
|
|
51
|
+
margin-top: 0.75rem;
|
|
52
|
+
margin-bottom: 0.75rem;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.titleCell,
|
|
56
|
+
:global(.byline-tree-list-title) {
|
|
57
|
+
display: inline-flex;
|
|
58
|
+
align-items: center;
|
|
59
|
+
gap: 0.4rem;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.branch,
|
|
63
|
+
:global(.byline-tree-list-branch) {
|
|
64
|
+
color: var(--gray-400);
|
|
65
|
+
font-family: var(--font-mono, monospace);
|
|
66
|
+
user-select: none;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.groupRow,
|
|
70
|
+
:global(.byline-tree-list-group) {
|
|
71
|
+
background-color: var(--gray-25);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
:is([data-theme="dark"], :global(.dark)) .groupRow,
|
|
75
|
+
:is([data-theme="dark"], :global(.dark)) :global(.byline-tree-list-group) {
|
|
76
|
+
background-color: var(--canvas-700);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.groupCell {
|
|
80
|
+
font-size: 0.75rem;
|
|
81
|
+
font-weight: 600;
|
|
82
|
+
text-transform: uppercase;
|
|
83
|
+
letter-spacing: 0.04em;
|
|
84
|
+
color: var(--gray-500);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.cellRight {
|
|
88
|
+
text-align: right;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.cellCenter {
|
|
92
|
+
text-align: center;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/* ── Drag-to-reorder / re-parent ────────────────────────────────────── */
|
|
96
|
+
|
|
97
|
+
.dragCell {
|
|
98
|
+
width: 34px;
|
|
99
|
+
padding-inline: 0.25rem;
|
|
100
|
+
text-align: center;
|
|
101
|
+
vertical-align: middle;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.dragHandle {
|
|
105
|
+
display: inline-flex;
|
|
106
|
+
align-items: center;
|
|
107
|
+
justify-content: center;
|
|
108
|
+
padding: 4px;
|
|
109
|
+
color: var(--gray-400);
|
|
110
|
+
background: none;
|
|
111
|
+
border: none;
|
|
112
|
+
border-radius: 4px;
|
|
113
|
+
cursor: grab;
|
|
114
|
+
touch-action: none;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.dragHandle:hover {
|
|
118
|
+
color: var(--gray-600);
|
|
119
|
+
background-color: var(--gray-50);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.dragHandle:active {
|
|
123
|
+
cursor: grabbing;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
:is([data-theme="dark"], :global(.dark)) .dragHandle:hover {
|
|
127
|
+
color: var(--gray-200);
|
|
128
|
+
background-color: var(--canvas-700);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/* The row being dragged — highlight + a left rule that reads as "this is the
|
|
132
|
+
* grabbed node at its projected depth". */
|
|
133
|
+
.draggingRow {
|
|
134
|
+
background-color: var(--gray-25);
|
|
135
|
+
box-shadow: inset 2px 0 0 var(--theme-400, var(--gray-400));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
:is([data-theme="dark"], :global(.dark)) .draggingRow {
|
|
139
|
+
background-color: var(--canvas-700);
|
|
140
|
+
}
|
|
@@ -0,0 +1,354 @@
|
|
|
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
|
+
|
|
9
|
+
import type React from 'react'
|
|
10
|
+
import { useEffect, useMemo, useState } from 'react'
|
|
11
|
+
import { useRouter } from '@tanstack/react-router'
|
|
12
|
+
|
|
13
|
+
import { renderFormatted, StatusBadge } from '@byline/admin/react'
|
|
14
|
+
import type { ColumnDefinition, WorkflowStatus } from '@byline/core'
|
|
15
|
+
import { useTranslation } from '@byline/i18n/react'
|
|
16
|
+
import {
|
|
17
|
+
Container,
|
|
18
|
+
GripperVerticalIcon,
|
|
19
|
+
IconButton,
|
|
20
|
+
PlusIcon,
|
|
21
|
+
Section,
|
|
22
|
+
Table,
|
|
23
|
+
useToastManager,
|
|
24
|
+
} from '@byline/ui/react'
|
|
25
|
+
import {
|
|
26
|
+
DndContext,
|
|
27
|
+
type DragEndEvent,
|
|
28
|
+
type DragMoveEvent,
|
|
29
|
+
type DragOverEvent,
|
|
30
|
+
type DragStartEvent,
|
|
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'
|
|
42
|
+
import cx from 'classnames'
|
|
43
|
+
|
|
44
|
+
import { Link } from '../chrome/loose-router.js'
|
|
45
|
+
import { TableHeadingCellSortable } from '../chrome/th-sortable.js'
|
|
46
|
+
import { formatNumber } from '../chrome/utils.js'
|
|
47
|
+
import styles from './tree-list.module.css'
|
|
48
|
+
import { applyProjection, getTreeProjection } from './tree-list-projection.js'
|
|
49
|
+
import type { CollectionTreeRow } from '../../server-fns/collections/tree.js'
|
|
50
|
+
|
|
51
|
+
/** px per depth level — must match `--byline-tree-indent` in the CSS and the
|
|
52
|
+
* unit of the drag offset fed to the projection. */
|
|
53
|
+
const INDENT = 24
|
|
54
|
+
|
|
55
|
+
export type TreeMoveFn = (params: {
|
|
56
|
+
documentId: string
|
|
57
|
+
parentDocumentId: string | null
|
|
58
|
+
beforeDocumentId: string | null
|
|
59
|
+
afterDocumentId: string | null
|
|
60
|
+
}) => Promise<unknown>
|
|
61
|
+
|
|
62
|
+
// biome-ignore lint/suspicious/noExplicitAny: tree rows carry heterogeneous fields
|
|
63
|
+
function getColumnValue(row: CollectionTreeRow, fieldName: string): any {
|
|
64
|
+
if (row.fields && fieldName in row.fields) return row.fields[fieldName]
|
|
65
|
+
return (row as Record<string, any>)[fieldName]
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* One draggable `<tr>` in the tree. Renders directly (not via `Table.Row`) so it
|
|
70
|
+
* can take dnd-kit's function `ref`. Indentation uses the *projected* depth for
|
|
71
|
+
* the active row mid-drag, so the row visibly shifts level as it is dragged
|
|
72
|
+
* horizontally.
|
|
73
|
+
*/
|
|
74
|
+
function SortableTreeRow({
|
|
75
|
+
id,
|
|
76
|
+
depth,
|
|
77
|
+
dragging,
|
|
78
|
+
dragHandleLabel,
|
|
79
|
+
children,
|
|
80
|
+
}: {
|
|
81
|
+
id: string
|
|
82
|
+
depth: number
|
|
83
|
+
dragging: boolean
|
|
84
|
+
dragHandleLabel: string
|
|
85
|
+
children: React.ReactNode
|
|
86
|
+
}) {
|
|
87
|
+
const { setNodeRef, attributes, listeners, transform, transition, isDragging } = useSortable({
|
|
88
|
+
id,
|
|
89
|
+
})
|
|
90
|
+
const style: React.CSSProperties = {
|
|
91
|
+
transform: transform ? `translate3d(0, ${transform.y}px, 0)` : undefined,
|
|
92
|
+
transition,
|
|
93
|
+
position: 'relative',
|
|
94
|
+
zIndex: isDragging ? 10 : 'auto',
|
|
95
|
+
opacity: isDragging ? 0.5 : 1,
|
|
96
|
+
}
|
|
97
|
+
return (
|
|
98
|
+
<tr
|
|
99
|
+
ref={setNodeRef}
|
|
100
|
+
className={cx('byline-table-row', { [styles.draggingRow]: dragging })}
|
|
101
|
+
style={style}
|
|
102
|
+
>
|
|
103
|
+
<td className={cx('byline-tree-list-drag-cell', styles.dragCell)}>
|
|
104
|
+
<button
|
|
105
|
+
type="button"
|
|
106
|
+
className={cx('byline-tree-list-drag-handle', styles.dragHandle)}
|
|
107
|
+
aria-label={dragHandleLabel}
|
|
108
|
+
{...attributes}
|
|
109
|
+
{...listeners}
|
|
110
|
+
>
|
|
111
|
+
<GripperVerticalIcon />
|
|
112
|
+
</button>
|
|
113
|
+
</td>
|
|
114
|
+
{children}
|
|
115
|
+
</tr>
|
|
116
|
+
)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export const TreeListView = ({
|
|
120
|
+
rows,
|
|
121
|
+
columns,
|
|
122
|
+
workflowStatuses,
|
|
123
|
+
useAsTitle,
|
|
124
|
+
collection,
|
|
125
|
+
collectionLabels,
|
|
126
|
+
onMove,
|
|
127
|
+
}: {
|
|
128
|
+
rows: CollectionTreeRow[]
|
|
129
|
+
columns: ColumnDefinition[]
|
|
130
|
+
workflowStatuses?: WorkflowStatus[]
|
|
131
|
+
useAsTitle?: string
|
|
132
|
+
collection: string
|
|
133
|
+
collectionLabels: { singular: string; plural: string }
|
|
134
|
+
/** When provided, the placed tree becomes drag-to-reorder / re-parent. */
|
|
135
|
+
onMove?: TreeMoveFn
|
|
136
|
+
}) => {
|
|
137
|
+
const { t } = useTranslation('byline-admin')
|
|
138
|
+
const router = useRouter()
|
|
139
|
+
const toastManager = useToastManager()
|
|
140
|
+
|
|
141
|
+
const unplaced = useMemo(() => rows.filter((r) => r.unplaced), [rows])
|
|
142
|
+
|
|
143
|
+
// Local mirror of the placed tree so drag can paint optimistically before the
|
|
144
|
+
// server roundtrip; resync from props when fresh loader data arrives and no
|
|
145
|
+
// move is mid-flight (clobbering an in-flight optimistic state flashes the row
|
|
146
|
+
// back). Mirrors the flat-list reorder pattern.
|
|
147
|
+
const [localPlaced, setLocalPlaced] = useState(() => rows.filter((r) => !r.unplaced))
|
|
148
|
+
const [isMoving, setIsMoving] = useState(false)
|
|
149
|
+
useEffect(() => {
|
|
150
|
+
if (!isMoving) setLocalPlaced(rows.filter((r) => !r.unplaced))
|
|
151
|
+
}, [rows, isMoving])
|
|
152
|
+
|
|
153
|
+
const [activeId, setActiveId] = useState<string | null>(null)
|
|
154
|
+
const [overId, setOverId] = useState<string | null>(null)
|
|
155
|
+
const [offsetLeft, setOffsetLeft] = useState(0)
|
|
156
|
+
|
|
157
|
+
const sensors = useSensors(
|
|
158
|
+
useSensor(PointerSensor, { activationConstraint: { distance: 5 } }),
|
|
159
|
+
useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates })
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
// Live projection while dragging — drives the active row's indentation.
|
|
163
|
+
const projection = useMemo(() => {
|
|
164
|
+
if (activeId == null || overId == null) return null
|
|
165
|
+
return getTreeProjection(localPlaced, activeId, overId, offsetLeft, INDENT)
|
|
166
|
+
}, [activeId, overId, offsetLeft, localPlaced])
|
|
167
|
+
|
|
168
|
+
const resetDnd = () => {
|
|
169
|
+
setActiveId(null)
|
|
170
|
+
setOverId(null)
|
|
171
|
+
setOffsetLeft(0)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const handleDragStart = ({ active }: DragStartEvent) => setActiveId(String(active.id))
|
|
175
|
+
const handleDragMove = ({ delta }: DragMoveEvent) => setOffsetLeft(delta.x)
|
|
176
|
+
const handleDragOver = ({ over }: DragOverEvent) => setOverId(over ? String(over.id) : null)
|
|
177
|
+
|
|
178
|
+
const handleDragEnd = async ({ active, over }: DragEndEvent) => {
|
|
179
|
+
const proj =
|
|
180
|
+
over != null
|
|
181
|
+
? getTreeProjection(localPlaced, String(active.id), String(over.id), offsetLeft, INDENT)
|
|
182
|
+
: null
|
|
183
|
+
resetDnd()
|
|
184
|
+
if (!onMove || proj == null) return
|
|
185
|
+
|
|
186
|
+
const documentId = String(active.id)
|
|
187
|
+
const next = applyProjection(localPlaced, documentId, proj)
|
|
188
|
+
const current = localPlaced
|
|
189
|
+
const unchanged =
|
|
190
|
+
next.map((r) => r.id).join() === current.map((r) => r.id).join() &&
|
|
191
|
+
next.find((r) => r.id === documentId)?.parentId ===
|
|
192
|
+
current.find((r) => r.id === documentId)?.parentId
|
|
193
|
+
if (unchanged) return
|
|
194
|
+
|
|
195
|
+
setLocalPlaced(next)
|
|
196
|
+
setIsMoving(true)
|
|
197
|
+
try {
|
|
198
|
+
await onMove({
|
|
199
|
+
documentId,
|
|
200
|
+
parentDocumentId: proj.parentId,
|
|
201
|
+
beforeDocumentId: proj.beforeId,
|
|
202
|
+
afterDocumentId: proj.afterId,
|
|
203
|
+
})
|
|
204
|
+
await router.invalidate()
|
|
205
|
+
} catch {
|
|
206
|
+
setLocalPlaced(current)
|
|
207
|
+
toastManager.add({
|
|
208
|
+
title: t('collections.list.reorderFailedToast'),
|
|
209
|
+
description: t('collections.list.reorderFailedDescription'),
|
|
210
|
+
data: { intent: 'danger', iconType: 'danger', icon: true, close: true },
|
|
211
|
+
})
|
|
212
|
+
} finally {
|
|
213
|
+
setIsMoving(false)
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const renderCells = (row: CollectionTreeRow, depth: number) =>
|
|
218
|
+
columns.map((column) => {
|
|
219
|
+
const fieldName = column.fieldName as string
|
|
220
|
+
const isTitle = useAsTitle != null && fieldName === useAsTitle
|
|
221
|
+
return (
|
|
222
|
+
<Table.Cell
|
|
223
|
+
key={fieldName}
|
|
224
|
+
className={cx({
|
|
225
|
+
[styles.cellRight]: column.align === 'right',
|
|
226
|
+
[styles.cellCenter]: column.align === 'center',
|
|
227
|
+
})}
|
|
228
|
+
>
|
|
229
|
+
{isTitle ? (
|
|
230
|
+
<span
|
|
231
|
+
className={cx('byline-tree-list-title', styles.titleCell)}
|
|
232
|
+
style={{ paddingInlineStart: `${depth * INDENT}px` }}
|
|
233
|
+
>
|
|
234
|
+
{depth > 0 && (
|
|
235
|
+
<span aria-hidden="true" className={cx('byline-tree-list-branch', styles.branch)}>
|
|
236
|
+
└─
|
|
237
|
+
</span>
|
|
238
|
+
)}
|
|
239
|
+
<Link
|
|
240
|
+
to={'/admin/collections/$collection/$id' as never}
|
|
241
|
+
params={{ collection, id: row.id }}
|
|
242
|
+
>
|
|
243
|
+
{column.formatter
|
|
244
|
+
? renderFormatted(getColumnValue(row, fieldName), row, column.formatter)
|
|
245
|
+
: (getColumnValue(row, fieldName) ?? row.path ?? '------')}
|
|
246
|
+
</Link>
|
|
247
|
+
</span>
|
|
248
|
+
) : column.formatter ? (
|
|
249
|
+
renderFormatted(getColumnValue(row, fieldName), row, column.formatter)
|
|
250
|
+
) : fieldName === 'status' && workflowStatuses ? (
|
|
251
|
+
<StatusBadge status={row.status} workflowStatuses={workflowStatuses} />
|
|
252
|
+
) : (
|
|
253
|
+
String(getColumnValue(row, fieldName) ?? '')
|
|
254
|
+
)}
|
|
255
|
+
</Table.Cell>
|
|
256
|
+
)
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
const dndEnabled = onMove != null
|
|
260
|
+
const colSpanLead = dndEnabled ? 1 : 0
|
|
261
|
+
|
|
262
|
+
return (
|
|
263
|
+
<Section>
|
|
264
|
+
<Container>
|
|
265
|
+
<div className={cx('byline-coll-list-head', styles.head)}>
|
|
266
|
+
<h1 className={cx('byline-coll-list-title', styles.title)}>{collectionLabels.plural}</h1>
|
|
267
|
+
<span className={cx('byline-coll-list-stats', styles.stats)}>
|
|
268
|
+
{formatNumber(rows.length, 0)}
|
|
269
|
+
</span>
|
|
270
|
+
<IconButton
|
|
271
|
+
aria-label={t('collections.list.createAriaLabel')}
|
|
272
|
+
render={
|
|
273
|
+
<Link to={'/admin/collections/$collection/create' as never} params={{ collection }} />
|
|
274
|
+
}
|
|
275
|
+
>
|
|
276
|
+
<PlusIcon height="18px" width="18px" svgClassName="stroke-white" />
|
|
277
|
+
</IconButton>
|
|
278
|
+
</div>
|
|
279
|
+
|
|
280
|
+
<Table.Container className={cx('byline-tree-list-table-wrap', styles.tableWrap)}>
|
|
281
|
+
<DndContext
|
|
282
|
+
sensors={sensors}
|
|
283
|
+
onDragStart={handleDragStart}
|
|
284
|
+
onDragMove={handleDragMove}
|
|
285
|
+
onDragOver={handleDragOver}
|
|
286
|
+
onDragEnd={handleDragEnd}
|
|
287
|
+
onDragCancel={resetDnd}
|
|
288
|
+
>
|
|
289
|
+
<Table>
|
|
290
|
+
<Table.Header>
|
|
291
|
+
<Table.Row>
|
|
292
|
+
{dndEnabled && (
|
|
293
|
+
<th scope="col" className={cx('byline-tree-list-drag-cell', styles.dragCell)} />
|
|
294
|
+
)}
|
|
295
|
+
{columns.map((column) => (
|
|
296
|
+
<TableHeadingCellSortable
|
|
297
|
+
key={String(column.fieldName)}
|
|
298
|
+
fieldName={String(column.fieldName)}
|
|
299
|
+
label={column.label}
|
|
300
|
+
sortable={false}
|
|
301
|
+
scope="col"
|
|
302
|
+
align={column.align}
|
|
303
|
+
className={column.className}
|
|
304
|
+
/>
|
|
305
|
+
))}
|
|
306
|
+
</Table.Row>
|
|
307
|
+
</Table.Header>
|
|
308
|
+
<Table.Body>
|
|
309
|
+
<SortableContext
|
|
310
|
+
items={localPlaced.map((r) => r.id)}
|
|
311
|
+
strategy={verticalListSortingStrategy}
|
|
312
|
+
>
|
|
313
|
+
{localPlaced.map((row) => {
|
|
314
|
+
const depth = row.id === activeId && projection ? projection.depth : row.depth
|
|
315
|
+
return dndEnabled ? (
|
|
316
|
+
<SortableTreeRow
|
|
317
|
+
key={row.id}
|
|
318
|
+
id={row.id}
|
|
319
|
+
depth={depth}
|
|
320
|
+
dragging={row.id === activeId}
|
|
321
|
+
dragHandleLabel={t('collections.list.dragHandleAriaLabel')}
|
|
322
|
+
>
|
|
323
|
+
{renderCells(row, depth)}
|
|
324
|
+
</SortableTreeRow>
|
|
325
|
+
) : (
|
|
326
|
+
<Table.Row key={row.id}>{renderCells(row, row.depth)}</Table.Row>
|
|
327
|
+
)
|
|
328
|
+
})}
|
|
329
|
+
</SortableContext>
|
|
330
|
+
|
|
331
|
+
{unplaced.length > 0 && (
|
|
332
|
+
<Table.Row className={cx('byline-tree-list-group', styles.groupRow)}>
|
|
333
|
+
<Table.Cell className={styles.groupCell} colSpan={colSpanLead + 1}>
|
|
334
|
+
{t('treeListView.unplacedHeading')}
|
|
335
|
+
</Table.Cell>
|
|
336
|
+
{columns.slice(1).map((column) => (
|
|
337
|
+
<Table.Cell key={String(column.fieldName)} />
|
|
338
|
+
))}
|
|
339
|
+
</Table.Row>
|
|
340
|
+
)}
|
|
341
|
+
{unplaced.map((row) => (
|
|
342
|
+
<Table.Row key={row.id}>
|
|
343
|
+
{dndEnabled && <Table.Cell className={styles.dragCell} />}
|
|
344
|
+
{renderCells(row, 0)}
|
|
345
|
+
</Table.Row>
|
|
346
|
+
))}
|
|
347
|
+
</Table.Body>
|
|
348
|
+
</Table>
|
|
349
|
+
</DndContext>
|
|
350
|
+
</Table.Container>
|
|
351
|
+
</Container>
|
|
352
|
+
</Section>
|
|
353
|
+
)
|
|
354
|
+
}
|
|
@@ -19,10 +19,20 @@
|
|
|
19
19
|
import type {
|
|
20
20
|
BylineFieldServices,
|
|
21
21
|
GetCollectionDocumentsFn,
|
|
22
|
+
GetTreeAncestorsFn,
|
|
23
|
+
GetTreeParentFn,
|
|
24
|
+
PlaceTreeNodeFn,
|
|
25
|
+
RemoveFromTreeFn,
|
|
22
26
|
UploadFieldFn,
|
|
23
27
|
} from '@byline/admin/react'
|
|
24
28
|
|
|
25
29
|
import { getCollectionDocuments as serverGetCollectionDocuments } from '../server-fns/collections/list.js'
|
|
30
|
+
import {
|
|
31
|
+
getTreeAncestors as serverGetTreeAncestors,
|
|
32
|
+
getTreeParent as serverGetTreeParent,
|
|
33
|
+
placeTreeNode as serverPlaceTreeNode,
|
|
34
|
+
removeFromTree as serverRemoveFromTree,
|
|
35
|
+
} from '../server-fns/collections/tree.js'
|
|
26
36
|
import { uploadField as serverUploadField } from '../server-fns/collections/upload.js'
|
|
27
37
|
|
|
28
38
|
const getCollectionDocuments: GetCollectionDocumentsFn = ({ collection, params }) =>
|
|
@@ -33,7 +43,26 @@ const getCollectionDocuments: GetCollectionDocumentsFn = ({ collection, params }
|
|
|
33
43
|
const uploadField: UploadFieldFn = (collection, formData, createDocument) =>
|
|
34
44
|
serverUploadField(collection, formData, createDocument)
|
|
35
45
|
|
|
46
|
+
const placeTreeNode: PlaceTreeNodeFn = async (input) => {
|
|
47
|
+
const { orderKey } = await serverPlaceTreeNode({ data: input })
|
|
48
|
+
return { orderKey }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const removeFromTree: RemoveFromTreeFn = async (input) => {
|
|
52
|
+
await serverRemoveFromTree({ data: input })
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const getTreeAncestors: GetTreeAncestorsFn = (input) =>
|
|
56
|
+
serverGetTreeAncestors({ data: input }) as ReturnType<GetTreeAncestorsFn>
|
|
57
|
+
|
|
58
|
+
const getTreeParent: GetTreeParentFn = (input) =>
|
|
59
|
+
serverGetTreeParent({ data: input }) as ReturnType<GetTreeParentFn>
|
|
60
|
+
|
|
36
61
|
export const bylineFieldServices: BylineFieldServices = {
|
|
37
62
|
getCollectionDocuments,
|
|
38
63
|
uploadField,
|
|
64
|
+
placeTreeNode,
|
|
65
|
+
removeFromTree,
|
|
66
|
+
getTreeAncestors,
|
|
67
|
+
getTreeParent,
|
|
39
68
|
}
|
|
@@ -22,10 +22,14 @@ import { z } from 'zod'
|
|
|
22
22
|
import { BreadcrumbsClient } from '../admin-shell/chrome/breadcrumbs/breadcrumbs-client.js'
|
|
23
23
|
import { useNavigate } from '../admin-shell/chrome/loose-router.js'
|
|
24
24
|
import { ListView } from '../admin-shell/collections/list.js'
|
|
25
|
+
import { TreeListView } from '../admin-shell/collections/tree-list.js'
|
|
25
26
|
import {
|
|
26
27
|
getCollectionDocuments,
|
|
28
|
+
getCollectionTree,
|
|
29
|
+
placeTreeNode,
|
|
27
30
|
reorderCollectionDocument,
|
|
28
31
|
} from '../server-fns/collections/index.js'
|
|
32
|
+
import type { CollectionTreeRow } from '../server-fns/collections/tree.js'
|
|
29
33
|
|
|
30
34
|
const searchSchema = z.object({
|
|
31
35
|
page: z.coerce.number().min(1).optional(),
|
|
@@ -71,6 +75,15 @@ export function createCollectionListRoute(path: string) {
|
|
|
71
75
|
throw notFound()
|
|
72
76
|
}
|
|
73
77
|
|
|
78
|
+
// `tree: true` collections use the built-in tree list view: load the whole
|
|
79
|
+
// tree (ordered, depth-tagged rows + unplaced docs) rather than a
|
|
80
|
+
// paginated single-collection page.
|
|
81
|
+
if (collectionDef.tree === true) {
|
|
82
|
+
return await getCollectionTree({
|
|
83
|
+
data: { collection: params.collection, locale: deps.locale },
|
|
84
|
+
})
|
|
85
|
+
}
|
|
86
|
+
|
|
74
87
|
// Derive the field names the list view needs from the admin column config.
|
|
75
88
|
// This lets findDocuments query only the relevant store tables.
|
|
76
89
|
const adminConfig = getCollectionAdminConfig(params.collection)
|
|
@@ -157,7 +170,32 @@ export function createCollectionListRoute(path: string) {
|
|
|
157
170
|
},
|
|
158
171
|
]}
|
|
159
172
|
/>
|
|
160
|
-
{
|
|
173
|
+
{collectionDef.tree === true ? (
|
|
174
|
+
<TreeListView
|
|
175
|
+
rows={(data as { rows: CollectionTreeRow[] }).rows}
|
|
176
|
+
columns={columns}
|
|
177
|
+
workflowStatuses={workflowStatuses}
|
|
178
|
+
useAsTitle={collectionDef.useAsTitle}
|
|
179
|
+
collection={collection}
|
|
180
|
+
collectionLabels={data.included.collection.labels}
|
|
181
|
+
onMove={async ({
|
|
182
|
+
documentId,
|
|
183
|
+
parentDocumentId,
|
|
184
|
+
beforeDocumentId,
|
|
185
|
+
afterDocumentId,
|
|
186
|
+
}) => {
|
|
187
|
+
await placeTreeNode({
|
|
188
|
+
data: {
|
|
189
|
+
collection,
|
|
190
|
+
documentId,
|
|
191
|
+
parentDocumentId,
|
|
192
|
+
beforeDocumentId,
|
|
193
|
+
afterDocumentId,
|
|
194
|
+
},
|
|
195
|
+
})
|
|
196
|
+
}}
|
|
197
|
+
/>
|
|
198
|
+
) : CustomListView ? (
|
|
161
199
|
<CustomListView data={data} workflowStatuses={workflowStatuses} />
|
|
162
200
|
) : (
|
|
163
201
|
<ListView
|