@byline/host-tanstack-start 3.8.0 → 3.10.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/document-history.d.ts +53 -0
- package/dist/admin-shell/collections/document-history.js +103 -0
- package/dist/admin-shell/collections/document-history.module.js +9 -0
- package/dist/admin-shell/collections/document-history_module.css +28 -0
- package/dist/admin-shell/collections/history.d.ts +8 -1
- package/dist/admin-shell/collections/history.js +73 -29
- package/dist/admin-shell/collections/history.module.js +1 -0
- package/dist/admin-shell/collections/history_module.css +4 -0
- package/dist/routes/create-collection-history-route.js +23 -6
- package/dist/server-fns/collections/audit.d.ts +50 -0
- package/dist/server-fns/collections/audit.js +40 -0
- package/dist/server-fns/collections/index.d.ts +1 -0
- package/dist/server-fns/collections/index.js +1 -0
- package/package.json +8 -8
- package/src/admin-shell/collections/document-history.module.css +46 -0
- package/src/admin-shell/collections/document-history.tsx +156 -0
- package/src/admin-shell/collections/history.module.css +6 -0
- package/src/admin-shell/collections/history.tsx +331 -281
- package/src/routes/create-collection-history-route.tsx +25 -3
- package/src/server-fns/collections/audit.ts +97 -0
- package/src/server-fns/collections/index.ts +1 -0
|
@@ -21,6 +21,7 @@ import { BreadcrumbsClient } from '../admin-shell/chrome/breadcrumbs/breadcrumbs
|
|
|
21
21
|
import { HistoryView } from '../admin-shell/collections/history.js'
|
|
22
22
|
import {
|
|
23
23
|
getCollectionDocument,
|
|
24
|
+
getCollectionDocumentAuditLog,
|
|
24
25
|
getCollectionDocumentHistory,
|
|
25
26
|
} from '../server-fns/collections/index.js'
|
|
26
27
|
import type { ContentLocaleOption } from '../admin-shell/collections/view-menu.js'
|
|
@@ -31,8 +32,17 @@ const searchSchema = z.object({
|
|
|
31
32
|
order: z.string().optional(),
|
|
32
33
|
desc: z.coerce.boolean().optional(),
|
|
33
34
|
locale: z.string().optional(),
|
|
35
|
+
// Which sub-view of the history page is active (docs/AUDIT.md — Workstream
|
|
36
|
+
// 3). 'versions' is the content version stream; 'document' is the
|
|
37
|
+
// document-grain audit log. Absent → 'versions'.
|
|
38
|
+
tab: z.enum(['versions', 'document']).optional(),
|
|
34
39
|
})
|
|
35
40
|
|
|
41
|
+
// The per-document audit log is small and bounded (path / locale / status
|
|
42
|
+
// changes + the deletion event), so v1 fetches a single generous page rather
|
|
43
|
+
// than wiring a second, tab-specific pager into the shared history route.
|
|
44
|
+
const AUDIT_LOG_PAGE_SIZE = 100
|
|
45
|
+
|
|
36
46
|
interface CollectionHistoryOpts {
|
|
37
47
|
contentLocales: ReadonlyArray<ContentLocaleOption>
|
|
38
48
|
defaultContentLocale: string
|
|
@@ -67,7 +77,7 @@ export function createCollectionHistoryRoute(path: string, opts: CollectionHisto
|
|
|
67
77
|
throw notFound()
|
|
68
78
|
}
|
|
69
79
|
|
|
70
|
-
const [history, currentDocument] = await Promise.all([
|
|
80
|
+
const [history, currentDocument, auditLog] = await Promise.all([
|
|
71
81
|
getCollectionDocumentHistory({
|
|
72
82
|
data: {
|
|
73
83
|
collection: params.collection,
|
|
@@ -84,15 +94,26 @@ export function createCollectionHistoryRoute(path: string, opts: CollectionHisto
|
|
|
84
94
|
// Fetch the current document with the same locale (or 'all') so diffs
|
|
85
95
|
// compare the same shape as what the user is viewing.
|
|
86
96
|
getCollectionDocument(params.collection, params.id, deps.locale ?? 'all'),
|
|
97
|
+
// Document-grain audit log for the "Document history" tab (W3). Fetched
|
|
98
|
+
// in parallel and unconditionally — it's cheap, and the active tab is a
|
|
99
|
+
// pure render concern read from the URL, so switching tabs never
|
|
100
|
+
// refetches.
|
|
101
|
+
getCollectionDocumentAuditLog({
|
|
102
|
+
data: {
|
|
103
|
+
collection: params.collection,
|
|
104
|
+
id: params.id,
|
|
105
|
+
params: { page: 1, page_size: AUDIT_LOG_PAGE_SIZE },
|
|
106
|
+
},
|
|
107
|
+
}),
|
|
87
108
|
])
|
|
88
109
|
|
|
89
|
-
return { history, currentDocument }
|
|
110
|
+
return { history, currentDocument, auditLog }
|
|
90
111
|
},
|
|
91
112
|
staleTime: 0,
|
|
92
113
|
gcTime: 0,
|
|
93
114
|
shouldReload: true,
|
|
94
115
|
component: function CollectionHistoryComponent() {
|
|
95
|
-
const { history, currentDocument } = Route.useLoaderData()
|
|
116
|
+
const { history, currentDocument, auditLog } = Route.useLoaderData()
|
|
96
117
|
const { collection } = Route.useParams() as { collection: string; id: string }
|
|
97
118
|
const collectionDef = getCollectionDefinition(collection) as CollectionDefinition
|
|
98
119
|
const adminConfig = getCollectionAdminConfig(collection)
|
|
@@ -119,6 +140,7 @@ export function createCollectionHistoryRoute(path: string, opts: CollectionHisto
|
|
|
119
140
|
workflowStatuses={getWorkflowStatuses(collectionDef)}
|
|
120
141
|
adminConfig={adminConfig ?? undefined}
|
|
121
142
|
data={history}
|
|
143
|
+
auditLog={auditLog}
|
|
122
144
|
currentDocument={currentDocument as Record<string, unknown> | null}
|
|
123
145
|
contentLocales={opts.contentLocales}
|
|
124
146
|
defaultContentLocale={opts.defaultContentLocale}
|
|
@@ -0,0 +1,97 @@
|
|
|
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 { ERR_NOT_FOUND, getLogger } from '@byline/core'
|
|
12
|
+
|
|
13
|
+
import { ensureCollection } from '../../integrations/api-utils.js'
|
|
14
|
+
import { getAdminBylineClient } from '../../integrations/byline-client.js'
|
|
15
|
+
import { type ActorLabelMap, resolveActorLabels } from './actors.js'
|
|
16
|
+
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// Shared param types
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
export interface AuditLogSearchParams {
|
|
22
|
+
page?: number
|
|
23
|
+
page_size?: number
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Audit entry as it crosses the server-fn boundary. `occurredAt` is an ISO
|
|
28
|
+
* string (Date doesn't survive serialization) and `before` / `after` are
|
|
29
|
+
* narrowed from the storage layer's `unknown` jsonb to the concrete shapes the
|
|
30
|
+
* shipped actions actually carry (path/status strings, the available-locales
|
|
31
|
+
* array, or null for the deletion event) — `unknown` is not a serializable
|
|
32
|
+
* type the TanStack server-fn validator accepts.
|
|
33
|
+
*/
|
|
34
|
+
export interface AuditLogEntryDto {
|
|
35
|
+
id: string
|
|
36
|
+
documentId: string | null
|
|
37
|
+
collectionId: string | null
|
|
38
|
+
actorId: string | null
|
|
39
|
+
actorRealm: string
|
|
40
|
+
action: string
|
|
41
|
+
field: string | null
|
|
42
|
+
before: string | string[] | null
|
|
43
|
+
after: string | string[] | null
|
|
44
|
+
occurredAt: string
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
// Get document-grain audit log (docs/AUDIT.md — Workstream 3)
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
export const getCollectionDocumentAuditLog = createServerFn({ method: 'GET' })
|
|
52
|
+
.inputValidator(
|
|
53
|
+
(input: { collection: string; id: string; params?: AuditLogSearchParams }) => input
|
|
54
|
+
)
|
|
55
|
+
.handler(async ({ data }) => {
|
|
56
|
+
const { collection: path, id, params } = data
|
|
57
|
+
const config = await ensureCollection(path)
|
|
58
|
+
if (!config) {
|
|
59
|
+
throw ERR_NOT_FOUND({
|
|
60
|
+
message: 'Collection not found',
|
|
61
|
+
details: { collectionPath: path },
|
|
62
|
+
}).log(getLogger())
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Routes through CollectionHandle.auditLog so the document's own read gate
|
|
66
|
+
// (`beforeRead` via `findById`) is applied — identical to the history
|
|
67
|
+
// server fn. An actor whose predicate excludes the document gets an empty
|
|
68
|
+
// log rather than leaked change metadata.
|
|
69
|
+
const result = await getAdminBylineClient().collection(path).auditLog(id, {
|
|
70
|
+
page: params?.page,
|
|
71
|
+
pageSize: params?.page_size,
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
// Acting-user labels for the audit list (docs/AUDIT.md — W3). Resolved
|
|
75
|
+
// here, in the admin realm, from each entry's raw `actorId`; the UI joins
|
|
76
|
+
// by id. System/tooling rows (NULL actorId) and deleted users are absent
|
|
77
|
+
// from the map — the UI renders the corresponding tombstone label.
|
|
78
|
+
const actors: ActorLabelMap = await resolveActorLabels(result.entries.map((e) => e.actorId))
|
|
79
|
+
|
|
80
|
+
// Map to the serializable DTO: ISO-string the timestamp and narrow the
|
|
81
|
+
// jsonb before/after off `unknown` so the value survives the TanStack
|
|
82
|
+
// server-fn boundary.
|
|
83
|
+
const entries: AuditLogEntryDto[] = result.entries.map((e) => ({
|
|
84
|
+
id: e.id,
|
|
85
|
+
documentId: e.documentId,
|
|
86
|
+
collectionId: e.collectionId,
|
|
87
|
+
actorId: e.actorId,
|
|
88
|
+
actorRealm: e.actorRealm,
|
|
89
|
+
action: e.action,
|
|
90
|
+
field: e.field,
|
|
91
|
+
before: e.before as string | string[] | null,
|
|
92
|
+
after: e.after as string | string[] | null,
|
|
93
|
+
occurredAt: e.occurredAt instanceof Date ? e.occurredAt.toISOString() : String(e.occurredAt),
|
|
94
|
+
}))
|
|
95
|
+
|
|
96
|
+
return { entries, meta: result.meta, actors }
|
|
97
|
+
})
|