@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
|
@@ -0,0 +1,166 @@
|
|
|
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 { createServerFn } from '@tanstack/react-start'
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
assertActorCanPerform,
|
|
13
|
+
ERR_NOT_FOUND,
|
|
14
|
+
ERR_VALIDATION,
|
|
15
|
+
generateKeyBetween,
|
|
16
|
+
generateNKeysBetween,
|
|
17
|
+
getCollectionAdminConfig,
|
|
18
|
+
getLogger,
|
|
19
|
+
getServerConfig,
|
|
20
|
+
} from '@byline/core'
|
|
21
|
+
|
|
22
|
+
import { getAdminRequestContext } from '../../auth/auth-context.js'
|
|
23
|
+
import { ensureCollection } from '../../integrations/api-utils.js'
|
|
24
|
+
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// Reorder a single document within an `orderable: true` collection.
|
|
27
|
+
//
|
|
28
|
+
// One of `beforeDocumentId` / `afterDocumentId` should be provided — they
|
|
29
|
+
// identify the neighbours the dragged row should land between. Either may
|
|
30
|
+
// be null:
|
|
31
|
+
// - both null → empty collection or "append-to-end" no-op (writes
|
|
32
|
+
// a fresh key)
|
|
33
|
+
// - beforeId set, afterId null → append after `beforeId`
|
|
34
|
+
// - beforeId null, afterId set → prepend before `afterId`
|
|
35
|
+
//
|
|
36
|
+
// Writes a single column on `byline_documents` and does NOT create a new
|
|
37
|
+
// document version. Goes through the `collections.<path>.update` ability;
|
|
38
|
+
// reordering is metadata-level update, not a new ability slug.
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
export const reorderCollectionDocument = createServerFn({ method: 'POST' })
|
|
42
|
+
.inputValidator(
|
|
43
|
+
(input: {
|
|
44
|
+
collection: string
|
|
45
|
+
documentId: string
|
|
46
|
+
beforeDocumentId?: string | null
|
|
47
|
+
afterDocumentId?: string | null
|
|
48
|
+
}) => input
|
|
49
|
+
)
|
|
50
|
+
.handler(async ({ data: input }) => {
|
|
51
|
+
const { collection: path, documentId, beforeDocumentId, afterDocumentId } = input
|
|
52
|
+
const logger = getLogger()
|
|
53
|
+
|
|
54
|
+
const config = await ensureCollection(path)
|
|
55
|
+
if (!config) {
|
|
56
|
+
throw ERR_NOT_FOUND({
|
|
57
|
+
message: 'Collection not found',
|
|
58
|
+
details: { collectionPath: path },
|
|
59
|
+
}).log(logger)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const adminConfig = getCollectionAdminConfig(path)
|
|
63
|
+
if (adminConfig?.orderable !== true) {
|
|
64
|
+
throw ERR_VALIDATION({
|
|
65
|
+
message: `collection '${path}' is not orderable; set \`orderable: true\` on its admin config to enable reordering`,
|
|
66
|
+
details: { collectionPath: path },
|
|
67
|
+
}).log(logger)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const requestContext = await getAdminRequestContext()
|
|
71
|
+
assertActorCanPerform(requestContext, path, 'update')
|
|
72
|
+
|
|
73
|
+
const serverConfig = getServerConfig()
|
|
74
|
+
const collectionId = config.collection.id
|
|
75
|
+
|
|
76
|
+
// Read the whole collection in canonical display order. Small by design
|
|
77
|
+
// for orderable use cases (bios, FAQs, sections), so a full read is
|
|
78
|
+
// cheap — and it gives us a single, consistent snapshot to reason about
|
|
79
|
+
// both backfill and recovery in one pass.
|
|
80
|
+
const canonical = await serverConfig.db.queries.documents.getCanonicalDocumentOrder({
|
|
81
|
+
collection_id: collectionId,
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
// Detect pathological key state — duplicates or non-ascending runs of
|
|
85
|
+
// keyed rows. These can land in the DB from prior buggy code paths
|
|
86
|
+
// or interrupted writes; once present, `generateKeyBetween` throws
|
|
87
|
+
// with `a >= b`. When detected, re-key the entire collection in the
|
|
88
|
+
// editor's current visible order so subsequent operations work from
|
|
89
|
+
// a clean baseline.
|
|
90
|
+
let corrupted = false
|
|
91
|
+
{
|
|
92
|
+
const seen = new Set<string>()
|
|
93
|
+
let lastKey: string | null = null
|
|
94
|
+
for (const doc of canonical) {
|
|
95
|
+
if (doc.order_key == null) continue
|
|
96
|
+
if (seen.has(doc.order_key)) {
|
|
97
|
+
corrupted = true
|
|
98
|
+
break
|
|
99
|
+
}
|
|
100
|
+
if (lastKey != null && doc.order_key <= lastKey) {
|
|
101
|
+
corrupted = true
|
|
102
|
+
break
|
|
103
|
+
}
|
|
104
|
+
seen.add(doc.order_key)
|
|
105
|
+
lastKey = doc.order_key
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (corrupted) {
|
|
110
|
+
const allKeys = generateNKeysBetween(null, null, canonical.length)
|
|
111
|
+
for (let i = 0; i < canonical.length; i++) {
|
|
112
|
+
await serverConfig.db.commands.documents.setOrderKey({
|
|
113
|
+
document_id: canonical[i]?.id,
|
|
114
|
+
order_key: allKeys[i]!,
|
|
115
|
+
})
|
|
116
|
+
}
|
|
117
|
+
} else {
|
|
118
|
+
// Happy path — backfill trailing NULLs after the largest existing
|
|
119
|
+
// key. After canonical sort the NULL rows are always contiguous at
|
|
120
|
+
// the tail (`NULLS LAST`).
|
|
121
|
+
const firstNullIdx = canonical.findIndex((d) => d.order_key == null)
|
|
122
|
+
if (firstNullIdx !== -1) {
|
|
123
|
+
const nullDocs = canonical.slice(firstNullIdx)
|
|
124
|
+
const lastExistingKey = firstNullIdx === 0 ? null : canonical[firstNullIdx - 1]?.order_key
|
|
125
|
+
const newKeys = generateNKeysBetween(lastExistingKey, null, nullDocs.length)
|
|
126
|
+
for (let i = 0; i < nullDocs.length; i++) {
|
|
127
|
+
await serverConfig.db.commands.documents.setOrderKey({
|
|
128
|
+
document_id: nullDocs[i]?.id,
|
|
129
|
+
order_key: newKeys[i]!,
|
|
130
|
+
})
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Keys are now clean — resolve neighbours and place the moved row.
|
|
136
|
+
const { left, right } = await serverConfig.db.queries.documents.getNeighborOrderKeys({
|
|
137
|
+
collection_id: collectionId,
|
|
138
|
+
before_document_id: beforeDocumentId ?? null,
|
|
139
|
+
after_document_id: afterDocumentId ?? null,
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
let newKey: string
|
|
143
|
+
try {
|
|
144
|
+
newKey = generateKeyBetween(left, right)
|
|
145
|
+
} catch (err) {
|
|
146
|
+
throw ERR_VALIDATION({
|
|
147
|
+
message: 'cannot generate order_key between supplied neighbors',
|
|
148
|
+
details: {
|
|
149
|
+
collectionPath: path,
|
|
150
|
+
documentId,
|
|
151
|
+
beforeDocumentId,
|
|
152
|
+
afterDocumentId,
|
|
153
|
+
left,
|
|
154
|
+
right,
|
|
155
|
+
cause: err instanceof Error ? err.message : String(err),
|
|
156
|
+
},
|
|
157
|
+
}).log(logger)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
await serverConfig.db.commands.documents.setOrderKey({
|
|
161
|
+
document_id: documentId,
|
|
162
|
+
order_key: newKey,
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
return { status: 'ok' as const, orderKey: newKey }
|
|
166
|
+
})
|