@byline/host-tanstack-start 4.16.0 → 4.17.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.
Files changed (146) hide show
  1. package/dist/admin-shell/chrome/admin-menu-drawer.js +14 -14
  2. package/dist/admin-shell/chrome/dashboard.js +41 -5
  3. package/dist/admin-shell/chrome/dashboard.test.d.ts +8 -0
  4. package/dist/admin-shell/collections/api.d.ts +2 -2
  5. package/dist/admin-shell/collections/create.d.ts +2 -2
  6. package/dist/admin-shell/collections/create.js +1 -0
  7. package/dist/admin-shell/collections/edit.d.ts +2 -2
  8. package/dist/admin-shell/collections/edit.js +1 -0
  9. package/dist/admin-shell/collections/history.d.ts +4 -12
  10. package/dist/admin-shell/collections/history.js +112 -303
  11. package/dist/admin-shell/collections/reindex-button.d.ts +1 -1
  12. package/dist/admin-shell/collections/reindex-button.js +5 -1
  13. package/dist/admin-shell/collections/restore-version-modal.d.ts +4 -4
  14. package/dist/admin-shell/collections/restore-version-modal.js +8 -24
  15. package/dist/admin-shell/collections/version-history.d.ts +72 -0
  16. package/dist/admin-shell/collections/version-history.js +259 -0
  17. package/dist/admin-shell/singletons/history.d.ts +19 -0
  18. package/dist/admin-shell/singletons/history.js +170 -0
  19. package/dist/admin-shell/singletons/history.test.d.ts +8 -0
  20. package/dist/admin-shell/singletons/preview-link.d.ts +12 -0
  21. package/dist/admin-shell/singletons/preview-link.js +54 -0
  22. package/dist/admin-shell/singletons/view-menu.d.ts +13 -0
  23. package/dist/admin-shell/singletons/view-menu.js +116 -0
  24. package/dist/admin-shell/singletons/view.d.ts +18 -0
  25. package/dist/admin-shell/singletons/view.js +259 -0
  26. package/dist/admin-shell/singletons/view.test.d.ts +8 -0
  27. package/dist/integrations/api-utils.d.ts +18 -4
  28. package/dist/integrations/api-utils.js +10 -3
  29. package/dist/routes/create-admin-dashboard-route.js +2 -2
  30. package/dist/routes/create-collection-api-route.js +3 -3
  31. package/dist/routes/create-collection-create-route.js +4 -3
  32. package/dist/routes/create-collection-edit-route.js +4 -3
  33. package/dist/routes/create-collection-history-route.js +4 -3
  34. package/dist/routes/create-collection-list-route.js +4 -3
  35. package/dist/routes/create-singleton-history-route.d.ts +8 -0
  36. package/dist/routes/create-singleton-history-route.js +99 -0
  37. package/dist/routes/create-singleton-route.d.ts +35 -0
  38. package/dist/routes/create-singleton-route.js +130 -0
  39. package/dist/routes/get-multi-collection-definition.d.ts +10 -0
  40. package/dist/routes/get-multi-collection-definition.js +6 -0
  41. package/dist/routes/get-singleton-definition.d.ts +10 -0
  42. package/dist/routes/get-singleton-definition.js +6 -0
  43. package/dist/routes/index.d.ts +2 -0
  44. package/dist/routes/index.js +2 -0
  45. package/dist/server-fns/actors.d.ts +16 -0
  46. package/dist/server-fns/actors.js +25 -0
  47. package/dist/server-fns/admin-document-presentation.d.ts +30 -0
  48. package/dist/server-fns/admin-document-presentation.js +32 -0
  49. package/dist/server-fns/collections/actors.d.ts +1 -10
  50. package/dist/server-fns/collections/actors.js +1 -25
  51. package/dist/server-fns/collections/get.js +9 -26
  52. package/dist/server-fns/collections/reorder.js +1 -1
  53. package/dist/server-fns/collections/scheduled-publication.js +2 -2
  54. package/dist/server-fns/collections/upload.js +3 -3
  55. package/dist/server-fns/collections/utils.d.ts +1 -7
  56. package/dist/server-fns/collections/utils.js +1 -4
  57. package/dist/server-fns/serialise.d.ts +14 -0
  58. package/dist/server-fns/serialise.js +4 -0
  59. package/dist/server-fns/singleton-document-read.d.ts +20 -0
  60. package/dist/server-fns/singleton-document-read.js +10 -0
  61. package/dist/server-fns/singletons/cancel-scheduled-publish.d.ts +12 -0
  62. package/dist/server-fns/singletons/cancel-scheduled-publish.js +7 -0
  63. package/dist/server-fns/singletons/change-status.d.ts +14 -0
  64. package/dist/server-fns/singletons/change-status.js +7 -0
  65. package/dist/server-fns/singletons/confirm-scheduled-publish.d.ts +14 -0
  66. package/dist/server-fns/singletons/confirm-scheduled-publish.js +9 -0
  67. package/dist/server-fns/singletons/copy-to-locale.d.ts +18 -0
  68. package/dist/server-fns/singletons/copy-to-locale.js +11 -0
  69. package/dist/server-fns/singletons/find-by-version.d.ts +16 -0
  70. package/dist/server-fns/singletons/find-by-version.js +9 -0
  71. package/dist/server-fns/singletons/get-scheduled-publish.d.ts +12 -0
  72. package/dist/server-fns/singletons/get-scheduled-publish.js +7 -0
  73. package/dist/server-fns/singletons/get.d.ts +24 -0
  74. package/dist/server-fns/singletons/get.js +6 -0
  75. package/dist/server-fns/singletons/history.d.ts +31 -0
  76. package/dist/server-fns/singletons/history.js +22 -0
  77. package/dist/server-fns/singletons/index.d.ts +19 -0
  78. package/dist/server-fns/singletons/index.js +12 -0
  79. package/dist/server-fns/singletons/restore-version.d.ts +14 -0
  80. package/dist/server-fns/singletons/restore-version.js +7 -0
  81. package/dist/server-fns/singletons/schedule-publish.d.ts +16 -0
  82. package/dist/server-fns/singletons/schedule-publish.js +10 -0
  83. package/dist/server-fns/singletons/unpublish.d.ts +12 -0
  84. package/dist/server-fns/singletons/unpublish.js +7 -0
  85. package/dist/server-fns/singletons/update.d.ts +14 -0
  86. package/dist/server-fns/singletons/update.js +14 -0
  87. package/dist/vitest-jsdom.test.d.ts +8 -0
  88. package/package.json +24 -18
  89. package/src/admin-shell/chrome/admin-menu-drawer.tsx +13 -13
  90. package/src/admin-shell/chrome/dashboard.test.tsx +199 -0
  91. package/src/admin-shell/chrome/dashboard.tsx +43 -9
  92. package/src/admin-shell/collections/api.tsx +2 -2
  93. package/src/admin-shell/collections/create.tsx +6 -2
  94. package/src/admin-shell/collections/edit.tsx +6 -2
  95. package/src/admin-shell/collections/history.module.css +1 -1
  96. package/src/admin-shell/collections/history.tsx +162 -442
  97. package/src/admin-shell/collections/reindex-button.tsx +4 -2
  98. package/src/admin-shell/collections/restore-version-modal.tsx +19 -25
  99. package/src/admin-shell/collections/version-history.tsx +349 -0
  100. package/src/admin-shell/singletons/history.test.tsx +407 -0
  101. package/src/admin-shell/singletons/history.tsx +185 -0
  102. package/src/admin-shell/singletons/preview-link.tsx +75 -0
  103. package/src/admin-shell/singletons/view-menu.tsx +136 -0
  104. package/src/admin-shell/singletons/view.test.tsx +400 -0
  105. package/src/admin-shell/singletons/view.tsx +351 -0
  106. package/src/integrations/api-utils.ts +34 -9
  107. package/src/routes/create-admin-dashboard-route.test.node.ts +70 -0
  108. package/src/routes/create-admin-dashboard-route.tsx +10 -2
  109. package/src/routes/create-collection-api-route.tsx +4 -4
  110. package/src/routes/create-collection-create-route.tsx +5 -8
  111. package/src/routes/create-collection-edit-route.tsx +5 -4
  112. package/src/routes/create-collection-history-route.tsx +5 -8
  113. package/src/routes/create-collection-list-route.tsx +5 -8
  114. package/src/routes/create-singleton-history-route.test.node.ts +148 -0
  115. package/src/routes/create-singleton-history-route.tsx +116 -0
  116. package/src/routes/create-singleton-route.test.node.ts +172 -0
  117. package/src/routes/create-singleton-route.tsx +176 -0
  118. package/src/routes/get-multi-collection-definition.ts +16 -0
  119. package/src/routes/get-singleton-definition.ts +16 -0
  120. package/src/routes/index.ts +2 -0
  121. package/src/server-fns/actors.ts +43 -0
  122. package/src/server-fns/admin-document-presentation.ts +78 -0
  123. package/src/server-fns/collections/actors.ts +1 -52
  124. package/src/server-fns/collections/get.ts +10 -49
  125. package/src/server-fns/collections/reorder.ts +1 -1
  126. package/src/server-fns/collections/scheduled-publication.ts +7 -1
  127. package/src/server-fns/collections/upload-singleton.test.node.ts +164 -0
  128. package/src/server-fns/collections/upload.ts +7 -3
  129. package/src/server-fns/collections/utils.ts +1 -9
  130. package/src/server-fns/serialise.ts +17 -0
  131. package/src/server-fns/singleton-document-read.ts +32 -0
  132. package/src/server-fns/singletons/cancel-scheduled-publish.ts +19 -0
  133. package/src/server-fns/singletons/change-status.ts +19 -0
  134. package/src/server-fns/singletons/confirm-scheduled-publish.ts +23 -0
  135. package/src/server-fns/singletons/copy-to-locale.ts +32 -0
  136. package/src/server-fns/singletons/find-by-version.ts +23 -0
  137. package/src/server-fns/singletons/get-scheduled-publish.ts +19 -0
  138. package/src/server-fns/singletons/get.ts +32 -0
  139. package/src/server-fns/singletons/history.ts +41 -0
  140. package/src/server-fns/singletons/index.ts +20 -0
  141. package/src/server-fns/singletons/restore-version.ts +19 -0
  142. package/src/server-fns/singletons/schedule-publish.ts +24 -0
  143. package/src/server-fns/singletons/singletons.test.node.ts +298 -0
  144. package/src/server-fns/singletons/unpublish.ts +19 -0
  145. package/src/server-fns/singletons/update.ts +31 -0
  146. package/src/vitest-jsdom.test.tsx +27 -0
@@ -6,57 +6,40 @@
6
6
  * Copyright (c) Infonomic Company Limited
7
7
  */
8
8
 
9
- import { Fragment, lazy, Suspense, useState } from 'react'
10
- import { useParams, useRouterState } from '@tanstack/react-router'
9
+ import { useParams, useRouter, useRouterState } from '@tanstack/react-router'
11
10
 
12
11
  import { AdminTabs, renderFormatted, StatusBadge } from '@byline/admin/react'
13
12
  import { useBylineAdminServices } from '@byline/admin/services'
14
- import type { CollectionAdminConfig, CollectionDefinition, WorkflowStatus } from '@byline/core'
13
+ import type { CollectionAdminConfig, MultiCollectionDefinition, WorkflowStatus } from '@byline/core'
15
14
  import type { AnyCollectionSchemaTypes } from '@byline/core/zod-schemas'
16
15
  import { useTranslation } from '@byline/i18n/react'
17
- import {
18
- Button,
19
- CloseIcon,
20
- Container,
21
- IconButton,
22
- Modal,
23
- Section,
24
- Select,
25
- Table,
26
- } from '@byline/ui/react'
16
+ import { Button, Container, Section, Table } from '@byline/ui/react'
27
17
  import cx from 'clsx'
28
18
 
29
19
  import { getAdminRouteId, getAdminRoutePath } from '../../routes/admin-path.js'
20
+ import { restoreDocumentVersion } from '../../server-fns/collections/index.js'
30
21
  import { Link, useNavigate } from '../chrome/loose-router.js'
31
- import { RouterPager } from '../chrome/router-pager.js'
32
22
  import { TableHeadingCellSortable } from '../chrome/th-sortable.js'
33
- import { formatNumber } from '../chrome/utils.js'
34
23
  import { type DocumentHistoryData, DocumentHistoryView } from './document-history.js'
35
24
  import styles from './history.module.css'
36
- import { RestoreVersionModal } from './restore-version-modal.js'
25
+ import {
26
+ HistoryStats,
27
+ VersionHistoryCore,
28
+ type VersionHistoryData,
29
+ type VersionHistoryRowPresentation,
30
+ } from './version-history.js'
37
31
  import { ViewMenu } from './view-menu.js'
38
32
  import type { ContentLocaleOption } from './view-menu.js'
39
33
 
40
- /**
41
- * Resolve a column value from a document, checking `fields` first (user-defined
42
- * collection fields) then the root (metadata like status, updated_at).
43
- */
44
- function getColumnValue(document: any, fieldName: string): any {
45
- if (document.fields && fieldName in document.fields) {
46
- return document.fields[fieldName]
34
+ /** Resolve user fields before root document metadata such as status. */
35
+ function getColumnValue(document: Record<string, unknown>, fieldName: string): unknown {
36
+ if (document.fields && typeof document.fields === 'object' && fieldName in document.fields) {
37
+ return (document.fields as Record<string, unknown>)[fieldName]
47
38
  }
48
39
  return document[fieldName]
49
40
  }
50
41
 
51
- // Lazy-load DiffModal because react-diff-viewer-continued uses a web worker
52
- // bundle that cannot be resolved by Node during SSR.
53
- const DiffModal = lazy(() => import('@byline/admin/react').then((m) => ({ default: m.DiffModal })))
54
-
55
- /**
56
- * Safely extract a displayable string from a field value that may be a plain
57
- * string or a locale-keyed object (when locale='all').
58
- * Falls back to the supplied default content locale, then the first available value.
59
- */
42
+ /** Resolve a scalar display value from a per-locale history response. */
60
43
  function resolveDisplayValue(
61
44
  value: unknown,
62
45
  locale: string | undefined,
@@ -74,40 +57,6 @@ function resolveDisplayValue(
74
57
  return String(value)
75
58
  }
76
59
 
77
- function Stats({ total }: { total: number }) {
78
- return (
79
- <span className={cx('byline-coll-history-stats', styles.stats)}>
80
- {formatNumber(total as number, 0)}
81
- </span>
82
- )
83
- }
84
-
85
- function padRows(value: number) {
86
- return Array.from({ length: value }).map((_, index) => (
87
- <div
88
- key={`empty-row-${
89
- // biome-ignore lint/suspicious/noArrayIndexKey: we're okay here
90
- index
91
- }`}
92
- className={cx('byline-coll-history-pad-row', styles.padRow)}
93
- >
94
- &nbsp;
95
- </div>
96
- ))
97
- }
98
-
99
- /**
100
- * Maps `event_type` values to audit-strip action labels. Unknown event
101
- * types fall back to the raw value rather than a missing-key warning.
102
- */
103
- const AUDIT_ACTION_KEYS: Record<string, string> = {
104
- create: 'collections.history.audit.actionCreate',
105
- update: 'collections.history.audit.actionUpdate',
106
- restore: 'collections.history.audit.actionRestore',
107
- copy_to_locale: 'collections.history.audit.actionCopyToLocale',
108
- delete_locale: 'collections.history.audit.actionDeleteLocale',
109
- }
110
-
111
60
  export const HistoryView = ({
112
61
  collectionDefinition,
113
62
  adminConfig,
@@ -118,21 +67,13 @@ export const HistoryView = ({
118
67
  contentLocales,
119
68
  defaultContentLocale,
120
69
  }: {
121
- collectionDefinition: CollectionDefinition
70
+ collectionDefinition: MultiCollectionDefinition
122
71
  adminConfig?: CollectionAdminConfig
123
72
  data: AnyCollectionSchemaTypes['HistoryType'] & {
124
- /**
125
- * Audit display labels (the acting user per version), resolved admin-side from each
126
- * version's `createdBy` id (see docs/07-auth-and-security/02-auditability.md — Workstream 1). Ids
127
- * absent from the map belong to deleted users.
128
- */
73
+ /** Admin-resolved acting-user labels keyed by raw version actor id. */
129
74
  actors?: Record<string, { label: string }>
130
75
  }
131
- /**
132
- * Document-grain audit log for the "Document history" tab (docs/07-auth-and-security/02-auditability.md —
133
- * Workstream 3): the non-versioned path / available-locales / status
134
- * changes and the deletion event, with admin-resolved actor labels.
135
- */
76
+ /** Collection-only document-grain audit log. */
136
77
  auditLog?: DocumentHistoryData
137
78
  workflowStatuses?: WorkflowStatus[]
138
79
  currentDocument?: Record<string, unknown> | null
@@ -143,63 +84,150 @@ export const HistoryView = ({
143
84
  from: getAdminRouteId('collections', '$collection', '$id', 'history') as never,
144
85
  }) as { id: string; collection: string }
145
86
  const navigate = useNavigate()
87
+ const router = useRouter()
146
88
  const { getCollectionDocumentVersion } = useBylineAdminServices()
147
89
  const { t } = useTranslation('byline-admin')
148
90
  const columns = adminConfig?.columns || []
149
91
  const { labels } = collectionDefinition
150
- // The identity column — drives the clickable compare cell and the
151
- // injected restore column, mirroring list.tsx. `useAsTitle` is optional;
152
- // when absent, those affordances (and the strip colspan's +1) turn off.
92
+ // Preserve the collection view's existing identity-column contract: its
93
+ // restore cell sits immediately after the configured `useAsTitle` column.
94
+ // Singleton history supplies a separate fixed projection in its wrapper.
153
95
  const titleFieldName = collectionDefinition.useAsTitle
154
- const location = useRouterState({ select: (s) => s.location })
96
+ const hasRestoreColumn =
97
+ titleFieldName != null && columns.some((column) => column.fieldName === titleFieldName)
98
+ const location = useRouterState({ select: (state) => state.location })
155
99
  const locale = (location.search as { locale?: string }).locale
156
- // Which sub-view is active (docs/07-auth-and-security/02-auditability.md — W3). Absent / anything but
157
- // 'document' → the content version stream.
158
100
  const activeTab =
159
101
  (location.search as { tab?: string }).tab === 'document' ? 'document' : 'versions'
160
- const [selectedVersion, setSelectedVersion] = useState<{
161
- versionId: string
162
- label: string
163
- } | null>(null)
164
- const [restoreTarget, setRestoreTarget] = useState<{
165
- versionId: string
166
- label: string
167
- versionNumber: number
168
- } | null>(null)
169
- const currentVersionId =
170
- currentDocument && typeof currentDocument.versionId === 'string'
171
- ? (currentDocument.versionId as string)
172
- : null
173
102
 
174
103
  function handleTabChange(name: string): void {
175
- // Switching tabs resets pagination; 'versions' (the default) drops the
176
- // `tab` param entirely to keep the URL clean.
177
104
  const params = structuredClone(location.search)
178
105
  delete params.page
179
- if (name === 'document') {
180
- params.tab = 'document'
181
- } else {
182
- delete params.tab
183
- }
106
+ if (name === 'document') params.tab = 'document'
107
+ else delete params.tab
184
108
  navigate({
185
109
  to: getAdminRoutePath('collections', '$collection', '$id', 'history'),
186
- params: { collection, id } as never,
110
+ params: { collection, id },
187
111
  search: params,
188
112
  })
189
113
  }
190
114
 
191
- function handleOnPageSizeChange(value: string | null): void {
192
- if (typeof value !== 'string' || value.length === 0) return
115
+ function handlePageSizeChange(pageSize: number): void {
193
116
  const params = structuredClone(location.search)
194
117
  delete params.page
195
- params.page_size = Number.parseInt(value, 10)
118
+ params.page_size = pageSize
196
119
  navigate({
197
120
  to: getAdminRoutePath('collections', '$collection', '$id', 'history'),
198
- params: { collection, id } as never,
121
+ params: { collection, id },
199
122
  search: params,
200
123
  })
201
124
  }
202
125
 
126
+ const rowPresentation: VersionHistoryRowPresentation = {
127
+ headers: columns.flatMap((column) => {
128
+ const cell = (
129
+ <TableHeadingCellSortable
130
+ key={String(column.fieldName)}
131
+ fieldName={String(column.fieldName)}
132
+ label={column.label}
133
+ sortable={column.sortable}
134
+ scope="col"
135
+ align={column.align}
136
+ className={column.className}
137
+ />
138
+ )
139
+ return titleFieldName != null && column.fieldName === titleFieldName
140
+ ? [
141
+ cell,
142
+ <th
143
+ key="__restore"
144
+ scope="col"
145
+ className={cx('byline-coll-history-col-restore', styles.colRestore)}
146
+ />,
147
+ ]
148
+ : [cell]
149
+ }),
150
+ auditColSpan: columns.length + (hasRestoreColumn ? 1 : 0),
151
+ renderCells: ({
152
+ document,
153
+ versionId,
154
+ versionNumber,
155
+ currentVersionId,
156
+ restoreStatusLabel,
157
+ openCompare,
158
+ openRestore,
159
+ }) =>
160
+ columns.flatMap((column) => {
161
+ const value = getColumnValue(document, String(column.fieldName))
162
+ const dataCell = (
163
+ <Table.Cell
164
+ key={String(column.fieldName)}
165
+ className={cx({
166
+ 'byline-coll-history-cell-right': column.align === 'right',
167
+ [styles.cellRight]: column.align === 'right',
168
+ 'byline-coll-history-cell-center': column.align === 'center',
169
+ [styles.cellCenter]: column.align === 'center',
170
+ })}
171
+ >
172
+ {titleFieldName != null && column.fieldName === titleFieldName ? (
173
+ versionId && currentDocument ? (
174
+ <button
175
+ type="button"
176
+ className={cx('byline-coll-history-title-button', styles.titleButton)}
177
+ onClick={openCompare}
178
+ >
179
+ {column.formatter
180
+ ? renderFormatted(value, document, column.formatter)
181
+ : resolveDisplayValue(value, locale, defaultContentLocale) || '------'}
182
+ </button>
183
+ ) : (
184
+ <Link
185
+ to={getAdminRoutePath('collections', '$collection', '$id')}
186
+ params={{ collection, id: document.id }}
187
+ >
188
+ {column.formatter
189
+ ? renderFormatted(value, document, column.formatter)
190
+ : resolveDisplayValue(value, locale, defaultContentLocale) || '------'}
191
+ </Link>
192
+ )
193
+ ) : column.formatter ? (
194
+ renderFormatted(value, document, column.formatter)
195
+ ) : column.fieldName === 'status' && workflowStatuses ? (
196
+ <StatusBadge status={document.status} workflowStatuses={workflowStatuses} />
197
+ ) : (
198
+ resolveDisplayValue(value, locale, defaultContentLocale) || ''
199
+ )}
200
+ </Table.Cell>
201
+ )
202
+
203
+ return titleFieldName != null && column.fieldName === titleFieldName
204
+ ? [
205
+ dataCell,
206
+ <Table.Cell
207
+ key="__restore"
208
+ className={cx('byline-coll-history-restore-cell', styles.restoreCell)}
209
+ >
210
+ {versionId && versionId !== currentVersionId ? (
211
+ <Button
212
+ type="button"
213
+ variant="outlined"
214
+ size="xs"
215
+ intent="noeffect"
216
+ onClick={openRestore}
217
+ className={cx('byline-coll-history-restore-button', styles.restoreButton)}
218
+ title={t('collections.history.restoreButtonTitle', {
219
+ status: restoreStatusLabel,
220
+ })}
221
+ >
222
+ {t('collections.history.restoreButton')}
223
+ </Button>
224
+ ) : null}
225
+ </Table.Cell>,
226
+ ]
227
+ : [dataCell]
228
+ }),
229
+ }
230
+
203
231
  return (
204
232
  <>
205
233
  <Section>
@@ -207,8 +235,8 @@ export const HistoryView = ({
207
235
  <div className={cx('byline-coll-history-head', styles.head)}>
208
236
  <h2 className={cx('byline-coll-history-title', styles.title)}>
209
237
  {t('collections.history.title', { label: labels.singular })}{' '}
210
- <Stats
211
- total={activeTab === 'document' ? (auditLog?.meta.total ?? 0) : data?.meta.total}
238
+ <HistoryStats
239
+ total={activeTab === 'document' ? (auditLog?.meta.total ?? 0) : data.meta.total}
212
240
  />
213
241
  </h2>
214
242
  <ViewMenu
@@ -231,348 +259,40 @@ export const HistoryView = ({
231
259
  />
232
260
  </Container>
233
261
  </Section>
234
- {activeTab === 'document' && (
262
+
263
+ {activeTab === 'document' ? (
235
264
  <DocumentHistoryView
236
265
  data={
237
266
  auditLog ?? { entries: [], meta: { total: 0, page: 1, pageSize: 0, totalPages: 0 } }
238
267
  }
239
268
  />
240
- )}
241
-
242
- {activeTab !== 'document' && (
269
+ ) : (
243
270
  <Section>
244
271
  <Container>
245
- <div className={cx('byline-coll-history-options', styles.options)}>
246
- <RouterPager
247
- page={data?.meta.page}
248
- count={data?.meta.totalPages}
249
- showFirstButton
250
- showLastButton
251
- componentName="pagerTop"
252
- aria-label={t('collections.list.pagerTopAriaLabel')}
253
- />
254
- </div>
255
- <Table.Container className={cx('byline-coll-history-table-wrap', styles.tableWrap)}>
256
- <Table>
257
- <Table.Header>
258
- <Table.Row>
259
- <th
260
- scope="col"
261
- className={cx('byline-coll-history-col-version', styles.colVersion)}
262
- />
263
- {columns.flatMap((column) => {
264
- const cell = (
265
- <TableHeadingCellSortable
266
- key={String(column.fieldName)}
267
- fieldName={String(column.fieldName)}
268
- label={column.label}
269
- sortable={column.sortable}
270
- scope="col"
271
- align={column.align}
272
- className={column.className}
273
- />
274
- )
275
- if (titleFieldName != null && column.fieldName === titleFieldName) {
276
- return [
277
- cell,
278
- <th
279
- key="__restore"
280
- scope="col"
281
- className={cx('byline-coll-history-col-restore', styles.colRestore)}
282
- />,
283
- ]
284
- }
285
- return [cell]
286
- })}
287
- </Table.Row>
288
- </Table.Header>
289
-
290
- <Table.Body>
291
- {data?.docs?.map((document, rowIndex) => {
292
- const versionId = document.versionId
293
- const { total, page, pageSize, desc } = data.meta
294
- const versionNumber = desc
295
- ? total - (page - 1) * pageSize - rowIndex
296
- : (page - 1) * pageSize + rowIndex + 1
297
- // Audit strip (docs/07-auth-and-security/02-auditability.md — W1): who created this
298
- // version, via which action. A present-but-unresolved id
299
- // is a deleted user; an absent id is a row written before
300
- // audit wiring or an internal-tooling write.
301
- const actorLabel = document.createdBy
302
- ? (data.actors?.[document.createdBy]?.label ??
303
- t('collections.history.audit.formerUser'))
304
- : t('collections.history.audit.unknown')
305
- const actionKey = document.eventType
306
- ? AUDIT_ACTION_KEYS[document.eventType]
307
- : undefined
308
- const actionLabel = actionKey ? t(actionKey) : (document.eventType ?? '')
309
- // The strip starts on the second column — an empty spacer
310
- // cell sits under the version-number column, then one cell
311
- // spans the data columns plus the restore cell appended
312
- // after the identity column when present.
313
- const auditColSpan =
314
- columns.length +
315
- (titleFieldName != null && columns.some((c) => c.fieldName === titleFieldName)
316
- ? 1
317
- : 0)
318
- return (
319
- <Fragment key={versionId ?? document.id}>
320
- <Table.Row className={cx('byline-coll-history-row', styles.historyRow)}>
321
- <Table.Cell
322
- className={cx('byline-coll-history-version-cell', styles.versionCell)}
323
- >
324
- {versionId && currentDocument ? (
325
- <IconButton
326
- size="xs"
327
- variant="outlined"
328
- intent="noeffect"
329
- aria-label={t('collections.history.compareAriaLabel')}
330
- title={t('collections.history.compareTitle')}
331
- className={cx(
332
- 'byline-coll-history-version-button',
333
- styles.versionButton
334
- )}
335
- onClick={() =>
336
- setSelectedVersion({
337
- versionId,
338
- label: new Date(document.createdAt).toLocaleString(),
339
- })
340
- }
341
- >
342
- {versionNumber}
343
- </IconButton>
344
- ) : null}
345
- </Table.Cell>
346
- {columns.flatMap((column) => {
347
- const dataCell = (
348
- <Table.Cell
349
- key={String(column.fieldName)}
350
- className={cx({
351
- 'byline-coll-history-cell-right': column.align === 'right',
352
- [styles.cellRight]: column.align === 'right',
353
- 'byline-coll-history-cell-center': column.align === 'center',
354
- [styles.cellCenter]: column.align === 'center',
355
- })}
356
- >
357
- {titleFieldName != null && column.fieldName === titleFieldName ? (
358
- versionId && currentDocument ? (
359
- <button
360
- type="button"
361
- className={cx(
362
- 'byline-coll-history-title-button',
363
- styles.titleButton
364
- )}
365
- onClick={() =>
366
- setSelectedVersion({
367
- versionId,
368
- label: new Date(document.createdAt).toLocaleString(),
369
- })
370
- }
371
- >
372
- {column.formatter
373
- ? renderFormatted(
374
- getColumnValue(document, column.fieldName as string),
375
- document,
376
- column.formatter
377
- )
378
- : resolveDisplayValue(
379
- getColumnValue(document, column.fieldName as string),
380
- locale,
381
- defaultContentLocale
382
- ) || '------'}
383
- </button>
384
- ) : (
385
- <Link
386
- to={getAdminRoutePath('collections', '$collection', '$id')}
387
- params={{
388
- collection,
389
- id: document.id,
390
- }}
391
- >
392
- {column.formatter
393
- ? renderFormatted(
394
- getColumnValue(document, column.fieldName as string),
395
- document,
396
- column.formatter
397
- )
398
- : resolveDisplayValue(
399
- getColumnValue(document, column.fieldName as string),
400
- locale,
401
- defaultContentLocale
402
- ) || '------'}
403
- </Link>
404
- )
405
- ) : column.formatter ? (
406
- renderFormatted(
407
- getColumnValue(document, column.fieldName as string),
408
- document,
409
- column.formatter
410
- )
411
- ) : column.fieldName === 'status' && workflowStatuses ? (
412
- <StatusBadge
413
- status={document.status}
414
- workflowStatuses={workflowStatuses}
415
- />
416
- ) : (
417
- resolveDisplayValue(
418
- getColumnValue(document, column.fieldName as string),
419
- locale,
420
- defaultContentLocale
421
- ) || ''
422
- )}
423
- </Table.Cell>
424
- )
425
- if (titleFieldName != null && column.fieldName === titleFieldName) {
426
- return [
427
- dataCell,
428
- <Table.Cell
429
- key="__restore"
430
- className={cx(
431
- 'byline-coll-history-restore-cell',
432
- styles.restoreCell
433
- )}
434
- >
435
- {versionId && versionId !== currentVersionId ? (
436
- <Button
437
- type="button"
438
- variant="outlined"
439
- size="xs"
440
- intent="noeffect"
441
- onClick={() =>
442
- setRestoreTarget({
443
- versionId,
444
- label: new Date(document.createdAt).toLocaleString(),
445
- versionNumber,
446
- })
447
- }
448
- className={cx(
449
- 'byline-coll-history-restore-button',
450
- styles.restoreButton
451
- )}
452
- title={t('collections.history.restoreButtonTitle')}
453
- >
454
- {t('collections.history.restoreButton')}
455
- </Button>
456
- ) : null}
457
- </Table.Cell>,
458
- ]
459
- }
460
- return [dataCell]
461
- })}
462
- </Table.Row>
463
- <Table.Row
464
- className={cx('byline-coll-history-audit-row', styles.auditRow)}
465
- aria-label={t('collections.history.audit.createdBy', {
466
- label: actorLabel,
467
- })}
468
- >
469
- <Table.Cell
470
- className={cx(
471
- 'byline-coll-history-audit-spacer-cell',
472
- styles.auditSpacerCell
473
- )}
474
- />
475
- <Table.Cell
476
- colSpan={auditColSpan}
477
- className={cx('byline-coll-history-audit-cell', styles.auditCell)}
478
- >
479
- <span className={cx('byline-coll-history-audit', styles.audit)}>
480
- {actionLabel}
481
- {' · '}
482
- {t('collections.history.audit.createdBy', { label: actorLabel })}
483
- {' · '}
484
- {new Date(document.createdAt).toLocaleString()}
485
- </span>
486
- </Table.Cell>
487
- </Table.Row>
488
- </Fragment>
489
- )
490
- })}
491
- </Table.Body>
492
- </Table>
493
- {padRows(6 - (data?.docs?.length ?? 0))}
494
- </Table.Container>
495
- <div
496
- className={cx(
497
- 'byline-coll-history-options byline-coll-history-options-bottom',
498
- styles.options,
499
- styles.optionsBottom
500
- )}
501
- >
502
- <Select<string>
503
- containerClassName={cx('byline-coll-history-page-size', styles.pageSize)}
504
- id="page_size"
505
- name="page_size"
506
- size="sm"
507
- defaultValue="15"
508
- items={[
509
- { value: '15', label: '15' },
510
- { value: '30', label: '30' },
511
- { value: '50', label: '50' },
512
- { value: '100', label: '100' },
513
- ]}
514
- onValueChange={handleOnPageSizeChange}
515
- />
516
- <RouterPager
517
- smoothScrollToTop={true}
518
- page={data?.meta.page}
519
- count={data?.meta.totalPages}
520
- showFirstButton
521
- showLastButton
522
- componentName="pagerBottom"
523
- aria-label={t('collections.list.pagerBottomAriaLabel')}
524
- />
525
- </div>
272
+ <VersionHistoryCore
273
+ data={data as unknown as VersionHistoryData}
274
+ resourceDefinition={collectionDefinition}
275
+ resourcePath={collection}
276
+ documentId={id}
277
+ currentDocument={currentDocument}
278
+ locale={locale}
279
+ rowPresentation={rowPresentation}
280
+ loadHistoricalVersion={getCollectionDocumentVersion}
281
+ onPageSizeChange={handlePageSizeChange}
282
+ restoreVersion={(versionId) =>
283
+ restoreDocumentVersion({ data: { collection, id, versionId } })
284
+ }
285
+ onRestoreComplete={async () => {
286
+ await router.invalidate()
287
+ navigate({
288
+ to: getAdminRoutePath('collections', '$collection', '$id'),
289
+ params: { collection, id },
290
+ })
291
+ }}
292
+ />
526
293
  </Container>
527
294
  </Section>
528
295
  )}
529
-
530
- {selectedVersion && currentDocument && (
531
- <Suspense fallback={null}>
532
- <DiffModal
533
- isOpen={true}
534
- onDismiss={() => setSelectedVersion(null)}
535
- collection={collection}
536
- documentId={id}
537
- versionId={selectedVersion.versionId}
538
- versionLabel={selectedVersion.label}
539
- currentDocument={currentDocument}
540
- locale={locale}
541
- loadHistoricalVersion={getCollectionDocumentVersion}
542
- />
543
- </Suspense>
544
- )}
545
-
546
- <Modal
547
- isOpen={restoreTarget != null}
548
- onDismiss={() => setRestoreTarget(null)}
549
- closeOnOverlayClick={false}
550
- >
551
- <Modal.Container className={cx('byline-coll-history-restore-modal', styles.restoreModal)}>
552
- <Modal.Header
553
- className={cx('byline-coll-history-restore-modal-head', styles.restoreModalHead)}
554
- >
555
- <h3 className="m-0">{t('collections.history.restoreModalTitle')}</h3>
556
- <IconButton
557
- aria-label={t('common.actions.close')}
558
- size="xs"
559
- onClick={() => setRestoreTarget(null)}
560
- >
561
- <CloseIcon width="14px" height="14px" svgClassName="white-icon" />
562
- </IconButton>
563
- </Modal.Header>
564
- {restoreTarget ? (
565
- <RestoreVersionModal
566
- collection={collection}
567
- documentId={id}
568
- versionId={restoreTarget.versionId}
569
- versionLabel={restoreTarget.label}
570
- versionNumber={restoreTarget.versionNumber}
571
- onClose={() => setRestoreTarget(null)}
572
- />
573
- ) : null}
574
- </Modal.Container>
575
- </Modal>
576
296
  </>
577
297
  )
578
298
  }