@byline/host-tanstack-start 4.15.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 (183) hide show
  1. package/dist/admin-shell/chrome/admin-menu-drawer.d.ts +2 -1
  2. package/dist/admin-shell/chrome/admin-menu-drawer.js +17 -5
  3. package/dist/admin-shell/chrome/dashboard.js +41 -5
  4. package/dist/admin-shell/chrome/dashboard.test.d.ts +8 -0
  5. package/dist/admin-shell/collections/api.d.ts +2 -2
  6. package/dist/admin-shell/collections/create.d.ts +2 -2
  7. package/dist/admin-shell/collections/create.js +1 -0
  8. package/dist/admin-shell/collections/edit.d.ts +2 -2
  9. package/dist/admin-shell/collections/edit.js +1 -0
  10. package/dist/admin-shell/collections/history.d.ts +4 -12
  11. package/dist/admin-shell/collections/history.js +112 -303
  12. package/dist/admin-shell/collections/reindex-button.d.ts +1 -1
  13. package/dist/admin-shell/collections/reindex-button.js +5 -1
  14. package/dist/admin-shell/collections/restore-version-modal.d.ts +4 -4
  15. package/dist/admin-shell/collections/restore-version-modal.js +8 -24
  16. package/dist/admin-shell/collections/version-history.d.ts +72 -0
  17. package/dist/admin-shell/collections/version-history.js +259 -0
  18. package/dist/admin-shell/singletons/history.d.ts +19 -0
  19. package/dist/admin-shell/singletons/history.js +170 -0
  20. package/dist/admin-shell/singletons/history.test.d.ts +8 -0
  21. package/dist/admin-shell/singletons/preview-link.d.ts +12 -0
  22. package/dist/admin-shell/singletons/preview-link.js +54 -0
  23. package/dist/admin-shell/singletons/view-menu.d.ts +13 -0
  24. package/dist/admin-shell/singletons/view-menu.js +116 -0
  25. package/dist/admin-shell/singletons/view.d.ts +18 -0
  26. package/dist/admin-shell/singletons/view.js +259 -0
  27. package/dist/admin-shell/singletons/view.test.d.ts +8 -0
  28. package/dist/integrations/analytics-agent.d.ts +20 -0
  29. package/dist/integrations/analytics-agent.js +57 -0
  30. package/dist/integrations/analytics-events.d.ts +51 -0
  31. package/dist/integrations/analytics-events.js +129 -0
  32. package/dist/integrations/api-utils.d.ts +18 -4
  33. package/dist/integrations/api-utils.js +10 -3
  34. package/dist/integrations/serve-analytics-agent.d.ts +8 -0
  35. package/dist/integrations/serve-analytics-agent.js +11 -0
  36. package/dist/routes/analytics-range.d.ts +12 -0
  37. package/dist/routes/analytics-range.js +35 -0
  38. package/dist/routes/create-admin-analytics-route.d.ts +8 -0
  39. package/dist/routes/create-admin-analytics-route.js +108 -0
  40. package/dist/routes/create-admin-dashboard-route.js +2 -2
  41. package/dist/routes/create-admin-layout-route.js +10 -4
  42. package/dist/routes/create-collection-api-route.js +3 -3
  43. package/dist/routes/create-collection-create-route.js +4 -3
  44. package/dist/routes/create-collection-edit-route.js +4 -3
  45. package/dist/routes/create-collection-history-route.js +4 -3
  46. package/dist/routes/create-collection-list-route.js +4 -3
  47. package/dist/routes/create-singleton-history-route.d.ts +8 -0
  48. package/dist/routes/create-singleton-history-route.js +99 -0
  49. package/dist/routes/create-singleton-route.d.ts +35 -0
  50. package/dist/routes/create-singleton-route.js +130 -0
  51. package/dist/routes/get-multi-collection-definition.d.ts +10 -0
  52. package/dist/routes/get-multi-collection-definition.js +6 -0
  53. package/dist/routes/get-singleton-definition.d.ts +10 -0
  54. package/dist/routes/get-singleton-definition.js +6 -0
  55. package/dist/routes/index.d.ts +3 -0
  56. package/dist/routes/index.js +3 -0
  57. package/dist/server-fns/actors.d.ts +16 -0
  58. package/dist/server-fns/actors.js +25 -0
  59. package/dist/server-fns/admin-document-presentation.d.ts +30 -0
  60. package/dist/server-fns/admin-document-presentation.js +32 -0
  61. package/dist/server-fns/analytics/index.d.ts +9 -0
  62. package/dist/server-fns/analytics/index.js +2 -0
  63. package/dist/server-fns/analytics/maintenance.d.ts +22 -0
  64. package/dist/server-fns/analytics/maintenance.js +42 -0
  65. package/dist/server-fns/analytics/queries.d.ts +25 -0
  66. package/dist/server-fns/analytics/queries.js +54 -0
  67. package/dist/server-fns/collections/actors.d.ts +1 -10
  68. package/dist/server-fns/collections/actors.js +1 -25
  69. package/dist/server-fns/collections/get.js +9 -26
  70. package/dist/server-fns/collections/reorder.js +1 -1
  71. package/dist/server-fns/collections/scheduled-publication.js +2 -2
  72. package/dist/server-fns/collections/upload.js +3 -3
  73. package/dist/server-fns/collections/utils.d.ts +1 -7
  74. package/dist/server-fns/collections/utils.js +1 -4
  75. package/dist/server-fns/serialise.d.ts +14 -0
  76. package/dist/server-fns/serialise.js +4 -0
  77. package/dist/server-fns/singleton-document-read.d.ts +20 -0
  78. package/dist/server-fns/singleton-document-read.js +10 -0
  79. package/dist/server-fns/singletons/cancel-scheduled-publish.d.ts +12 -0
  80. package/dist/server-fns/singletons/cancel-scheduled-publish.js +7 -0
  81. package/dist/server-fns/singletons/change-status.d.ts +14 -0
  82. package/dist/server-fns/singletons/change-status.js +7 -0
  83. package/dist/server-fns/singletons/confirm-scheduled-publish.d.ts +14 -0
  84. package/dist/server-fns/singletons/confirm-scheduled-publish.js +9 -0
  85. package/dist/server-fns/singletons/copy-to-locale.d.ts +18 -0
  86. package/dist/server-fns/singletons/copy-to-locale.js +11 -0
  87. package/dist/server-fns/singletons/find-by-version.d.ts +16 -0
  88. package/dist/server-fns/singletons/find-by-version.js +9 -0
  89. package/dist/server-fns/singletons/get-scheduled-publish.d.ts +12 -0
  90. package/dist/server-fns/singletons/get-scheduled-publish.js +7 -0
  91. package/dist/server-fns/singletons/get.d.ts +24 -0
  92. package/dist/server-fns/singletons/get.js +6 -0
  93. package/dist/server-fns/singletons/history.d.ts +31 -0
  94. package/dist/server-fns/singletons/history.js +22 -0
  95. package/dist/server-fns/singletons/index.d.ts +19 -0
  96. package/dist/server-fns/singletons/index.js +12 -0
  97. package/dist/server-fns/singletons/restore-version.d.ts +14 -0
  98. package/dist/server-fns/singletons/restore-version.js +7 -0
  99. package/dist/server-fns/singletons/schedule-publish.d.ts +16 -0
  100. package/dist/server-fns/singletons/schedule-publish.js +10 -0
  101. package/dist/server-fns/singletons/unpublish.d.ts +12 -0
  102. package/dist/server-fns/singletons/unpublish.js +7 -0
  103. package/dist/server-fns/singletons/update.d.ts +14 -0
  104. package/dist/server-fns/singletons/update.js +14 -0
  105. package/dist/vite/analytics-dev.d.ts +22 -0
  106. package/dist/vite/analytics-dev.js +28 -0
  107. package/dist/vite/index.d.ts +1 -0
  108. package/dist/vite/index.js +1 -0
  109. package/dist/vitest-jsdom.test.d.ts +8 -0
  110. package/package.json +28 -16
  111. package/src/admin-shell/chrome/admin-menu-drawer.tsx +19 -5
  112. package/src/admin-shell/chrome/dashboard.test.tsx +199 -0
  113. package/src/admin-shell/chrome/dashboard.tsx +43 -9
  114. package/src/admin-shell/collections/api.tsx +2 -2
  115. package/src/admin-shell/collections/create.tsx +6 -2
  116. package/src/admin-shell/collections/edit.tsx +6 -2
  117. package/src/admin-shell/collections/history.module.css +1 -1
  118. package/src/admin-shell/collections/history.tsx +162 -442
  119. package/src/admin-shell/collections/reindex-button.tsx +4 -2
  120. package/src/admin-shell/collections/restore-version-modal.tsx +19 -25
  121. package/src/admin-shell/collections/version-history.tsx +349 -0
  122. package/src/admin-shell/singletons/history.test.tsx +407 -0
  123. package/src/admin-shell/singletons/history.tsx +185 -0
  124. package/src/admin-shell/singletons/preview-link.tsx +75 -0
  125. package/src/admin-shell/singletons/view-menu.tsx +136 -0
  126. package/src/admin-shell/singletons/view.test.tsx +400 -0
  127. package/src/admin-shell/singletons/view.tsx +351 -0
  128. package/src/integrations/analytics-agent.tsx +91 -0
  129. package/src/integrations/analytics-events.test.node.ts +245 -0
  130. package/src/integrations/analytics-events.ts +224 -0
  131. package/src/integrations/api-utils.ts +34 -9
  132. package/src/integrations/serve-analytics-agent.test.node.ts +20 -0
  133. package/src/integrations/serve-analytics-agent.ts +19 -0
  134. package/src/routes/analytics-range.ts +55 -0
  135. package/src/routes/create-admin-analytics-route.test.node.ts +46 -0
  136. package/src/routes/create-admin-analytics-route.tsx +109 -0
  137. package/src/routes/create-admin-dashboard-route.test.node.ts +70 -0
  138. package/src/routes/create-admin-dashboard-route.tsx +10 -2
  139. package/src/routes/create-admin-layout-route.tsx +15 -8
  140. package/src/routes/create-collection-api-route.tsx +4 -4
  141. package/src/routes/create-collection-create-route.tsx +5 -8
  142. package/src/routes/create-collection-edit-route.tsx +5 -4
  143. package/src/routes/create-collection-history-route.tsx +5 -8
  144. package/src/routes/create-collection-list-route.tsx +5 -8
  145. package/src/routes/create-singleton-history-route.test.node.ts +148 -0
  146. package/src/routes/create-singleton-history-route.tsx +116 -0
  147. package/src/routes/create-singleton-route.test.node.ts +172 -0
  148. package/src/routes/create-singleton-route.tsx +176 -0
  149. package/src/routes/get-multi-collection-definition.ts +16 -0
  150. package/src/routes/get-singleton-definition.ts +16 -0
  151. package/src/routes/index.ts +3 -0
  152. package/src/server-fns/actors.ts +43 -0
  153. package/src/server-fns/admin-document-presentation.ts +78 -0
  154. package/src/server-fns/analytics/index.ts +23 -0
  155. package/src/server-fns/analytics/maintenance.ts +54 -0
  156. package/src/server-fns/analytics/queries.ts +86 -0
  157. package/src/server-fns/collections/actors.ts +1 -52
  158. package/src/server-fns/collections/get.ts +10 -49
  159. package/src/server-fns/collections/reorder.ts +1 -1
  160. package/src/server-fns/collections/scheduled-publication.ts +7 -1
  161. package/src/server-fns/collections/upload-singleton.test.node.ts +164 -0
  162. package/src/server-fns/collections/upload.ts +7 -3
  163. package/src/server-fns/collections/utils.ts +1 -9
  164. package/src/server-fns/serialise.ts +17 -0
  165. package/src/server-fns/singleton-document-read.ts +32 -0
  166. package/src/server-fns/singletons/cancel-scheduled-publish.ts +19 -0
  167. package/src/server-fns/singletons/change-status.ts +19 -0
  168. package/src/server-fns/singletons/confirm-scheduled-publish.ts +23 -0
  169. package/src/server-fns/singletons/copy-to-locale.ts +32 -0
  170. package/src/server-fns/singletons/find-by-version.ts +23 -0
  171. package/src/server-fns/singletons/get-scheduled-publish.ts +19 -0
  172. package/src/server-fns/singletons/get.ts +32 -0
  173. package/src/server-fns/singletons/history.ts +41 -0
  174. package/src/server-fns/singletons/index.ts +20 -0
  175. package/src/server-fns/singletons/restore-version.ts +19 -0
  176. package/src/server-fns/singletons/schedule-publish.ts +24 -0
  177. package/src/server-fns/singletons/singletons.test.node.ts +298 -0
  178. package/src/server-fns/singletons/unpublish.ts +19 -0
  179. package/src/server-fns/singletons/update.ts +31 -0
  180. package/src/vite/analytics-dev.test.node.ts +126 -0
  181. package/src/vite/analytics-dev.ts +56 -0
  182. package/src/vite/index.ts +1 -0
  183. package/src/vitest-jsdom.test.tsx +27 -0
@@ -41,6 +41,7 @@ import { buildLocaleDefinitions } from '../i18n/locale-definitions.js'
41
41
  import { bylineAdminServices } from '../integrations/byline-admin-services.js'
42
42
  import { BylineAiAdminProvider } from '../integrations/byline-ai.js'
43
43
  import { bylineFieldServices } from '../integrations/byline-field-services.js'
44
+ import { getAnalyticsRuntime } from '../server-fns/analytics/index.js'
44
45
  import { getCurrentAdminUser } from '../server-fns/auth/index.js'
45
46
  import { getScheduledPublicationRuntime } from '../server-fns/collections/index.js'
46
47
  import { getActiveLocaleFn, setAdminLocaleFn } from '../server-fns/i18n/index.js'
@@ -58,9 +59,12 @@ export function createAdminLayoutRoute(path: string) {
58
59
  // than calling `resolveRequestLocale` directly) keeps
59
60
  // `@tanstack/react-start/server` out of the client bundle — the
60
61
  // same pattern `getCurrentAdminUser` uses for `getAdminRequestContext`.
61
- const activeLocale = await getActiveLocaleFn()
62
- const scheduledPublicationRuntime = await getScheduledPublicationRuntime()
63
- return { user, activeLocale, scheduledPublicationRuntime }
62
+ const [activeLocale, scheduledPublicationRuntime, analyticsRuntime] = await Promise.all([
63
+ getActiveLocaleFn(),
64
+ getScheduledPublicationRuntime(),
65
+ getAnalyticsRuntime(),
66
+ ])
67
+ return { user, activeLocale, scheduledPublicationRuntime, analyticsRuntime }
64
68
  } catch {
65
69
  // `getCurrentAdminUser` (via `getAdminRequestContext`) throws
66
70
  // `ERR_UNAUTHENTICATED` or a related auth error when no valid
@@ -73,11 +77,13 @@ export function createAdminLayoutRoute(path: string) {
73
77
  }
74
78
  },
75
79
  component: function AdminLayoutComponent() {
76
- const { user, activeLocale, scheduledPublicationRuntime } = Route.useRouteContext() as {
77
- user: Awaited<ReturnType<typeof getCurrentAdminUser>>
78
- activeLocale: LocaleCode
79
- scheduledPublicationRuntime: Awaited<ReturnType<typeof getScheduledPublicationRuntime>>
80
- }
80
+ const { user, activeLocale, scheduledPublicationRuntime, analyticsRuntime } =
81
+ Route.useRouteContext() as {
82
+ user: Awaited<ReturnType<typeof getCurrentAdminUser>>
83
+ activeLocale: LocaleCode
84
+ scheduledPublicationRuntime: Awaited<ReturnType<typeof getScheduledPublicationRuntime>>
85
+ analyticsRuntime: Awaited<ReturnType<typeof getAnalyticsRuntime>>
86
+ }
81
87
  const { i18n } = getAdminConfig()
82
88
  const localeDefinitions = buildLocaleDefinitions(
83
89
  i18n.admin.locales,
@@ -113,6 +119,7 @@ export function createAdminLayoutRoute(path: string) {
113
119
  <AdminAppBar user={user} />
114
120
  <main className={cx('byline-admin-layout-main', layoutStyles.main)}>
115
121
  <AdminMenuDrawer
122
+ analyticsEnabled={analyticsRuntime.enabled}
116
123
  scheduledPublicationEnabled={scheduledPublicationRuntime.enabled}
117
124
  />
118
125
  <Content>
@@ -8,8 +8,7 @@
8
8
 
9
9
  import { createFileRoute, notFound } from '@tanstack/react-router'
10
10
 
11
- import type { CollectionDefinition } from '@byline/core'
12
- import { getCollectionDefinition } from '@byline/core'
11
+ import type { MultiCollectionDefinition } from '@byline/core'
13
12
  import { useTranslation } from '@byline/i18n/react'
14
13
  import { z } from 'zod'
15
14
 
@@ -18,6 +17,7 @@ import { ApiView } from '../admin-shell/collections/api.js'
18
17
  import { getCollectionDocument } from '../server-fns/collections/index.js'
19
18
  import { getAdminRoutePath } from './admin-path.js'
20
19
  import { getContentLocaleRouteConfig } from './get-content-locale-route-config.js'
20
+ import { getMultiCollectionDefinition } from './get-multi-collection-definition.js'
21
21
 
22
22
  const searchSchema = z.object({
23
23
  locale: z.string().optional(),
@@ -44,7 +44,7 @@ export function createCollectionApiRoute(path: string) {
44
44
  params: { collection: string; id: string }
45
45
  deps: { locale?: string; depth?: number }
46
46
  }) => {
47
- const collectionDef = getCollectionDefinition(params.collection)
47
+ const collectionDef = getMultiCollectionDefinition(params.collection)
48
48
  if (!collectionDef) {
49
49
  throw notFound()
50
50
  }
@@ -74,7 +74,7 @@ export function createCollectionApiRoute(path: string) {
74
74
  const data = Route.useLoaderData()
75
75
  const { collection, id } = Route.useParams() as { collection: string; id: string }
76
76
  const { locale, depth } = Route.useSearch() as z.infer<typeof searchSchema>
77
- const collectionDef = getCollectionDefinition(collection) as CollectionDefinition
77
+ const collectionDef = getMultiCollectionDefinition(collection) as MultiCollectionDefinition
78
78
  const { t } = useTranslation('byline-admin')
79
79
  const { contentLocales, defaultContentLocale } = getContentLocaleRouteConfig()
80
80
 
@@ -8,18 +8,15 @@
8
8
 
9
9
  import { createFileRoute, notFound } from '@tanstack/react-router'
10
10
 
11
- import type { CollectionDefinition } from '@byline/core'
12
- import {
13
- buildInitialDataFromFields,
14
- getCollectionAdminConfig,
15
- getCollectionDefinition,
16
- } from '@byline/core'
11
+ import type { MultiCollectionDefinition } from '@byline/core'
12
+ import { buildInitialDataFromFields, getCollectionAdminConfig } from '@byline/core'
17
13
  import { useTranslation } from '@byline/i18n/react'
18
14
  import { z } from 'zod'
19
15
 
20
16
  import { BreadcrumbsClient } from '../admin-shell/chrome/breadcrumbs/breadcrumbs-client.js'
21
17
  import { CreateView } from '../admin-shell/collections/create.js'
22
18
  import { getAdminRoutePath } from './admin-path.js'
19
+ import { getMultiCollectionDefinition } from './get-multi-collection-definition.js'
23
20
 
24
21
  const searchSchema = z.object({
25
22
  /** URL-encoded list search state to return to on cancel — see list-return-state.ts. */
@@ -36,7 +33,7 @@ export function createCollectionCreateRoute(path: string) {
36
33
  params: { collection: string }
37
34
  // biome-ignore lint/suspicious/noExplicitAny: collection-specific shape
38
35
  }): Promise<{ initialData: any }> => {
39
- const collectionDef = getCollectionDefinition(params.collection)
36
+ const collectionDef = getMultiCollectionDefinition(params.collection)
40
37
  if (!collectionDef) {
41
38
  throw notFound()
42
39
  }
@@ -50,7 +47,7 @@ export function createCollectionCreateRoute(path: string) {
50
47
  const { collection } = Route.useParams() as { collection: string }
51
48
  // biome-ignore lint/suspicious/noExplicitAny: collection-specific shape
52
49
  const { initialData } = Route.useLoaderData() as { initialData: any }
53
- const collectionDef = getCollectionDefinition(collection) as CollectionDefinition
50
+ const collectionDef = getMultiCollectionDefinition(collection) as MultiCollectionDefinition
54
51
  const adminConfig = getCollectionAdminConfig(collection)
55
52
  const { t } = useTranslation('byline-admin')
56
53
  const search = Route.useSearch() as z.infer<typeof searchSchema>
@@ -9,8 +9,8 @@
9
9
  import { useEffect, useRef, useState } from 'react'
10
10
  import { createFileRoute, notFound } from '@tanstack/react-router'
11
11
 
12
- import type { CollectionDefinition } from '@byline/core'
13
- import { getCollectionAdminConfig, getCollectionDefinition } from '@byline/core'
12
+ import type { MultiCollectionDefinition } from '@byline/core'
13
+ import { getCollectionAdminConfig } from '@byline/core'
14
14
  import { useTranslation } from '@byline/i18n/react'
15
15
  import { useToastManager } from '@byline/ui/react'
16
16
  import { z } from 'zod'
@@ -21,6 +21,7 @@ import { EditView } from '../admin-shell/collections/edit.js'
21
21
  import { getCollectionDocument } from '../server-fns/collections/index.js'
22
22
  import { getAdminRoutePath } from './admin-path.js'
23
23
  import { getContentLocaleRouteConfig } from './get-content-locale-route-config.js'
24
+ import { getMultiCollectionDefinition } from './get-multi-collection-definition.js'
24
25
  import { decodeListReturnState } from './list-return-state.js'
25
26
  import { persistListReturnState, readListReturnState } from './list-return-storage.js'
26
27
 
@@ -46,7 +47,7 @@ export function createCollectionEditRoute(path: string) {
46
47
  params: { collection: string; id: string }
47
48
  deps: { locale?: string }
48
49
  }) => {
49
- const collectionDef = getCollectionDefinition(params.collection)
50
+ const collectionDef = getMultiCollectionDefinition(params.collection)
50
51
  if (!collectionDef) {
51
52
  throw notFound()
52
53
  }
@@ -77,7 +78,7 @@ export function createCollectionEditRoute(path: string) {
77
78
  const { collection, id } = Route.useParams() as { collection: string; id: string }
78
79
  const search = Route.useSearch() as z.infer<typeof searchSchema>
79
80
  const { locale } = search
80
- const collectionDef = getCollectionDefinition(collection) as CollectionDefinition
81
+ const collectionDef = getMultiCollectionDefinition(collection) as MultiCollectionDefinition
81
82
  const adminConfig = getCollectionAdminConfig(collection)
82
83
  const { t } = useTranslation('byline-admin')
83
84
  const toastManager = useToastManager()
@@ -8,12 +8,8 @@
8
8
 
9
9
  import { createFileRoute, notFound } from '@tanstack/react-router'
10
10
 
11
- import type { CollectionDefinition } from '@byline/core'
12
- import {
13
- getCollectionAdminConfig,
14
- getCollectionDefinition,
15
- getWorkflowStatuses,
16
- } from '@byline/core'
11
+ import type { MultiCollectionDefinition } from '@byline/core'
12
+ import { getCollectionAdminConfig, getWorkflowStatuses } from '@byline/core'
17
13
  import { useTranslation } from '@byline/i18n/react'
18
14
  import { z } from 'zod'
19
15
 
@@ -26,6 +22,7 @@ import {
26
22
  } from '../server-fns/collections/index.js'
27
23
  import { getAdminRoutePath } from './admin-path.js'
28
24
  import { getContentLocaleRouteConfig } from './get-content-locale-route-config.js'
25
+ import { getMultiCollectionDefinition } from './get-multi-collection-definition.js'
29
26
 
30
27
  const searchSchema = z.object({
31
28
  page: z.coerce.number().min(1).optional(),
@@ -68,7 +65,7 @@ export function createCollectionHistoryRoute(path: string) {
68
65
  locale?: string
69
66
  }
70
67
  }) => {
71
- const collectionDef = getCollectionDefinition(params.collection)
68
+ const collectionDef = getMultiCollectionDefinition(params.collection)
72
69
  if (!collectionDef) {
73
70
  throw notFound()
74
71
  }
@@ -111,7 +108,7 @@ export function createCollectionHistoryRoute(path: string) {
111
108
  component: function CollectionHistoryComponent() {
112
109
  const { history, currentDocument, auditLog } = Route.useLoaderData()
113
110
  const { collection } = Route.useParams() as { collection: string; id: string }
114
- const collectionDef = getCollectionDefinition(collection) as CollectionDefinition
111
+ const collectionDef = getMultiCollectionDefinition(collection) as MultiCollectionDefinition
115
112
  const adminConfig = getCollectionAdminConfig(collection)
116
113
  const { t } = useTranslation('byline-admin')
117
114
  const { contentLocales, defaultContentLocale } = getContentLocaleRouteConfig()
@@ -10,12 +10,8 @@ import { useEffect, useRef } from 'react'
10
10
  import { createFileRoute, notFound } from '@tanstack/react-router'
11
11
 
12
12
  import { applyRelationColumnFormatters } from '@byline/admin/react'
13
- import type { CollectionDefinition } from '@byline/core'
14
- import {
15
- getCollectionAdminConfig,
16
- getCollectionDefinition,
17
- getWorkflowStatuses,
18
- } from '@byline/core'
13
+ import type { MultiCollectionDefinition } from '@byline/core'
14
+ import { getCollectionAdminConfig, getWorkflowStatuses } from '@byline/core'
19
15
  import { useTranslation } from '@byline/i18n/react'
20
16
  import { useToastManager } from '@byline/ui/react'
21
17
  import type { z } from 'zod'
@@ -31,6 +27,7 @@ import {
31
27
  reorderCollectionDocument,
32
28
  } from '../server-fns/collections/index.js'
33
29
  import { getAdminRoutePath } from './admin-path.js'
30
+ import { getMultiCollectionDefinition } from './get-multi-collection-definition.js'
34
31
  import { collectionListSearchSchema as searchSchema } from './list-return-state.js'
35
32
  import type { CollectionTreeRow } from '../server-fns/collections/tree.js'
36
33
 
@@ -62,7 +59,7 @@ export function createCollectionListRoute(path: string) {
62
59
  status?: string
63
60
  }
64
61
  }) => {
65
- const collectionDef = getCollectionDefinition(params.collection)
62
+ const collectionDef = getMultiCollectionDefinition(params.collection)
66
63
  if (!collectionDef) {
67
64
  throw notFound()
68
65
  }
@@ -108,7 +105,7 @@ export function createCollectionListRoute(path: string) {
108
105
  const { collection } = Route.useParams() as { collection: string }
109
106
  const search = Route.useSearch() as z.infer<typeof searchSchema>
110
107
  const navigate = useNavigate()
111
- const collectionDef = getCollectionDefinition(collection) as CollectionDefinition
108
+ const collectionDef = getMultiCollectionDefinition(collection) as MultiCollectionDefinition
112
109
  const adminConfig = getCollectionAdminConfig(collection)
113
110
  // Apply the built-in relation formatter to any relation column without an
114
111
  // explicit one, so relation cells render the target's title (the list
@@ -0,0 +1,148 @@
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 { beforeEach, describe, expect, it, vi } from 'vitest'
10
+
11
+ const mocks = vi.hoisted(() => ({
12
+ getSingletonHistory: vi.fn(),
13
+ getSingleton: vi.fn(),
14
+ definition: {
15
+ singleton: true,
16
+ path: 'site-settings',
17
+ label: 'Site settings',
18
+ fields: [],
19
+ },
20
+ }))
21
+
22
+ vi.mock('@tanstack/react-router', () => ({
23
+ createFileRoute: () => (options: Record<string, unknown>) => ({ options }),
24
+ notFound: () => new Error('not found'),
25
+ }))
26
+
27
+ vi.mock('@byline/core', () => ({
28
+ getCollectionDefinition: (path: string) =>
29
+ path === mocks.definition.path ? mocks.definition : null,
30
+ isSingleton: (definition: { singleton?: boolean }) => definition.singleton === true,
31
+ getSingletonAdminConfig: () => null,
32
+ getWorkflowStatuses: () => [],
33
+ }))
34
+
35
+ vi.mock('../server-fns/singletons/index.js', () => ({
36
+ getSingletonHistory: mocks.getSingletonHistory,
37
+ getSingleton: mocks.getSingleton,
38
+ }))
39
+
40
+ vi.mock('../admin-shell/chrome/breadcrumbs/breadcrumbs-client.js', () => ({
41
+ BreadcrumbsClient: () => null,
42
+ }))
43
+ vi.mock('../admin-shell/singletons/history.js', () => ({ SingletonHistoryView: () => null }))
44
+ vi.mock('@byline/i18n/react', () => ({ useTranslation: () => ({ t: (key: string) => key }) }))
45
+ vi.mock('./admin-path.js', () => ({ getAdminRoutePath: () => '/admin' }))
46
+ vi.mock('./get-content-locale-route-config.js', () => ({
47
+ getContentLocaleRouteConfig: () => ({ contentLocales: [], defaultContentLocale: 'en' }),
48
+ }))
49
+
50
+ import { createSingletonHistoryRoute } from './create-singleton-history-route.js'
51
+
52
+ interface RouteLoaderInput {
53
+ params: { singleton: string }
54
+ deps: {
55
+ page?: number
56
+ page_size?: number
57
+ order?: string
58
+ desc?: boolean
59
+ locale?: string
60
+ }
61
+ }
62
+
63
+ type RouteLoader = (input: RouteLoaderInput) => Promise<unknown>
64
+
65
+ function loader(): RouteLoader {
66
+ const route = createSingletonHistoryRoute('/admin/singletons/$singleton/history') as {
67
+ options: { loader: RouteLoader }
68
+ }
69
+ return route.options.loader
70
+ }
71
+
72
+ describe('singleton history route loader', () => {
73
+ beforeEach(() => {
74
+ vi.clearAllMocks()
75
+ mocks.getSingleton.mockResolvedValue(null)
76
+ })
77
+
78
+ it('loads a paginated materialised singleton history without a document id input', async () => {
79
+ const history = {
80
+ docs: [{ id: 'document-settings', versionId: 'version-1', status: 'draft' }],
81
+ meta: { total: 3, page: 2, pageSize: 1, totalPages: 3 },
82
+ actors: {},
83
+ }
84
+ const currentDocument = {
85
+ id: 'document-settings',
86
+ versionId: 'version-3',
87
+ status: 'draft',
88
+ fields: {},
89
+ }
90
+ mocks.getSingletonHistory.mockResolvedValue(history)
91
+ mocks.getSingleton.mockResolvedValue(currentDocument)
92
+
93
+ await expect(
94
+ loader()({
95
+ params: { singleton: 'site-settings' },
96
+ deps: { page: 2, page_size: 1, order: 'created_at', desc: true, locale: 'fr' },
97
+ })
98
+ ).resolves.toEqual({ history, currentDocument })
99
+
100
+ expect(mocks.getSingletonHistory).toHaveBeenCalledWith({
101
+ data: {
102
+ singleton: 'site-settings',
103
+ params: {
104
+ page: 2,
105
+ page_size: 1,
106
+ order: 'created_at',
107
+ desc: true,
108
+ locale: 'fr',
109
+ },
110
+ },
111
+ })
112
+ expect(mocks.getSingleton).toHaveBeenCalledWith({
113
+ data: { singleton: 'site-settings', locale: 'fr' },
114
+ })
115
+ })
116
+
117
+ it('preserves the requested empty FindResult envelope for an unmaterialised slot', async () => {
118
+ const history = {
119
+ docs: [],
120
+ meta: { total: 0, page: 4, pageSize: 30, totalPages: 0 },
121
+ actors: {},
122
+ }
123
+ mocks.getSingletonHistory.mockResolvedValue(history)
124
+
125
+ await expect(
126
+ loader()({
127
+ params: { singleton: 'site-settings' },
128
+ deps: { page: 4, page_size: 30 },
129
+ })
130
+ ).resolves.toEqual({ history, currentDocument: null })
131
+
132
+ expect(mocks.getSingleton).toHaveBeenCalledWith({
133
+ data: { singleton: 'site-settings', locale: 'all' },
134
+ })
135
+ })
136
+
137
+ it('preserves a kind-aware read denial from the singleton handle', async () => {
138
+ const forbidden = {
139
+ code: 'ERR_FORBIDDEN',
140
+ details: { ability: 'singletons.site-settings.read' },
141
+ }
142
+ mocks.getSingletonHistory.mockRejectedValue(forbidden)
143
+
144
+ await expect(loader()({ params: { singleton: 'site-settings' }, deps: {} })).rejects.toBe(
145
+ forbidden
146
+ )
147
+ })
148
+ })
@@ -0,0 +1,116 @@
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 { createFileRoute, notFound } from '@tanstack/react-router'
10
+
11
+ import { getSingletonAdminConfig, getWorkflowStatuses } from '@byline/core'
12
+ import { useTranslation } from '@byline/i18n/react'
13
+ import { z } from 'zod'
14
+
15
+ import { BreadcrumbsClient } from '../admin-shell/chrome/breadcrumbs/breadcrumbs-client.js'
16
+ import { SingletonHistoryView } from '../admin-shell/singletons/history.js'
17
+ import { getSingleton, getSingletonHistory } from '../server-fns/singletons/index.js'
18
+ import { getAdminRoutePath } from './admin-path.js'
19
+ import { getContentLocaleRouteConfig } from './get-content-locale-route-config.js'
20
+ import { getSingletonDefinition } from './get-singleton-definition.js'
21
+ import type { VersionHistoryData } from '../admin-shell/collections/version-history.js'
22
+
23
+ const searchSchema = z.object({
24
+ page: z.coerce.number().min(1).optional(),
25
+ page_size: z.coerce.number().max(100).optional(),
26
+ order: z.string().optional(),
27
+ desc: z.coerce.boolean().optional(),
28
+ locale: z.string().optional(),
29
+ })
30
+
31
+ export function createSingletonHistoryRoute(path: string) {
32
+ // biome-ignore lint/suspicious/noExplicitAny: dynamic path bypasses route-tree typing
33
+ const Route: any = createFileRoute(path as never)({
34
+ validateSearch: searchSchema,
35
+ loaderDeps: ({ search }: { search: z.infer<typeof searchSchema> }) => ({
36
+ page: search.page,
37
+ page_size: search.page_size,
38
+ order: search.order,
39
+ desc: search.desc,
40
+ locale: search.locale,
41
+ }),
42
+ loader: async ({
43
+ params,
44
+ deps,
45
+ }: {
46
+ params: { singleton: string }
47
+ deps: z.infer<typeof searchSchema>
48
+ }) => {
49
+ if (getSingletonDefinition(params.singleton) == null) throw notFound()
50
+
51
+ const [history, currentDocument] = await Promise.all([
52
+ getSingletonHistory({
53
+ data: {
54
+ singleton: params.singleton,
55
+ params: {
56
+ page: deps.page,
57
+ page_size: deps.page_size,
58
+ order: deps.order,
59
+ desc: deps.desc,
60
+ locale: deps.locale,
61
+ },
62
+ },
63
+ }),
64
+ // Use the same locale shape as the version stream so DiffModal compares
65
+ // like with like. An empty slot returns null while history returns its
66
+ // requested empty FindResult envelope.
67
+ getSingleton({
68
+ data: { singleton: params.singleton, locale: deps.locale ?? 'all' },
69
+ }),
70
+ ])
71
+
72
+ return { history, currentDocument }
73
+ },
74
+ staleTime: 0,
75
+ gcTime: 0,
76
+ shouldReload: true,
77
+ component: function SingletonHistoryRouteComponent() {
78
+ const { history, currentDocument } = Route.useLoaderData()
79
+ const { singleton } = Route.useParams() as { singleton: string }
80
+ const definition = getSingletonDefinition(singleton)
81
+ if (definition == null) throw notFound()
82
+
83
+ const { t } = useTranslation('byline-admin')
84
+ const { contentLocales, defaultContentLocale } = getContentLocaleRouteConfig()
85
+
86
+ return (
87
+ <>
88
+ <BreadcrumbsClient
89
+ breadcrumbs={[
90
+ { label: t('chrome.menu.dashboard'), href: getAdminRoutePath() },
91
+ {
92
+ label: definition.label,
93
+ href: getAdminRoutePath('singletons', singleton),
94
+ },
95
+ {
96
+ label: t('collections.breadcrumbs.history'),
97
+ href: getAdminRoutePath('singletons', singleton, 'history'),
98
+ },
99
+ ]}
100
+ />
101
+ <SingletonHistoryView
102
+ singletonDefinition={definition}
103
+ adminConfig={getSingletonAdminConfig(singleton) ?? undefined}
104
+ data={history as VersionHistoryData}
105
+ currentDocument={currentDocument as Record<string, unknown> | null}
106
+ contentLocales={contentLocales}
107
+ defaultContentLocale={defaultContentLocale}
108
+ workflowStatuses={getWorkflowStatuses(definition)}
109
+ />
110
+ </>
111
+ )
112
+ },
113
+ })
114
+
115
+ return Route
116
+ }