@byline/host-tanstack-start 3.12.1 → 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,203 @@
|
|
|
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
|
+
/**
|
|
10
|
+
* Document-tree server functions for `tree: true` collections — the transport
|
|
11
|
+
* behind the sidebar tree-placement widget. Thin wrappers over the
|
|
12
|
+
* `@byline/client` tree API, which asserts the actor ability, enforces the
|
|
13
|
+
* cycle / same-collection invariants, and fires the `afterTreeChange`
|
|
14
|
+
* invalidation hook. See docs/DOCUMENT-TREE.md.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { createServerFn } from '@tanstack/react-start'
|
|
18
|
+
|
|
19
|
+
import { ERR_NOT_FOUND, getLogger } from '@byline/core'
|
|
20
|
+
|
|
21
|
+
import { ensureCollection } from '../../integrations/api-utils.js'
|
|
22
|
+
import { getAdminBylineClient } from '../../integrations/byline-client.js'
|
|
23
|
+
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// Place / move a node within the tree (place / reorder / re-parent)
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
export const placeTreeNode = createServerFn({ method: 'POST' })
|
|
29
|
+
.validator(
|
|
30
|
+
(input: {
|
|
31
|
+
collection: string
|
|
32
|
+
documentId: string
|
|
33
|
+
parentDocumentId: string | null
|
|
34
|
+
beforeDocumentId?: string | null
|
|
35
|
+
afterDocumentId?: string | null
|
|
36
|
+
}) => input
|
|
37
|
+
)
|
|
38
|
+
.handler(async ({ data }) => {
|
|
39
|
+
const { collection: path, documentId } = data
|
|
40
|
+
const config = await ensureCollection(path)
|
|
41
|
+
if (!config) {
|
|
42
|
+
throw ERR_NOT_FOUND({
|
|
43
|
+
message: 'Collection not found',
|
|
44
|
+
details: { collectionPath: path },
|
|
45
|
+
}).log(getLogger())
|
|
46
|
+
}
|
|
47
|
+
const handle = getAdminBylineClient().collection(path)
|
|
48
|
+
const result = await handle.placeTreeNode(documentId, {
|
|
49
|
+
parentDocumentId: data.parentDocumentId,
|
|
50
|
+
beforeDocumentId: data.beforeDocumentId ?? null,
|
|
51
|
+
afterDocumentId: data.afterDocumentId ?? null,
|
|
52
|
+
})
|
|
53
|
+
return { status: 'ok' as const, orderKey: result.orderKey }
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
// Remove a node from the tree (back to the unplaced state)
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
export const removeFromTree = createServerFn({ method: 'POST' })
|
|
61
|
+
.validator((input: { collection: string; documentId: string }) => input)
|
|
62
|
+
.handler(async ({ data }) => {
|
|
63
|
+
const { collection: path, documentId } = data
|
|
64
|
+
const config = await ensureCollection(path)
|
|
65
|
+
if (!config) {
|
|
66
|
+
throw ERR_NOT_FOUND({
|
|
67
|
+
message: 'Collection not found',
|
|
68
|
+
details: { collectionPath: path },
|
|
69
|
+
}).log(getLogger())
|
|
70
|
+
}
|
|
71
|
+
await getAdminBylineClient().collection(path).removeFromTree(documentId)
|
|
72
|
+
return { status: 'ok' as const }
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
// Resolve a document's ancestor chain (root-first), hydrated with titles
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
|
|
79
|
+
export const getTreeAncestors = createServerFn({ method: 'GET' })
|
|
80
|
+
.validator((input: { collection: string; documentId: string }) => input)
|
|
81
|
+
.handler(async ({ data }) => {
|
|
82
|
+
const { collection: path, documentId } = data
|
|
83
|
+
const config = await ensureCollection(path)
|
|
84
|
+
if (!config) {
|
|
85
|
+
throw ERR_NOT_FOUND({
|
|
86
|
+
message: 'Collection not found',
|
|
87
|
+
details: { collectionPath: path },
|
|
88
|
+
}).log(getLogger())
|
|
89
|
+
}
|
|
90
|
+
const useAsTitle = config.definition.useAsTitle
|
|
91
|
+
// Admin context: read with `status: 'any'` so draft ancestors still show.
|
|
92
|
+
const ancestors = await getAdminBylineClient()
|
|
93
|
+
.collection(path)
|
|
94
|
+
.getAncestors(documentId, { status: 'any' })
|
|
95
|
+
return ancestors.map((doc) => {
|
|
96
|
+
const fields = doc.fields as Record<string, any> | undefined
|
|
97
|
+
const title = useAsTitle ? fields?.[useAsTitle] : undefined
|
|
98
|
+
return {
|
|
99
|
+
id: doc.id,
|
|
100
|
+
title: typeof title === 'string' && title.length > 0 ? title : doc.path,
|
|
101
|
+
path: doc.path,
|
|
102
|
+
}
|
|
103
|
+
})
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
// Resolve a document's placement state in the tree (unplaced / root / child)
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
|
|
110
|
+
export const getTreeParent = createServerFn({ method: 'GET' })
|
|
111
|
+
.validator((input: { collection: string; documentId: string }) => input)
|
|
112
|
+
.handler(async ({ data }) => {
|
|
113
|
+
const { collection: path, documentId } = data
|
|
114
|
+
const config = await ensureCollection(path)
|
|
115
|
+
if (!config) {
|
|
116
|
+
throw ERR_NOT_FOUND({
|
|
117
|
+
message: 'Collection not found',
|
|
118
|
+
details: { collectionPath: path },
|
|
119
|
+
}).log(getLogger())
|
|
120
|
+
}
|
|
121
|
+
return getAdminBylineClient().collection(path).getTreeParent(documentId)
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
// ---------------------------------------------------------------------------
|
|
125
|
+
// Read the whole collection tree as ordered, depth-tagged rows for the built-in
|
|
126
|
+
// tree list view. Placed nodes come first (pre-order, root-first), then any
|
|
127
|
+
// *unplaced* documents (created but not yet positioned) so nothing is
|
|
128
|
+
// unreachable from the list. Admin reads use `status: 'any'`.
|
|
129
|
+
// ---------------------------------------------------------------------------
|
|
130
|
+
|
|
131
|
+
export interface CollectionTreeRow {
|
|
132
|
+
id: string
|
|
133
|
+
/** Parent document id, or `null` for a root or unplaced node. Drives the
|
|
134
|
+
* client-side tree reconstruction for drag-to-reorder / re-parent. */
|
|
135
|
+
parentId: string | null
|
|
136
|
+
depth: number
|
|
137
|
+
unplaced: boolean
|
|
138
|
+
status: string
|
|
139
|
+
path: string
|
|
140
|
+
createdAt: Date
|
|
141
|
+
updatedAt: Date
|
|
142
|
+
fields: Record<string, any>
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export const getCollectionTree = createServerFn({ method: 'GET' })
|
|
146
|
+
.validator((input: { collection: string; locale?: string }) => input)
|
|
147
|
+
.handler(async ({ data }) => {
|
|
148
|
+
const { collection: path } = data
|
|
149
|
+
const config = await ensureCollection(path)
|
|
150
|
+
if (!config) {
|
|
151
|
+
throw ERR_NOT_FOUND({
|
|
152
|
+
message: 'Collection not found',
|
|
153
|
+
details: { collectionPath: path },
|
|
154
|
+
}).log(getLogger())
|
|
155
|
+
}
|
|
156
|
+
const handle = getAdminBylineClient().collection(path)
|
|
157
|
+
|
|
158
|
+
const forest = await handle.getSubtree({ status: 'any', locale: data.locale })
|
|
159
|
+
const rows: CollectionTreeRow[] = []
|
|
160
|
+
const placed = new Set<string>()
|
|
161
|
+
const walk = (nodes: typeof forest, depth: number, parentId: string | null): void => {
|
|
162
|
+
for (const node of nodes) {
|
|
163
|
+
const doc = node.document
|
|
164
|
+
rows.push({
|
|
165
|
+
id: doc.id,
|
|
166
|
+
parentId,
|
|
167
|
+
depth,
|
|
168
|
+
unplaced: false,
|
|
169
|
+
status: doc.status,
|
|
170
|
+
path: doc.path,
|
|
171
|
+
createdAt: doc.createdAt,
|
|
172
|
+
updatedAt: doc.updatedAt,
|
|
173
|
+
fields: doc.fields as Record<string, any>,
|
|
174
|
+
})
|
|
175
|
+
placed.add(doc.id)
|
|
176
|
+
walk(node.children, depth + 1, doc.id)
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
walk(forest, 0, null)
|
|
180
|
+
|
|
181
|
+
// Surface documents not yet in the tree (e.g. freshly created) so they
|
|
182
|
+
// remain reachable. Trees are small by design, so a single wide read is fine.
|
|
183
|
+
const all = await handle.find({ status: 'any', pageSize: 1000, _bypassBeforeRead: true })
|
|
184
|
+
for (const doc of all.docs) {
|
|
185
|
+
if (placed.has(doc.id)) continue
|
|
186
|
+
rows.push({
|
|
187
|
+
id: doc.id,
|
|
188
|
+
parentId: null,
|
|
189
|
+
depth: 0,
|
|
190
|
+
unplaced: true,
|
|
191
|
+
status: doc.status,
|
|
192
|
+
path: doc.path,
|
|
193
|
+
createdAt: doc.createdAt,
|
|
194
|
+
updatedAt: doc.updatedAt,
|
|
195
|
+
fields: doc.fields as Record<string, any>,
|
|
196
|
+
})
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
rows,
|
|
201
|
+
included: { collection: { path: config.collection.path, labels: config.definition.labels } },
|
|
202
|
+
}
|
|
203
|
+
})
|