@byline/host-tanstack-start 3.7.0 → 3.9.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/history.d.ts +10 -1
- package/dist/admin-shell/collections/history.js +111 -66
- package/dist/admin-shell/collections/history.module.js +3 -0
- package/dist/admin-shell/collections/history_module.css +37 -1
- package/dist/server-fns/collections/actors.d.ts +17 -0
- package/dist/server-fns/collections/actors.js +25 -0
- package/dist/server-fns/collections/history.d.ts +4 -0
- package/dist/server-fns/collections/history.js +10 -2
- package/dist/server-fns/collections/list.d.ts +2 -0
- package/package.json +8 -8
- package/src/admin-shell/collections/history.module.css +54 -1
- package/src/admin-shell/collections/history.tsx +217 -143
- package/src/server-fns/collections/actors.ts +60 -0
- package/src/server-fns/collections/history.ts +11 -2
|
@@ -11,7 +11,16 @@ import type { ContentLocaleOption } from './view-menu.js';
|
|
|
11
11
|
export declare const HistoryView: ({ collectionDefinition, adminConfig, data, workflowStatuses, currentDocument, contentLocales, defaultContentLocale, }: {
|
|
12
12
|
collectionDefinition: CollectionDefinition;
|
|
13
13
|
adminConfig?: CollectionAdminConfig;
|
|
14
|
-
data: AnyCollectionSchemaTypes["HistoryType"]
|
|
14
|
+
data: AnyCollectionSchemaTypes["HistoryType"] & {
|
|
15
|
+
/**
|
|
16
|
+
* Audit display labels (the acting user per version), resolved admin-side from each
|
|
17
|
+
* version's `createdBy` id (see docs/AUDIT.md — Workstream 1). Ids
|
|
18
|
+
* absent from the map belong to deleted users.
|
|
19
|
+
*/
|
|
20
|
+
actors?: Record<string, {
|
|
21
|
+
label: string;
|
|
22
|
+
}>;
|
|
23
|
+
};
|
|
15
24
|
workflowStatuses?: WorkflowStatus[];
|
|
16
25
|
currentDocument?: Record<string, unknown> | null;
|
|
17
26
|
contentLocales: ReadonlyArray<ContentLocaleOption>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { Suspense, lazy, useState } from "react";
|
|
2
|
+
import { Fragment as external_react_Fragment, Suspense, lazy, useState } from "react";
|
|
3
3
|
import { useParams, useRouterState } from "@tanstack/react-router";
|
|
4
4
|
import { StatusBadge, renderFormatted } from "@byline/admin/react";
|
|
5
5
|
import { useBylineAdminServices } from "@byline/admin/services";
|
|
@@ -43,6 +43,13 @@ function padRows(value) {
|
|
|
43
43
|
children: "\xa0"
|
|
44
44
|
}, `empty-row-${index}`));
|
|
45
45
|
}
|
|
46
|
+
const AUDIT_ACTION_KEYS = {
|
|
47
|
+
create: 'collections.history.audit.actionCreate',
|
|
48
|
+
update: 'collections.history.audit.actionUpdate',
|
|
49
|
+
restore: 'collections.history.audit.actionRestore',
|
|
50
|
+
copy_to_locale: 'collections.history.audit.actionCopyToLocale',
|
|
51
|
+
delete_locale: 'collections.history.audit.actionDeleteLocale'
|
|
52
|
+
};
|
|
46
53
|
const HistoryView = ({ collectionDefinition, adminConfig, data, workflowStatuses, currentDocument, contentLocales, defaultContentLocale })=>{
|
|
47
54
|
const { id, collection } = useParams({
|
|
48
55
|
from: '/_byline/admin/collections/$collection/$id/history'
|
|
@@ -52,6 +59,7 @@ const HistoryView = ({ collectionDefinition, adminConfig, data, workflowStatuses
|
|
|
52
59
|
const { t } = useTranslation('byline-admin');
|
|
53
60
|
const columns = adminConfig?.columns || [];
|
|
54
61
|
const { labels } = collectionDefinition;
|
|
62
|
+
const titleFieldName = collectionDefinition.useAsTitle;
|
|
55
63
|
const location = useRouterState({
|
|
56
64
|
select: (s)=>s.location
|
|
57
65
|
});
|
|
@@ -65,9 +73,10 @@ const HistoryView = ({ collectionDefinition, adminConfig, data, workflowStatuses
|
|
|
65
73
|
delete params.page;
|
|
66
74
|
params.page_size = Number.parseInt(value, 10);
|
|
67
75
|
navigate({
|
|
68
|
-
to: '/admin/collections/$collection',
|
|
76
|
+
to: '/admin/collections/$collection/$id/history',
|
|
69
77
|
params: {
|
|
70
|
-
collection
|
|
78
|
+
collection,
|
|
79
|
+
id
|
|
71
80
|
},
|
|
72
81
|
search: params
|
|
73
82
|
});
|
|
@@ -138,7 +147,7 @@ const HistoryView = ({ collectionDefinition, adminConfig, data, workflowStatuses
|
|
|
138
147
|
align: column.align,
|
|
139
148
|
className: column.className
|
|
140
149
|
}, String(column.fieldName));
|
|
141
|
-
if (
|
|
150
|
+
if (null != titleFieldName && column.fieldName === titleFieldName) return [
|
|
142
151
|
cell,
|
|
143
152
|
/*#__PURE__*/ jsx("th", {
|
|
144
153
|
scope: "col",
|
|
@@ -157,75 +166,111 @@ const HistoryView = ({ collectionDefinition, adminConfig, data, workflowStatuses
|
|
|
157
166
|
const versionId = document.versionId;
|
|
158
167
|
const { total, page, pageSize, desc } = data.meta;
|
|
159
168
|
const versionNumber = desc ? total - (page - 1) * pageSize - rowIndex : (page - 1) * pageSize + rowIndex + 1;
|
|
160
|
-
|
|
169
|
+
const actorLabel = document.createdBy ? data.actors?.[document.createdBy]?.label ?? t('collections.history.audit.formerUser') : t('collections.history.audit.unknown');
|
|
170
|
+
const actionKey = document.eventType ? AUDIT_ACTION_KEYS[document.eventType] : void 0;
|
|
171
|
+
const actionLabel = actionKey ? t(actionKey) : document.eventType ?? '';
|
|
172
|
+
const auditColSpan = columns.length + (null != titleFieldName && columns.some((c)=>c.fieldName === titleFieldName) ? 1 : 0);
|
|
173
|
+
return /*#__PURE__*/ jsxs(external_react_Fragment, {
|
|
161
174
|
children: [
|
|
162
|
-
/*#__PURE__*/
|
|
163
|
-
className: classnames('byline-coll-history-
|
|
164
|
-
children:
|
|
165
|
-
size: "xs",
|
|
166
|
-
variant: "outlined",
|
|
167
|
-
intent: "noeffect",
|
|
168
|
-
"aria-label": t('collections.history.compareAriaLabel'),
|
|
169
|
-
title: t('collections.history.compareTitle'),
|
|
170
|
-
className: classnames('byline-coll-history-version-button', history_module.versionButton),
|
|
171
|
-
onClick: ()=>setSelectedVersion({
|
|
172
|
-
versionId,
|
|
173
|
-
label: new Date(document.createdAt).toLocaleString()
|
|
174
|
-
}),
|
|
175
|
-
children: versionNumber
|
|
176
|
-
}) : null
|
|
177
|
-
}),
|
|
178
|
-
columns.flatMap((column)=>{
|
|
179
|
-
const dataCell = /*#__PURE__*/ jsx(Table.Cell, {
|
|
180
|
-
className: classnames({
|
|
181
|
-
'byline-coll-history-cell-right': 'right' === column.align,
|
|
182
|
-
[history_module.cellRight]: 'right' === column.align,
|
|
183
|
-
'byline-coll-history-cell-center': 'center' === column.align,
|
|
184
|
-
[history_module.cellCenter]: 'center' === column.align
|
|
185
|
-
}),
|
|
186
|
-
children: 'title' === column.fieldName ? versionId && currentDocument ? /*#__PURE__*/ jsx("button", {
|
|
187
|
-
type: "button",
|
|
188
|
-
className: classnames('byline-coll-history-title-button', history_module.titleButton),
|
|
189
|
-
onClick: ()=>setSelectedVersion({
|
|
190
|
-
versionId,
|
|
191
|
-
label: new Date(document.createdAt).toLocaleString()
|
|
192
|
-
}),
|
|
193
|
-
children: column.formatter ? renderFormatted(getColumnValue(document, column.fieldName), document, column.formatter) : resolveDisplayValue(getColumnValue(document, column.fieldName), locale, defaultContentLocale) || '------'
|
|
194
|
-
}) : /*#__PURE__*/ jsx(Link, {
|
|
195
|
-
to: '/admin/collections/$collection/$id',
|
|
196
|
-
params: {
|
|
197
|
-
collection,
|
|
198
|
-
id: document.id
|
|
199
|
-
},
|
|
200
|
-
children: column.formatter ? renderFormatted(getColumnValue(document, column.fieldName), document, column.formatter) : resolveDisplayValue(getColumnValue(document, column.fieldName), locale, defaultContentLocale) || '------'
|
|
201
|
-
}) : column.formatter ? renderFormatted(getColumnValue(document, column.fieldName), document, column.formatter) : 'status' === column.fieldName && workflowStatuses ? /*#__PURE__*/ jsx(StatusBadge, {
|
|
202
|
-
status: document.status,
|
|
203
|
-
workflowStatuses: workflowStatuses
|
|
204
|
-
}) : resolveDisplayValue(getColumnValue(document, column.fieldName), locale, defaultContentLocale) || ''
|
|
205
|
-
}, String(column.fieldName));
|
|
206
|
-
if ('title' === column.fieldName) return [
|
|
207
|
-
dataCell,
|
|
175
|
+
/*#__PURE__*/ jsxs(Table.Row, {
|
|
176
|
+
className: classnames('byline-coll-history-row', history_module.historyRow),
|
|
177
|
+
children: [
|
|
208
178
|
/*#__PURE__*/ jsx(Table.Cell, {
|
|
209
|
-
className: classnames('byline-coll-history-
|
|
210
|
-
children: versionId &&
|
|
211
|
-
type: "button",
|
|
212
|
-
variant: "outlined",
|
|
179
|
+
className: classnames('byline-coll-history-version-cell', history_module.versionCell),
|
|
180
|
+
children: versionId && currentDocument ? /*#__PURE__*/ jsx(IconButton, {
|
|
213
181
|
size: "xs",
|
|
182
|
+
variant: "outlined",
|
|
214
183
|
intent: "noeffect",
|
|
215
|
-
|
|
184
|
+
"aria-label": t('collections.history.compareAriaLabel'),
|
|
185
|
+
title: t('collections.history.compareTitle'),
|
|
186
|
+
className: classnames('byline-coll-history-version-button', history_module.versionButton),
|
|
187
|
+
onClick: ()=>setSelectedVersion({
|
|
216
188
|
versionId,
|
|
217
|
-
label: new Date(document.createdAt).toLocaleString()
|
|
218
|
-
versionNumber
|
|
189
|
+
label: new Date(document.createdAt).toLocaleString()
|
|
219
190
|
}),
|
|
220
|
-
|
|
221
|
-
title: t('collections.history.restoreButtonTitle'),
|
|
222
|
-
children: t('collections.history.restoreButton')
|
|
191
|
+
children: versionNumber
|
|
223
192
|
}) : null
|
|
224
|
-
},
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
193
|
+
}),
|
|
194
|
+
columns.flatMap((column)=>{
|
|
195
|
+
const dataCell = /*#__PURE__*/ jsx(Table.Cell, {
|
|
196
|
+
className: classnames({
|
|
197
|
+
'byline-coll-history-cell-right': 'right' === column.align,
|
|
198
|
+
[history_module.cellRight]: 'right' === column.align,
|
|
199
|
+
'byline-coll-history-cell-center': 'center' === column.align,
|
|
200
|
+
[history_module.cellCenter]: 'center' === column.align
|
|
201
|
+
}),
|
|
202
|
+
children: null != titleFieldName && column.fieldName === titleFieldName ? versionId && currentDocument ? /*#__PURE__*/ jsx("button", {
|
|
203
|
+
type: "button",
|
|
204
|
+
className: classnames('byline-coll-history-title-button', history_module.titleButton),
|
|
205
|
+
onClick: ()=>setSelectedVersion({
|
|
206
|
+
versionId,
|
|
207
|
+
label: new Date(document.createdAt).toLocaleString()
|
|
208
|
+
}),
|
|
209
|
+
children: column.formatter ? renderFormatted(getColumnValue(document, column.fieldName), document, column.formatter) : resolveDisplayValue(getColumnValue(document, column.fieldName), locale, defaultContentLocale) || '------'
|
|
210
|
+
}) : /*#__PURE__*/ jsx(Link, {
|
|
211
|
+
to: '/admin/collections/$collection/$id',
|
|
212
|
+
params: {
|
|
213
|
+
collection,
|
|
214
|
+
id: document.id
|
|
215
|
+
},
|
|
216
|
+
children: column.formatter ? renderFormatted(getColumnValue(document, column.fieldName), document, column.formatter) : resolveDisplayValue(getColumnValue(document, column.fieldName), locale, defaultContentLocale) || '------'
|
|
217
|
+
}) : column.formatter ? renderFormatted(getColumnValue(document, column.fieldName), document, column.formatter) : 'status' === column.fieldName && workflowStatuses ? /*#__PURE__*/ jsx(StatusBadge, {
|
|
218
|
+
status: document.status,
|
|
219
|
+
workflowStatuses: workflowStatuses
|
|
220
|
+
}) : resolveDisplayValue(getColumnValue(document, column.fieldName), locale, defaultContentLocale) || ''
|
|
221
|
+
}, String(column.fieldName));
|
|
222
|
+
if (null != titleFieldName && column.fieldName === titleFieldName) return [
|
|
223
|
+
dataCell,
|
|
224
|
+
/*#__PURE__*/ jsx(Table.Cell, {
|
|
225
|
+
className: classnames('byline-coll-history-restore-cell', history_module.restoreCell),
|
|
226
|
+
children: versionId && versionId !== currentVersionId ? /*#__PURE__*/ jsx(Button, {
|
|
227
|
+
type: "button",
|
|
228
|
+
variant: "outlined",
|
|
229
|
+
size: "xs",
|
|
230
|
+
intent: "noeffect",
|
|
231
|
+
onClick: ()=>setRestoreTarget({
|
|
232
|
+
versionId,
|
|
233
|
+
label: new Date(document.createdAt).toLocaleString(),
|
|
234
|
+
versionNumber
|
|
235
|
+
}),
|
|
236
|
+
className: classnames('byline-coll-history-restore-button', history_module.restoreButton),
|
|
237
|
+
title: t('collections.history.restoreButtonTitle'),
|
|
238
|
+
children: t('collections.history.restoreButton')
|
|
239
|
+
}) : null
|
|
240
|
+
}, "__restore")
|
|
241
|
+
];
|
|
242
|
+
return [
|
|
243
|
+
dataCell
|
|
244
|
+
];
|
|
245
|
+
})
|
|
246
|
+
]
|
|
247
|
+
}),
|
|
248
|
+
/*#__PURE__*/ jsxs(Table.Row, {
|
|
249
|
+
className: classnames('byline-coll-history-audit-row', history_module.auditRow),
|
|
250
|
+
"aria-label": t('collections.history.audit.createdBy', {
|
|
251
|
+
label: actorLabel
|
|
252
|
+
}),
|
|
253
|
+
children: [
|
|
254
|
+
/*#__PURE__*/ jsx(Table.Cell, {
|
|
255
|
+
className: classnames('byline-coll-history-audit-spacer-cell', history_module.auditSpacerCell)
|
|
256
|
+
}),
|
|
257
|
+
/*#__PURE__*/ jsx(Table.Cell, {
|
|
258
|
+
colSpan: auditColSpan,
|
|
259
|
+
className: classnames('byline-coll-history-audit-cell', history_module.auditCell),
|
|
260
|
+
children: /*#__PURE__*/ jsxs("span", {
|
|
261
|
+
className: classnames('byline-coll-history-audit', history_module.audit),
|
|
262
|
+
children: [
|
|
263
|
+
actionLabel,
|
|
264
|
+
' · ',
|
|
265
|
+
t('collections.history.audit.createdBy', {
|
|
266
|
+
label: actorLabel
|
|
267
|
+
}),
|
|
268
|
+
' · ',
|
|
269
|
+
new Date(document.createdAt).toLocaleString()
|
|
270
|
+
]
|
|
271
|
+
})
|
|
272
|
+
})
|
|
273
|
+
]
|
|
229
274
|
})
|
|
230
275
|
]
|
|
231
276
|
}, versionId ?? document.id);
|
|
@@ -15,6 +15,9 @@ const history_module = {
|
|
|
15
15
|
titleButton: "titleButton-RPuvJm",
|
|
16
16
|
colRestore: "colRestore-D3b2hI",
|
|
17
17
|
restoreCell: "restoreCell-Exf1wn",
|
|
18
|
+
historyRow: "historyRow-P02Ptr",
|
|
19
|
+
auditRow: "auditRow-ZVH7hV",
|
|
20
|
+
audit: "audit-S9ZxPf",
|
|
18
21
|
restoreButton: "restoreButton-axrnAL",
|
|
19
22
|
restoreModal: "restoreModal-tNkjf6",
|
|
20
23
|
restoreModalHead: "restoreModalHead-cJAc46",
|
|
@@ -81,6 +81,8 @@
|
|
|
81
81
|
|
|
82
82
|
:is(.versionCell-Jc5W4g, .byline-coll-history-version-cell) {
|
|
83
83
|
text-align: left;
|
|
84
|
+
padding-left: .375rem;
|
|
85
|
+
padding-right: .25rem;
|
|
84
86
|
}
|
|
85
87
|
|
|
86
88
|
:is(.colVersion-xGf5qV, .byline-coll-history-col-version) {
|
|
@@ -88,7 +90,9 @@
|
|
|
88
90
|
}
|
|
89
91
|
|
|
90
92
|
:is(.versionButton-DXCZwj, .byline-coll-history-version-button) {
|
|
91
|
-
|
|
93
|
+
min-width: 20px;
|
|
94
|
+
min-height: 20px;
|
|
95
|
+
font-size: .6875rem;
|
|
92
96
|
font-family: var(--font-family-mono);
|
|
93
97
|
font-variant-numeric: tabular-nums;
|
|
94
98
|
}
|
|
@@ -127,6 +131,38 @@
|
|
|
127
131
|
white-space: nowrap;
|
|
128
132
|
}
|
|
129
133
|
|
|
134
|
+
.historyRow-P02Ptr td {
|
|
135
|
+
border-bottom: none;
|
|
136
|
+
padding-top: .375rem;
|
|
137
|
+
padding-bottom: .25rem;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.byline-coll-history-row td {
|
|
141
|
+
border-bottom: none;
|
|
142
|
+
padding-top: .375rem;
|
|
143
|
+
padding-bottom: .25rem;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.auditRow-ZVH7hV td {
|
|
147
|
+
padding-top: 0;
|
|
148
|
+
padding-bottom: .375rem;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.byline-coll-history-audit-row td {
|
|
152
|
+
padding-top: 0;
|
|
153
|
+
padding-bottom: .375rem;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
:is(.audit-S9ZxPf, .byline-coll-history-audit) {
|
|
157
|
+
color: var(--gray-500);
|
|
158
|
+
white-space: nowrap;
|
|
159
|
+
font-size: .75rem;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
:is(:is([data-theme="dark"], .dark) .audit-S9ZxPf, :is([data-theme="dark"], .dark) .byline-coll-history-audit) {
|
|
163
|
+
color: var(--gray-400);
|
|
164
|
+
}
|
|
165
|
+
|
|
130
166
|
:is(.restoreButton-axrnAL, .byline-coll-history-restore-button) {
|
|
131
167
|
opacity: 0;
|
|
132
168
|
transition: opacity .15s;
|
|
@@ -0,0 +1,17 @@
|
|
|
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
|
+
export type ActorLabelMap = Record<string, {
|
|
9
|
+
label: string;
|
|
10
|
+
}>;
|
|
11
|
+
/**
|
|
12
|
+
* Batch-resolve admin-user ids to display labels. Accepts the raw
|
|
13
|
+
* `createdBy` values straight off a page of shaped documents — nullish
|
|
14
|
+
* and duplicate entries are tolerated and deduplicated. Returns `{}`
|
|
15
|
+
* when there is nothing to resolve or no admin store is configured.
|
|
16
|
+
*/
|
|
17
|
+
export declare function resolveActorLabels(ids: Iterable<string | null | undefined>): Promise<ActorLabelMap>;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { bylineCore } from "../../integrations/byline-core.js";
|
|
2
|
+
function labelFor(row) {
|
|
3
|
+
const name = [
|
|
4
|
+
row.given_name,
|
|
5
|
+
row.family_name
|
|
6
|
+
].filter(Boolean).join(' ');
|
|
7
|
+
return name || row.username || row.email;
|
|
8
|
+
}
|
|
9
|
+
async function resolveActorLabels(ids) {
|
|
10
|
+
const unique = [
|
|
11
|
+
...new Set([
|
|
12
|
+
...ids
|
|
13
|
+
].filter((id)=>'string' == typeof id && id.length > 0))
|
|
14
|
+
];
|
|
15
|
+
if (0 === unique.length) return {};
|
|
16
|
+
const store = bylineCore().adminStore;
|
|
17
|
+
if (null == store) return {};
|
|
18
|
+
const rows = await store.adminUsers.getByIds(unique);
|
|
19
|
+
const map = {};
|
|
20
|
+
for (const row of rows)map[row.id] = {
|
|
21
|
+
label: labelFor(row)
|
|
22
|
+
};
|
|
23
|
+
return map;
|
|
24
|
+
}
|
|
25
|
+
export { resolveActorLabels };
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Copyright (c) Infonomic Company Limited
|
|
7
7
|
*/
|
|
8
|
+
import { type ActorLabelMap } from './actors.js';
|
|
8
9
|
export interface HistorySearchParams {
|
|
9
10
|
page?: number;
|
|
10
11
|
page_size?: number;
|
|
@@ -21,6 +22,7 @@ export declare const getCollectionDocumentHistory: import("@tanstack/react-start
|
|
|
21
22
|
id: string;
|
|
22
23
|
params: HistorySearchParams;
|
|
23
24
|
}, Promise<{
|
|
25
|
+
actors: ActorLabelMap;
|
|
24
26
|
docs: {
|
|
25
27
|
fields: Record<string, any>;
|
|
26
28
|
id: string;
|
|
@@ -31,6 +33,8 @@ export declare const getCollectionDocumentHistory: import("@tanstack/react-start
|
|
|
31
33
|
path?: string | undefined;
|
|
32
34
|
sourceLocale?: string | undefined;
|
|
33
35
|
hasPublishedVersion?: boolean | undefined;
|
|
36
|
+
createdBy?: string | undefined;
|
|
37
|
+
eventType?: string | undefined;
|
|
34
38
|
}[];
|
|
35
39
|
meta: {
|
|
36
40
|
page: number;
|
|
@@ -2,6 +2,7 @@ import { createServerFn } from "@tanstack/react-start";
|
|
|
2
2
|
import { ERR_NOT_FOUND, getCollectionSchemasForPath, getLogger } from "@byline/core";
|
|
3
3
|
import { ensureCollection } from "../../integrations/api-utils.js";
|
|
4
4
|
import { getAdminBylineClient } from "../../integrations/byline-client.js";
|
|
5
|
+
import { resolveActorLabels } from "./actors.js";
|
|
5
6
|
import { serialise } from "./utils.js";
|
|
6
7
|
const getCollectionDocumentHistory = createServerFn({
|
|
7
8
|
method: 'GET'
|
|
@@ -23,7 +24,14 @@ const getCollectionDocumentHistory = createServerFn({
|
|
|
23
24
|
});
|
|
24
25
|
const serialised = serialise(result);
|
|
25
26
|
const { history } = getCollectionSchemasForPath(path);
|
|
26
|
-
|
|
27
|
-
|
|
27
|
+
const actors = await resolveActorLabels(result.docs.map((d)=>d.createdBy));
|
|
28
|
+
if ('all' === params.locale) return {
|
|
29
|
+
...serialised,
|
|
30
|
+
actors
|
|
31
|
+
};
|
|
32
|
+
return {
|
|
33
|
+
...history.parse(serialised),
|
|
34
|
+
actors
|
|
35
|
+
};
|
|
28
36
|
});
|
|
29
37
|
export { getCollectionDocumentHistory };
|
|
@@ -32,6 +32,8 @@ export declare const getCollectionDocuments: import("@tanstack/react-start").Req
|
|
|
32
32
|
path?: string | undefined;
|
|
33
33
|
sourceLocale?: string | undefined;
|
|
34
34
|
hasPublishedVersion?: boolean | undefined;
|
|
35
|
+
createdBy?: string | undefined;
|
|
36
|
+
eventType?: string | undefined;
|
|
35
37
|
}[];
|
|
36
38
|
meta: {
|
|
37
39
|
page: number;
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"private": false,
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MPL-2.0",
|
|
6
|
-
"version": "3.
|
|
6
|
+
"version": "3.9.0",
|
|
7
7
|
"engines": {
|
|
8
8
|
"node": ">=20.9.0"
|
|
9
9
|
},
|
|
@@ -115,13 +115,13 @@
|
|
|
115
115
|
"react-swipeable": "^7.0.2",
|
|
116
116
|
"uuid": "^14.0.0",
|
|
117
117
|
"zod": "^4.4.3",
|
|
118
|
-
"@byline/
|
|
119
|
-
"@byline/
|
|
120
|
-
"@byline/auth": "3.
|
|
121
|
-
"@byline/
|
|
122
|
-
"@byline/
|
|
123
|
-
"@byline/
|
|
124
|
-
"@byline/ui": "3.
|
|
118
|
+
"@byline/core": "3.9.0",
|
|
119
|
+
"@byline/ai": "3.9.0",
|
|
120
|
+
"@byline/auth": "3.9.0",
|
|
121
|
+
"@byline/admin": "3.9.0",
|
|
122
|
+
"@byline/client": "3.9.0",
|
|
123
|
+
"@byline/i18n": "3.9.0",
|
|
124
|
+
"@byline/ui": "3.9.0"
|
|
125
125
|
},
|
|
126
126
|
"peerDependencies": {
|
|
127
127
|
"@tanstack/react-router": "^1.167.0",
|
|
@@ -13,6 +13,11 @@
|
|
|
13
13
|
* .byline-coll-history-version-button — number-stamped version button
|
|
14
14
|
* .byline-coll-history-title-button — clickable title cell button
|
|
15
15
|
* .byline-coll-history-page-size — page-size select alignment
|
|
16
|
+
* .byline-coll-history-row — content row (strip sibling above)
|
|
17
|
+
* .byline-coll-history-audit-row — audit strip row (acting user + action + time)
|
|
18
|
+
* .byline-coll-history-audit-spacer-cell — empty cell under the version column
|
|
19
|
+
* .byline-coll-history-audit-cell — colspan cell carrying the strip
|
|
20
|
+
* .byline-coll-history-audit — the muted strip line itself
|
|
16
21
|
*/
|
|
17
22
|
|
|
18
23
|
.head,
|
|
@@ -111,6 +116,10 @@
|
|
|
111
116
|
.versionCell,
|
|
112
117
|
:global(.byline-coll-history-version-cell) {
|
|
113
118
|
text-align: left;
|
|
119
|
+
/* The column is width: 1% (shrink-to-content), so the horizontal padding
|
|
120
|
+
is what actually sets its width around the 20px stamp. */
|
|
121
|
+
padding-left: 0.375rem;
|
|
122
|
+
padding-right: 0.25rem;
|
|
114
123
|
}
|
|
115
124
|
|
|
116
125
|
.colVersion,
|
|
@@ -120,7 +129,11 @@
|
|
|
120
129
|
|
|
121
130
|
.versionButton,
|
|
122
131
|
:global(.byline-coll-history-version-button) {
|
|
123
|
-
|
|
132
|
+
/* Shrink the xs round IconButton (26px) down to a 20px stamp so the
|
|
133
|
+
content row can sit tighter. */
|
|
134
|
+
min-height: 20px;
|
|
135
|
+
min-width: 20px;
|
|
136
|
+
font-size: 0.6875rem;
|
|
124
137
|
font-family: var(--font-family-mono);
|
|
125
138
|
font-variant-numeric: tabular-nums;
|
|
126
139
|
}
|
|
@@ -160,6 +173,46 @@
|
|
|
160
173
|
white-space: nowrap;
|
|
161
174
|
}
|
|
162
175
|
|
|
176
|
+
/* Audit strip (docs/AUDIT.md — W1). The content row's bottom
|
|
177
|
+
border is suppressed so the strip reads as part of the same entry; the
|
|
178
|
+
strip row carries the row divider instead. Vertical padding is tightened
|
|
179
|
+
against the table default (--spacing-8) to keep the two-row entry compact. */
|
|
180
|
+
.historyRow td,
|
|
181
|
+
:global(.byline-coll-history-row td) {
|
|
182
|
+
border-bottom: none;
|
|
183
|
+
padding-top: 0.375rem;
|
|
184
|
+
padding-bottom: 0.25rem;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
.auditRow td,
|
|
188
|
+
:global(.byline-coll-history-audit-row td) {
|
|
189
|
+
padding-top: 0;
|
|
190
|
+
padding-bottom: 0.375rem;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
.auditSpacerCell,
|
|
194
|
+
:global(.byline-coll-history-audit-spacer-cell) {
|
|
195
|
+
/* empty cell under the version-number column; the strip starts on the
|
|
196
|
+
second column */
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
.auditCell,
|
|
200
|
+
:global(.byline-coll-history-audit-cell) {
|
|
201
|
+
/* override handle */
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
.audit,
|
|
205
|
+
:global(.byline-coll-history-audit) {
|
|
206
|
+
font-size: 0.75rem;
|
|
207
|
+
color: var(--gray-500);
|
|
208
|
+
white-space: nowrap;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
:is([data-theme="dark"], :global(.dark)) .audit,
|
|
212
|
+
:is([data-theme="dark"], :global(.dark)) :global(.byline-coll-history-audit) {
|
|
213
|
+
color: var(--gray-400);
|
|
214
|
+
}
|
|
215
|
+
|
|
163
216
|
.restoreButton,
|
|
164
217
|
:global(.byline-coll-history-restore-button) {
|
|
165
218
|
opacity: 0;
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* Copyright (c) Infonomic Company Limited
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { lazy, Suspense, useState } from 'react'
|
|
9
|
+
import { Fragment, lazy, Suspense, useState } from 'react'
|
|
10
10
|
import { useParams, useRouterState } from '@tanstack/react-router'
|
|
11
11
|
|
|
12
12
|
import { renderFormatted, StatusBadge } from '@byline/admin/react'
|
|
@@ -94,6 +94,18 @@ function padRows(value: number) {
|
|
|
94
94
|
))
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
/**
|
|
98
|
+
* Maps `event_type` values to audit-strip action labels. Unknown event
|
|
99
|
+
* types fall back to the raw value rather than a missing-key warning.
|
|
100
|
+
*/
|
|
101
|
+
const AUDIT_ACTION_KEYS: Record<string, string> = {
|
|
102
|
+
create: 'collections.history.audit.actionCreate',
|
|
103
|
+
update: 'collections.history.audit.actionUpdate',
|
|
104
|
+
restore: 'collections.history.audit.actionRestore',
|
|
105
|
+
copy_to_locale: 'collections.history.audit.actionCopyToLocale',
|
|
106
|
+
delete_locale: 'collections.history.audit.actionDeleteLocale',
|
|
107
|
+
}
|
|
108
|
+
|
|
97
109
|
export const HistoryView = ({
|
|
98
110
|
collectionDefinition,
|
|
99
111
|
adminConfig,
|
|
@@ -105,7 +117,14 @@ export const HistoryView = ({
|
|
|
105
117
|
}: {
|
|
106
118
|
collectionDefinition: CollectionDefinition
|
|
107
119
|
adminConfig?: CollectionAdminConfig
|
|
108
|
-
data: AnyCollectionSchemaTypes['HistoryType']
|
|
120
|
+
data: AnyCollectionSchemaTypes['HistoryType'] & {
|
|
121
|
+
/**
|
|
122
|
+
* Audit display labels (the acting user per version), resolved admin-side from each
|
|
123
|
+
* version's `createdBy` id (see docs/AUDIT.md — Workstream 1). Ids
|
|
124
|
+
* absent from the map belong to deleted users.
|
|
125
|
+
*/
|
|
126
|
+
actors?: Record<string, { label: string }>
|
|
127
|
+
}
|
|
109
128
|
workflowStatuses?: WorkflowStatus[]
|
|
110
129
|
currentDocument?: Record<string, unknown> | null
|
|
111
130
|
contentLocales: ReadonlyArray<ContentLocaleOption>
|
|
@@ -119,6 +138,10 @@ export const HistoryView = ({
|
|
|
119
138
|
const { t } = useTranslation('byline-admin')
|
|
120
139
|
const columns = adminConfig?.columns || []
|
|
121
140
|
const { labels } = collectionDefinition
|
|
141
|
+
// The identity column — drives the clickable compare cell and the
|
|
142
|
+
// injected restore column, mirroring list.tsx. `useAsTitle` is optional;
|
|
143
|
+
// when absent, those affordances (and the strip colspan's +1) turn off.
|
|
144
|
+
const titleFieldName = collectionDefinition.useAsTitle
|
|
122
145
|
const location = useRouterState({ select: (s) => s.location })
|
|
123
146
|
const locale = (location.search as { locale?: string }).locale
|
|
124
147
|
const [selectedVersion, setSelectedVersion] = useState<{
|
|
@@ -141,8 +164,8 @@ export const HistoryView = ({
|
|
|
141
164
|
delete params.page
|
|
142
165
|
params.page_size = Number.parseInt(value, 10)
|
|
143
166
|
navigate({
|
|
144
|
-
to: '/admin/collections/$collection' as never,
|
|
145
|
-
params: { collection },
|
|
167
|
+
to: '/admin/collections/$collection/$id/history' as never,
|
|
168
|
+
params: { collection, id } as never,
|
|
146
169
|
search: params,
|
|
147
170
|
})
|
|
148
171
|
}
|
|
@@ -199,7 +222,7 @@ export const HistoryView = ({
|
|
|
199
222
|
className={column.className}
|
|
200
223
|
/>
|
|
201
224
|
)
|
|
202
|
-
if (column.fieldName ===
|
|
225
|
+
if (titleFieldName != null && column.fieldName === titleFieldName) {
|
|
203
226
|
return [
|
|
204
227
|
cell,
|
|
205
228
|
<th
|
|
@@ -221,147 +244,198 @@ export const HistoryView = ({
|
|
|
221
244
|
const versionNumber = desc
|
|
222
245
|
? total - (page - 1) * pageSize - rowIndex
|
|
223
246
|
: (page - 1) * pageSize + rowIndex + 1
|
|
247
|
+
// Audit strip (docs/AUDIT.md — W1): who created this
|
|
248
|
+
// version, via which action. A present-but-unresolved id
|
|
249
|
+
// is a deleted user; an absent id is a row written before
|
|
250
|
+
// audit wiring or an internal-tooling write.
|
|
251
|
+
const actorLabel = document.createdBy
|
|
252
|
+
? (data.actors?.[document.createdBy]?.label ??
|
|
253
|
+
t('collections.history.audit.formerUser'))
|
|
254
|
+
: t('collections.history.audit.unknown')
|
|
255
|
+
const actionKey = document.eventType
|
|
256
|
+
? AUDIT_ACTION_KEYS[document.eventType]
|
|
257
|
+
: undefined
|
|
258
|
+
const actionLabel = actionKey ? t(actionKey) : (document.eventType ?? '')
|
|
259
|
+
// The strip starts on the second column — an empty spacer
|
|
260
|
+
// cell sits under the version-number column, then one cell
|
|
261
|
+
// spans the data columns plus the restore cell appended
|
|
262
|
+
// after the identity column when present.
|
|
263
|
+
const auditColSpan =
|
|
264
|
+
columns.length +
|
|
265
|
+
(titleFieldName != null && columns.some((c) => c.fieldName === titleFieldName)
|
|
266
|
+
? 1
|
|
267
|
+
: 0)
|
|
224
268
|
return (
|
|
225
|
-
<
|
|
226
|
-
<Table.
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
key={String(column.fieldName)}
|
|
255
|
-
className={cx({
|
|
256
|
-
'byline-coll-history-cell-right': column.align === 'right',
|
|
257
|
-
[styles.cellRight]: column.align === 'right',
|
|
258
|
-
'byline-coll-history-cell-center': column.align === 'center',
|
|
259
|
-
[styles.cellCenter]: column.align === 'center',
|
|
260
|
-
})}
|
|
261
|
-
>
|
|
262
|
-
{column.fieldName === 'title' ? (
|
|
263
|
-
versionId && currentDocument ? (
|
|
264
|
-
<button
|
|
265
|
-
type="button"
|
|
266
|
-
className={cx(
|
|
267
|
-
'byline-coll-history-title-button',
|
|
268
|
-
styles.titleButton
|
|
269
|
-
)}
|
|
270
|
-
onClick={() =>
|
|
271
|
-
setSelectedVersion({
|
|
272
|
-
versionId,
|
|
273
|
-
label: new Date(document.createdAt).toLocaleString(),
|
|
274
|
-
})
|
|
275
|
-
}
|
|
276
|
-
>
|
|
277
|
-
{column.formatter
|
|
278
|
-
? renderFormatted(
|
|
279
|
-
getColumnValue(document, column.fieldName as string),
|
|
280
|
-
document,
|
|
281
|
-
column.formatter
|
|
282
|
-
)
|
|
283
|
-
: resolveDisplayValue(
|
|
284
|
-
getColumnValue(document, column.fieldName as string),
|
|
285
|
-
locale,
|
|
286
|
-
defaultContentLocale
|
|
287
|
-
) || '------'}
|
|
288
|
-
</button>
|
|
289
|
-
) : (
|
|
290
|
-
<Link
|
|
291
|
-
to={'/admin/collections/$collection/$id' as never}
|
|
292
|
-
params={{
|
|
293
|
-
collection,
|
|
294
|
-
id: document.id,
|
|
295
|
-
}}
|
|
296
|
-
>
|
|
297
|
-
{column.formatter
|
|
298
|
-
? renderFormatted(
|
|
299
|
-
getColumnValue(document, column.fieldName as string),
|
|
300
|
-
document,
|
|
301
|
-
column.formatter
|
|
302
|
-
)
|
|
303
|
-
: resolveDisplayValue(
|
|
304
|
-
getColumnValue(document, column.fieldName as string),
|
|
305
|
-
locale,
|
|
306
|
-
defaultContentLocale
|
|
307
|
-
) || '------'}
|
|
308
|
-
</Link>
|
|
309
|
-
)
|
|
310
|
-
) : column.formatter ? (
|
|
311
|
-
renderFormatted(
|
|
312
|
-
getColumnValue(document, column.fieldName as string),
|
|
313
|
-
document,
|
|
314
|
-
column.formatter
|
|
315
|
-
)
|
|
316
|
-
) : column.fieldName === 'status' && workflowStatuses ? (
|
|
317
|
-
<StatusBadge
|
|
318
|
-
status={document.status}
|
|
319
|
-
workflowStatuses={workflowStatuses}
|
|
320
|
-
/>
|
|
321
|
-
) : (
|
|
322
|
-
resolveDisplayValue(
|
|
323
|
-
getColumnValue(document, column.fieldName as string),
|
|
324
|
-
locale,
|
|
325
|
-
defaultContentLocale
|
|
326
|
-
) || ''
|
|
327
|
-
)}
|
|
328
|
-
</Table.Cell>
|
|
329
|
-
)
|
|
330
|
-
if (column.fieldName === 'title') {
|
|
331
|
-
return [
|
|
332
|
-
dataCell,
|
|
269
|
+
<Fragment key={versionId ?? document.id}>
|
|
270
|
+
<Table.Row className={cx('byline-coll-history-row', styles.historyRow)}>
|
|
271
|
+
<Table.Cell
|
|
272
|
+
className={cx('byline-coll-history-version-cell', styles.versionCell)}
|
|
273
|
+
>
|
|
274
|
+
{versionId && currentDocument ? (
|
|
275
|
+
<IconButton
|
|
276
|
+
size="xs"
|
|
277
|
+
variant="outlined"
|
|
278
|
+
intent="noeffect"
|
|
279
|
+
aria-label={t('collections.history.compareAriaLabel')}
|
|
280
|
+
title={t('collections.history.compareTitle')}
|
|
281
|
+
className={cx(
|
|
282
|
+
'byline-coll-history-version-button',
|
|
283
|
+
styles.versionButton
|
|
284
|
+
)}
|
|
285
|
+
onClick={() =>
|
|
286
|
+
setSelectedVersion({
|
|
287
|
+
versionId,
|
|
288
|
+
label: new Date(document.createdAt).toLocaleString(),
|
|
289
|
+
})
|
|
290
|
+
}
|
|
291
|
+
>
|
|
292
|
+
{versionNumber}
|
|
293
|
+
</IconButton>
|
|
294
|
+
) : null}
|
|
295
|
+
</Table.Cell>
|
|
296
|
+
{columns.flatMap((column) => {
|
|
297
|
+
const dataCell = (
|
|
333
298
|
<Table.Cell
|
|
334
|
-
key=
|
|
335
|
-
className={cx(
|
|
299
|
+
key={String(column.fieldName)}
|
|
300
|
+
className={cx({
|
|
301
|
+
'byline-coll-history-cell-right': column.align === 'right',
|
|
302
|
+
[styles.cellRight]: column.align === 'right',
|
|
303
|
+
'byline-coll-history-cell-center': column.align === 'center',
|
|
304
|
+
[styles.cellCenter]: column.align === 'center',
|
|
305
|
+
})}
|
|
336
306
|
>
|
|
337
|
-
{
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
307
|
+
{titleFieldName != null && column.fieldName === titleFieldName ? (
|
|
308
|
+
versionId && currentDocument ? (
|
|
309
|
+
<button
|
|
310
|
+
type="button"
|
|
311
|
+
className={cx(
|
|
312
|
+
'byline-coll-history-title-button',
|
|
313
|
+
styles.titleButton
|
|
314
|
+
)}
|
|
315
|
+
onClick={() =>
|
|
316
|
+
setSelectedVersion({
|
|
317
|
+
versionId,
|
|
318
|
+
label: new Date(document.createdAt).toLocaleString(),
|
|
319
|
+
})
|
|
320
|
+
}
|
|
321
|
+
>
|
|
322
|
+
{column.formatter
|
|
323
|
+
? renderFormatted(
|
|
324
|
+
getColumnValue(document, column.fieldName as string),
|
|
325
|
+
document,
|
|
326
|
+
column.formatter
|
|
327
|
+
)
|
|
328
|
+
: resolveDisplayValue(
|
|
329
|
+
getColumnValue(document, column.fieldName as string),
|
|
330
|
+
locale,
|
|
331
|
+
defaultContentLocale
|
|
332
|
+
) || '------'}
|
|
333
|
+
</button>
|
|
334
|
+
) : (
|
|
335
|
+
<Link
|
|
336
|
+
to={'/admin/collections/$collection/$id' as never}
|
|
337
|
+
params={{
|
|
338
|
+
collection,
|
|
339
|
+
id: document.id,
|
|
340
|
+
}}
|
|
341
|
+
>
|
|
342
|
+
{column.formatter
|
|
343
|
+
? renderFormatted(
|
|
344
|
+
getColumnValue(document, column.fieldName as string),
|
|
345
|
+
document,
|
|
346
|
+
column.formatter
|
|
347
|
+
)
|
|
348
|
+
: resolveDisplayValue(
|
|
349
|
+
getColumnValue(document, column.fieldName as string),
|
|
350
|
+
locale,
|
|
351
|
+
defaultContentLocale
|
|
352
|
+
) || '------'}
|
|
353
|
+
</Link>
|
|
354
|
+
)
|
|
355
|
+
) : column.formatter ? (
|
|
356
|
+
renderFormatted(
|
|
357
|
+
getColumnValue(document, column.fieldName as string),
|
|
358
|
+
document,
|
|
359
|
+
column.formatter
|
|
360
|
+
)
|
|
361
|
+
) : column.fieldName === 'status' && workflowStatuses ? (
|
|
362
|
+
<StatusBadge
|
|
363
|
+
status={document.status}
|
|
364
|
+
workflowStatuses={workflowStatuses}
|
|
365
|
+
/>
|
|
366
|
+
) : (
|
|
367
|
+
resolveDisplayValue(
|
|
368
|
+
getColumnValue(document, column.fieldName as string),
|
|
369
|
+
locale,
|
|
370
|
+
defaultContentLocale
|
|
371
|
+
) || ''
|
|
372
|
+
)}
|
|
373
|
+
</Table.Cell>
|
|
374
|
+
)
|
|
375
|
+
if (titleFieldName != null && column.fieldName === titleFieldName) {
|
|
376
|
+
return [
|
|
377
|
+
dataCell,
|
|
378
|
+
<Table.Cell
|
|
379
|
+
key="__restore"
|
|
380
|
+
className={cx(
|
|
381
|
+
'byline-coll-history-restore-cell',
|
|
382
|
+
styles.restoreCell
|
|
383
|
+
)}
|
|
384
|
+
>
|
|
385
|
+
{versionId && versionId !== currentVersionId ? (
|
|
386
|
+
<Button
|
|
387
|
+
type="button"
|
|
388
|
+
variant="outlined"
|
|
389
|
+
size="xs"
|
|
390
|
+
intent="noeffect"
|
|
391
|
+
onClick={() =>
|
|
392
|
+
setRestoreTarget({
|
|
393
|
+
versionId,
|
|
394
|
+
label: new Date(document.createdAt).toLocaleString(),
|
|
395
|
+
versionNumber,
|
|
396
|
+
})
|
|
397
|
+
}
|
|
398
|
+
className={cx(
|
|
399
|
+
'byline-coll-history-restore-button',
|
|
400
|
+
styles.restoreButton
|
|
401
|
+
)}
|
|
402
|
+
title={t('collections.history.restoreButtonTitle')}
|
|
403
|
+
>
|
|
404
|
+
{t('collections.history.restoreButton')}
|
|
405
|
+
</Button>
|
|
406
|
+
) : null}
|
|
407
|
+
</Table.Cell>,
|
|
408
|
+
]
|
|
409
|
+
}
|
|
410
|
+
return [dataCell]
|
|
411
|
+
})}
|
|
412
|
+
</Table.Row>
|
|
413
|
+
<Table.Row
|
|
414
|
+
className={cx('byline-coll-history-audit-row', styles.auditRow)}
|
|
415
|
+
aria-label={t('collections.history.audit.createdBy', {
|
|
416
|
+
label: actorLabel,
|
|
417
|
+
})}
|
|
418
|
+
>
|
|
419
|
+
<Table.Cell
|
|
420
|
+
className={cx(
|
|
421
|
+
'byline-coll-history-audit-spacer-cell',
|
|
422
|
+
styles.auditSpacerCell
|
|
423
|
+
)}
|
|
424
|
+
/>
|
|
425
|
+
<Table.Cell
|
|
426
|
+
colSpan={auditColSpan}
|
|
427
|
+
className={cx('byline-coll-history-audit-cell', styles.auditCell)}
|
|
428
|
+
>
|
|
429
|
+
<span className={cx('byline-coll-history-audit', styles.audit)}>
|
|
430
|
+
{actionLabel}
|
|
431
|
+
{' · '}
|
|
432
|
+
{t('collections.history.audit.createdBy', { label: actorLabel })}
|
|
433
|
+
{' · '}
|
|
434
|
+
{new Date(document.createdAt).toLocaleString()}
|
|
435
|
+
</span>
|
|
436
|
+
</Table.Cell>
|
|
437
|
+
</Table.Row>
|
|
438
|
+
</Fragment>
|
|
365
439
|
)
|
|
366
440
|
})}
|
|
367
441
|
</Table.Body>
|
|
@@ -0,0 +1,60 @@
|
|
|
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
|
+
* Audit actor-label resolution (docs/AUDIT.md — Workstream 1).
|
|
11
|
+
*
|
|
12
|
+
* Shaped documents carry only the raw `createdBy` uuid; turning ids into
|
|
13
|
+
* display labels is an **admin-realm** concern, so it happens here — in
|
|
14
|
+
* the admin server-fn layer, which already holds the admin actor and the
|
|
15
|
+
* `AdminStore` — never as a JOIN inside the document storage adapter
|
|
16
|
+
* (which must stay ignorant of admin-realm tables for the future
|
|
17
|
+
* `UserAuth` writer realm).
|
|
18
|
+
*
|
|
19
|
+
* Responses attach the result as an `actors` map alongside the page of
|
|
20
|
+
* documents; the UI joins by id. Ids with no matching admin-user row
|
|
21
|
+
* (deleted users) are absent from the map — consumers render a tombstone
|
|
22
|
+
* label.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import type { AdminUserRow } from '@byline/admin/admin-users'
|
|
26
|
+
|
|
27
|
+
import { bylineCore } from '../../integrations/byline-core.js'
|
|
28
|
+
|
|
29
|
+
export type ActorLabelMap = Record<string, { label: string }>
|
|
30
|
+
|
|
31
|
+
/** Display label: full name → username → email. */
|
|
32
|
+
function labelFor(row: AdminUserRow): string {
|
|
33
|
+
const name = [row.given_name, row.family_name].filter(Boolean).join(' ')
|
|
34
|
+
return name || row.username || row.email
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Batch-resolve admin-user ids to display labels. Accepts the raw
|
|
39
|
+
* `createdBy` values straight off a page of shaped documents — nullish
|
|
40
|
+
* and duplicate entries are tolerated and deduplicated. Returns `{}`
|
|
41
|
+
* when there is nothing to resolve or no admin store is configured.
|
|
42
|
+
*/
|
|
43
|
+
export async function resolveActorLabels(
|
|
44
|
+
ids: Iterable<string | null | undefined>
|
|
45
|
+
): Promise<ActorLabelMap> {
|
|
46
|
+
const unique = [
|
|
47
|
+
...new Set([...ids].filter((id): id is string => typeof id === 'string' && id.length > 0)),
|
|
48
|
+
]
|
|
49
|
+
if (unique.length === 0) return {}
|
|
50
|
+
|
|
51
|
+
const store = bylineCore().adminStore
|
|
52
|
+
if (store == null) return {}
|
|
53
|
+
|
|
54
|
+
const rows = await store.adminUsers.getByIds(unique)
|
|
55
|
+
const map: ActorLabelMap = {}
|
|
56
|
+
for (const row of rows) {
|
|
57
|
+
map[row.id] = { label: labelFor(row) }
|
|
58
|
+
}
|
|
59
|
+
return map
|
|
60
|
+
}
|
|
@@ -12,6 +12,7 @@ import { ERR_NOT_FOUND, getCollectionSchemasForPath, getLogger } from '@byline/c
|
|
|
12
12
|
|
|
13
13
|
import { ensureCollection } from '../../integrations/api-utils.js'
|
|
14
14
|
import { getAdminBylineClient } from '../../integrations/byline-client.js'
|
|
15
|
+
import { type ActorLabelMap, resolveActorLabels } from './actors.js'
|
|
15
16
|
import { serialise } from './utils'
|
|
16
17
|
|
|
17
18
|
// ---------------------------------------------------------------------------
|
|
@@ -60,14 +61,22 @@ export const getCollectionDocumentHistory = createServerFn({ method: 'GET' })
|
|
|
60
61
|
const serialised = serialise(result)
|
|
61
62
|
const { history } = getCollectionSchemasForPath(path)
|
|
62
63
|
|
|
64
|
+
// Acting-user labels for the audit strip (docs/AUDIT.md — W1).
|
|
65
|
+
// Resolved here, in the admin realm, from the page's raw `createdBy`
|
|
66
|
+
// ids; the UI joins by id. Deleted users are absent from the map.
|
|
67
|
+
const actors: ActorLabelMap = await resolveActorLabels(result.docs.map((d) => d.createdBy))
|
|
68
|
+
|
|
63
69
|
// When locale is 'all' the storage layer returns localized fields as
|
|
64
70
|
// locale-keyed objects which don't conform to the typed Zod schema — skip
|
|
65
71
|
// validation in that case, same as getCollectionDocument. Cast through
|
|
66
72
|
// the inferred Zod type so both branches share one return shape; the
|
|
67
73
|
// runtime contents are structurally compatible.
|
|
68
74
|
if (params.locale === 'all') {
|
|
69
|
-
return
|
|
75
|
+
return {
|
|
76
|
+
...(serialised as unknown as ReturnType<typeof history.parse>),
|
|
77
|
+
actors,
|
|
78
|
+
}
|
|
70
79
|
}
|
|
71
80
|
|
|
72
|
-
return history.parse(serialised)
|
|
81
|
+
return { ...history.parse(serialised), actors }
|
|
73
82
|
})
|