@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
@@ -0,0 +1,172 @@
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
+ definition: {
13
+ singleton: true,
14
+ path: 'site-settings',
15
+ label: 'Site settings',
16
+ fields: [{ name: 'title', type: 'text', defaultValue: 'Default title' }],
17
+ },
18
+ read: vi.fn(),
19
+ buildInitialData: vi.fn(),
20
+ getPublishedVersion: vi.fn(),
21
+ getRequestContext: vi.fn(),
22
+ getScheduledPublish: vi.fn(),
23
+ getCollectionRecord: vi.fn(),
24
+ scheduledPublicationEnabled: true,
25
+ }))
26
+
27
+ vi.mock('@tanstack/react-start', () => ({
28
+ createServerFn: () => {
29
+ let validate = (input: unknown) => input
30
+ const chain = {
31
+ validator(validator: (input: unknown) => unknown) {
32
+ validate = validator
33
+ return chain
34
+ },
35
+ handler(handler: (options: { data: any }) => Promise<unknown>) {
36
+ return async (options: { data: unknown }) => handler({ data: validate(options.data) })
37
+ },
38
+ }
39
+ return chain
40
+ },
41
+ }))
42
+
43
+ vi.mock('@tanstack/react-router', () => ({
44
+ createFileRoute: () => (options: Record<string, unknown>) => ({ options }),
45
+ notFound: () => new Error('not found'),
46
+ }))
47
+
48
+ vi.mock('@byline/client/server', () => ({
49
+ getAdminBylineClient: () => ({
50
+ singleton: () => ({ getScheduledPublish: mocks.getScheduledPublish }),
51
+ }),
52
+ getAdminRequestContext: mocks.getRequestContext,
53
+ }))
54
+
55
+ vi.mock('@byline/core', () => ({
56
+ buildInitialDataFromFields: mocks.buildInitialData,
57
+ documentAbilityKey: (_definition: unknown, verb: string) => `singletons.site-settings.${verb}`,
58
+ getCollectionDefinition: (path: string) =>
59
+ path === mocks.definition.path ? mocks.definition : null,
60
+ isSingleton: (definition: { singleton?: boolean }) => definition.singleton === true,
61
+ getServerConfig: () => ({
62
+ i18n: { content: { defaultLocale: 'en' } },
63
+ scheduledPublication: { enabled: mocks.scheduledPublicationEnabled },
64
+ db: { queries: { documents: { getPublishedVersion: mocks.getPublishedVersion } } },
65
+ }),
66
+ getSingletonAdminConfig: () => null,
67
+ }))
68
+
69
+ vi.mock('../admin-shell/chrome/breadcrumbs/breadcrumbs-client.js', () => ({
70
+ BreadcrumbsClient: () => null,
71
+ }))
72
+ vi.mock('../admin-shell/singletons/view.js', () => ({ SingletonView: () => null }))
73
+ vi.mock('../integrations/byline-core.js', () => ({
74
+ bylineCore: () => ({ getCollectionRecord: mocks.getCollectionRecord }),
75
+ }))
76
+ vi.mock('../server-fns/singleton-document-read.js', () => ({
77
+ readSingletonDocument: mocks.read,
78
+ }))
79
+ vi.mock('../server-fns/serialise.js', () => ({ serialise: (value: unknown) => value }))
80
+ vi.mock('@byline/i18n/react', () => ({ useTranslation: () => ({ t: (key: string) => key }) }))
81
+ vi.mock('./admin-path.js', () => ({ getAdminRoutePath: () => '/admin' }))
82
+ vi.mock('./get-content-locale-route-config.js', () => ({
83
+ getContentLocaleRouteConfig: () => ({ contentLocales: [], defaultContentLocale: 'en' }),
84
+ }))
85
+
86
+ import { getSingletonRouteDocument } from './create-singleton-route.js'
87
+
88
+ type ServerFunction = (options: { data: { singleton: string; locale?: string } }) => Promise<any>
89
+
90
+ const load = (locale?: string) =>
91
+ (getSingletonRouteDocument as ServerFunction)({
92
+ data: { singleton: 'site-settings', locale },
93
+ })
94
+
95
+ describe('singleton editor route loader', () => {
96
+ beforeEach(() => {
97
+ vi.clearAllMocks()
98
+ mocks.scheduledPublicationEnabled = true
99
+ mocks.buildInitialData.mockResolvedValue({ title: 'Default title' })
100
+ mocks.getCollectionRecord.mockReturnValue({ collectionId: 'collection-settings' })
101
+ mocks.getRequestContext.mockResolvedValue({
102
+ actor: {
103
+ hasAbility: (ability: string) =>
104
+ ability === 'singletons.site-settings.changeStatus' ||
105
+ ability === 'singletons.site-settings.publish',
106
+ },
107
+ })
108
+ })
109
+
110
+ it('resolves schema defaults without inventing a document for an empty slot', async () => {
111
+ mocks.read.mockResolvedValue(null)
112
+
113
+ await expect(load('fr')).resolves.toEqual({
114
+ document: null,
115
+ initialData: { title: 'Default title' },
116
+ })
117
+ expect(mocks.read).toHaveBeenCalledWith({
118
+ singleton: 'site-settings',
119
+ locale: 'fr',
120
+ populateRelations: true,
121
+ })
122
+ expect(mocks.buildInitialData).toHaveBeenCalledWith(mocks.definition.fields, { locale: 'fr' })
123
+ expect(mocks.getCollectionRecord).not.toHaveBeenCalled()
124
+ expect(mocks.getRequestContext).not.toHaveBeenCalled()
125
+ })
126
+
127
+ it('attaches publication and schedule metadata to a mapped document server-side', async () => {
128
+ mocks.read.mockResolvedValue({
129
+ id: 'document-settings',
130
+ versionId: 'version-current',
131
+ status: 'draft',
132
+ fields: { title: 'Loaded' },
133
+ })
134
+ mocks.getPublishedVersion.mockResolvedValue({
135
+ document_id: 'document-settings',
136
+ document_version_id: 'version-published',
137
+ status: 'published',
138
+ created_at: '2026-08-25T00:00:00.000Z',
139
+ updated_at: '2026-08-25T01:00:00.000Z',
140
+ })
141
+ mocks.getScheduledPublish.mockResolvedValue({
142
+ state: 'armed',
143
+ publishAt: '2026-09-01T00:00:00.000Z',
144
+ targetVersionId: 'version-current',
145
+ })
146
+
147
+ const result = await load()
148
+
149
+ expect(result.initialData).toBeUndefined()
150
+ expect(result.document).toMatchObject({
151
+ id: 'document-settings',
152
+ _publishedVersion: {
153
+ id: 'document-settings',
154
+ versionId: 'version-published',
155
+ status: 'published',
156
+ },
157
+ _scheduledPublicationEnabled: true,
158
+ _canSchedulePublication: true,
159
+ _scheduledPublish: {
160
+ state: 'armed',
161
+ targetVersionId: 'version-current',
162
+ },
163
+ scheduledPublishAt: '2026-09-01T00:00:00.000Z',
164
+ scheduledPublishVersionId: 'version-current',
165
+ })
166
+ expect(mocks.getPublishedVersion).toHaveBeenCalledWith({
167
+ collection_id: 'collection-settings',
168
+ document_id: 'document-settings',
169
+ })
170
+ expect(mocks.getScheduledPublish).toHaveBeenCalledTimes(1)
171
+ })
172
+ })
@@ -0,0 +1,176 @@
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
+ import { createServerFn } from '@tanstack/react-start'
11
+
12
+ import type { DocumentPublishScheduleInfo } from '@byline/client'
13
+ import { getAdminBylineClient, getAdminRequestContext } from '@byline/client/server'
14
+ import {
15
+ buildInitialDataFromFields,
16
+ documentAbilityKey,
17
+ getServerConfig,
18
+ getSingletonAdminConfig,
19
+ } from '@byline/core'
20
+ import { useTranslation } from '@byline/i18n/react'
21
+ import { z } from 'zod'
22
+
23
+ import { BreadcrumbsClient } from '../admin-shell/chrome/breadcrumbs/breadcrumbs-client.js'
24
+ import { SingletonView } from '../admin-shell/singletons/view.js'
25
+ import { bylineCore } from '../integrations/byline-core.js'
26
+ import { serialise } from '../server-fns/serialise.js'
27
+ import { readSingletonDocument } from '../server-fns/singleton-document-read.js'
28
+ import { getAdminRoutePath } from './admin-path.js'
29
+ import { getContentLocaleRouteConfig } from './get-content-locale-route-config.js'
30
+ import { getSingletonDefinition } from './get-singleton-definition.js'
31
+
32
+ const searchSchema = z.object({ locale: z.string().optional() })
33
+
34
+ /**
35
+ * Load the mapped singleton document and attach the admin-only lifecycle
36
+ * metadata consumed by the editor. Keeping this composition in the route
37
+ * layer leaves ordinary singleton reads free of publication timing and actor
38
+ * capability details.
39
+ */
40
+ export const getSingletonRouteDocument = createServerFn({ method: 'GET' })
41
+ .validator((input: { singleton: string; locale?: string }) => input)
42
+ .handler(async ({ data }) => {
43
+ const definition = getSingletonDefinition(data.singleton)
44
+ if (definition == null) return null
45
+
46
+ const document = await readSingletonDocument({
47
+ singleton: data.singleton,
48
+ locale: data.locale,
49
+ populateRelations: true,
50
+ })
51
+ if (document == null) {
52
+ return {
53
+ document: null,
54
+ initialData: serialise(
55
+ await buildInitialDataFromFields(definition.fields, {
56
+ locale: data.locale ?? getServerConfig().i18n.content.defaultLocale,
57
+ })
58
+ ),
59
+ }
60
+ }
61
+
62
+ const config = getServerConfig()
63
+ const record = bylineCore().getCollectionRecord(data.singleton)
64
+
65
+ // A current draft may coexist with an older published version. That fact
66
+ // is editorial metadata, so it is intentionally attached only here rather
67
+ // than to every singleton read.
68
+ let publishedVersion: Record<string, any> | null = null
69
+ if ((document as Record<string, unknown>).status !== 'published') {
70
+ const published = await config.db.queries.documents.getPublishedVersion({
71
+ collection_id: record.collectionId,
72
+ document_id: String((document as Record<string, unknown>).id),
73
+ })
74
+ publishedVersion = published
75
+ ? serialise({
76
+ id: published.document_id,
77
+ versionId: published.document_version_id,
78
+ status: published.status,
79
+ createdAt: published.created_at,
80
+ updatedAt: published.updated_at,
81
+ })
82
+ : null
83
+ }
84
+
85
+ // Scheduling metadata is revealed only when the feature is enabled and
86
+ // the authenticated actor holds both abilities needed to arm a publish.
87
+ const scheduledPublicationEnabled = config.scheduledPublication?.enabled === true
88
+ let canSchedulePublication = false
89
+ let scheduledPublish: DocumentPublishScheduleInfo | null = null
90
+ if (scheduledPublicationEnabled) {
91
+ const actor = (await getAdminRequestContext()).actor
92
+ canSchedulePublication =
93
+ actor?.hasAbility(documentAbilityKey(definition, 'changeStatus')) === true &&
94
+ actor.hasAbility(documentAbilityKey(definition, 'publish'))
95
+ if (canSchedulePublication) {
96
+ scheduledPublish = await getAdminBylineClient()
97
+ .singleton(data.singleton)
98
+ .getScheduledPublish()
99
+ }
100
+ }
101
+ const serializedSchedule = scheduledPublish == null ? null : serialise(scheduledPublish)
102
+
103
+ return {
104
+ document: {
105
+ ...(document as Record<string, unknown>),
106
+ _publishedVersion: publishedVersion,
107
+ _scheduledPublicationEnabled: scheduledPublicationEnabled,
108
+ _canSchedulePublication: canSchedulePublication,
109
+ _scheduledPublish: serializedSchedule,
110
+ ...(serializedSchedule == null
111
+ ? {}
112
+ : {
113
+ scheduledPublishAt: serializedSchedule.publishAt,
114
+ scheduledPublishVersionId: serializedSchedule.targetVersionId,
115
+ }),
116
+ },
117
+ initialData: undefined,
118
+ }
119
+ })
120
+
121
+ export function createSingletonRoute(path: string) {
122
+ // biome-ignore lint/suspicious/noExplicitAny: dynamic path bypasses route-tree typing
123
+ const Route: any = createFileRoute(path as never)({
124
+ validateSearch: searchSchema,
125
+ loaderDeps: ({ search }: { search: z.infer<typeof searchSchema> }) => ({
126
+ locale: search.locale,
127
+ }),
128
+ loader: async ({
129
+ params,
130
+ deps,
131
+ }: {
132
+ params: { singleton: string }
133
+ deps: { locale?: string }
134
+ }) => {
135
+ if (getSingletonDefinition(params.singleton) == null) throw notFound()
136
+ return getSingletonRouteDocument({
137
+ data: { singleton: params.singleton, locale: deps.locale },
138
+ })
139
+ },
140
+ staleTime: 0,
141
+ gcTime: 0,
142
+ shouldReload: true,
143
+ component: function SingletonRouteComponent() {
144
+ const routeState = Route.useLoaderData()
145
+ const { singleton } = Route.useParams() as { singleton: string }
146
+ const { locale } = Route.useSearch() as z.infer<typeof searchSchema>
147
+ const definition = getSingletonDefinition(singleton)
148
+ if (definition == null) throw notFound()
149
+
150
+ const { t } = useTranslation('byline-admin')
151
+ const { contentLocales, defaultContentLocale } = getContentLocaleRouteConfig()
152
+
153
+ return (
154
+ <>
155
+ <BreadcrumbsClient
156
+ breadcrumbs={[
157
+ { label: t('chrome.menu.dashboard'), href: getAdminRoutePath() },
158
+ { label: definition.label, href: getAdminRoutePath('singletons', singleton) },
159
+ ]}
160
+ />
161
+ <SingletonView
162
+ singletonDefinition={definition}
163
+ adminConfig={getSingletonAdminConfig(singleton) ?? undefined}
164
+ document={routeState.document}
165
+ initialData={routeState.initialData}
166
+ locale={locale}
167
+ contentLocales={contentLocales}
168
+ defaultContentLocale={defaultContentLocale}
169
+ />
170
+ </>
171
+ )
172
+ },
173
+ })
174
+
175
+ return Route
176
+ }
@@ -0,0 +1,16 @@
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 type { MultiCollectionDefinition } from '@byline/core'
10
+ import { getCollectionDefinition, isSingleton } from '@byline/core'
11
+
12
+ /** Resolve only the resource kind supported by the collection route family. */
13
+ export function getMultiCollectionDefinition(path: string): MultiCollectionDefinition | null {
14
+ const definition = getCollectionDefinition(path)
15
+ return definition != null && !isSingleton(definition) ? definition : null
16
+ }
@@ -0,0 +1,16 @@
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 type { SingletonDefinition } from '@byline/core'
10
+ import { getCollectionDefinition, isSingleton } from '@byline/core'
11
+
12
+ /** Resolve only the resource kind supported by the singleton route family. */
13
+ export function getSingletonDefinition(path: string): SingletonDefinition | null {
14
+ const definition = getCollectionDefinition(path)
15
+ return definition != null && isSingleton(definition) ? definition : null
16
+ }
@@ -33,6 +33,7 @@ export { getSignInRoutePath } from './sign-in-path.js'
33
33
 
34
34
  export { createAdminAccountRoute } from './create-admin-account-route.js'
35
35
  export { createAdminActivityRoute } from './create-admin-activity-route.js'
36
+ export { createAdminAnalyticsRoute } from './create-admin-analytics-route.js'
36
37
  export { createAdminDashboardRoute } from './create-admin-dashboard-route.js'
37
38
  export { createAdminLayoutRoute } from './create-admin-layout-route.js'
38
39
  export { createAdminPermissionsRoute } from './create-admin-permissions-route.js'
@@ -50,3 +51,5 @@ export {
50
51
  type CreateSignInRouteOptions,
51
52
  createSignInRoute,
52
53
  } from './create-sign-in-route.js'
54
+ export { createSingletonHistoryRoute } from './create-singleton-history-route.js'
55
+ export { createSingletonRoute } from './create-singleton-route.js'
@@ -0,0 +1,43 @@
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 type { AdminUserRow } from '@byline/admin/admin-users'
10
+
11
+ import { bylineCore } from '../integrations/byline-core.js'
12
+
13
+ export type ActorLabelMap = Record<string, { label: string }>
14
+
15
+ /** Display label: full name → username → email. */
16
+ function labelFor(row: AdminUserRow): string {
17
+ const name = [row.given_name, row.family_name].filter(Boolean).join(' ')
18
+ return name || row.username || row.email
19
+ }
20
+
21
+ /**
22
+ * Batch-resolve admin-user ids to display labels. The admin server-fn layer
23
+ * owns this presentation concern so both collection and singleton history
24
+ * can share it without teaching document storage about admin-realm tables.
25
+ */
26
+ export async function resolveActorLabels(
27
+ ids: Iterable<string | null | undefined>
28
+ ): Promise<ActorLabelMap> {
29
+ const unique = [
30
+ ...new Set([...ids].filter((id): id is string => typeof id === 'string' && id.length > 0)),
31
+ ]
32
+ if (unique.length === 0) return {}
33
+
34
+ const store = bylineCore().adminStore
35
+ if (store == null) return {}
36
+
37
+ const rows = await store.adminUsers.getByIds(unique)
38
+ const map: ActorLabelMap = {}
39
+ for (const row of rows) {
40
+ map[row.id] = { label: labelFor(row) }
41
+ }
42
+ return map
43
+ }
@@ -0,0 +1,78 @@
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 {
10
+ buildRelationSummaryPopulateMap,
11
+ type CollectionDefinition,
12
+ getCollectionAdminConfig,
13
+ getCollectionDefinition,
14
+ type PopulateSpec,
15
+ } from '@byline/core'
16
+
17
+ export interface AdminDocumentReadInput {
18
+ locale?: string
19
+ depth?: number
20
+ populateRelations?: boolean
21
+ }
22
+
23
+ export interface AdminDocumentReadOptions {
24
+ locale: string
25
+ populate?: PopulateSpec
26
+ depth?: number
27
+ status: 'any'
28
+ onMissingLocale: 'empty'
29
+ lenient: true
30
+ }
31
+
32
+ /**
33
+ * Resolve the shared read controls used by collection and singleton editors.
34
+ * Explicit depth is the API-preview mode; otherwise relation-summary mode
35
+ * projects only each target's picker/title fields for first-paint tiles.
36
+ */
37
+ export function resolveAdminDocumentRead(
38
+ definition: CollectionDefinition | null,
39
+ input: AdminDocumentReadInput
40
+ ): { options: AdminDocumentReadOptions; populatedTree: boolean } {
41
+ const populateRequested = typeof input.depth === 'number' && input.depth > 0
42
+ const autoRelationsActive = !populateRequested && input.populateRelations === true
43
+
44
+ let populate: PopulateSpec | undefined
45
+ let depth: number | undefined
46
+ if (populateRequested) {
47
+ populate = '*'
48
+ depth = input.depth
49
+ } else if (autoRelationsActive && definition != null) {
50
+ const populateMap = buildRelationSummaryPopulateMap(definition.fields, (targetPath) => ({
51
+ def: getCollectionDefinition(targetPath),
52
+ admin: getCollectionAdminConfig(targetPath),
53
+ }))
54
+ if (Object.keys(populateMap).length > 0) {
55
+ populate = populateMap
56
+ depth = 1
57
+ }
58
+ }
59
+
60
+ return {
61
+ options: {
62
+ locale: input.locale ?? 'en',
63
+ populate,
64
+ depth,
65
+ status: 'any',
66
+ // Editors need the raw requested-locale values. Untranslated localized
67
+ // fields stay empty instead of falling back, which is the signal that
68
+ // enables the Copy to Locale workflow. Using `omit` would hide the whole
69
+ // resource and make a missing translation look like a missing slot.
70
+ onMissingLocale: 'empty',
71
+ // Older rows may no longer reconstruct exactly against the current
72
+ // schema. Keep the editor loadable and surface those mismatches through
73
+ // `_restoreWarnings`, which the form renders as an alert.
74
+ lenient: true,
75
+ },
76
+ populatedTree: populateRequested || autoRelationsActive,
77
+ }
78
+ }
@@ -0,0 +1,23 @@
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
+ export {
10
+ type DeleteAnalyticsEventsInput,
11
+ deleteAnalyticsEvents,
12
+ rebuildAnalyticsDay,
13
+ } from './maintenance.js'
14
+ export {
15
+ type AnalyticsRuntimeState,
16
+ type AnalyticsTopInput,
17
+ getAnalyticsCountries,
18
+ getAnalyticsReferrers,
19
+ getAnalyticsReportCoverage,
20
+ getAnalyticsRuntime,
21
+ getAnalyticsSummary,
22
+ getAnalyticsTop,
23
+ } from './queries.js'
@@ -0,0 +1,54 @@
1
+ /**
2
+ * This Source Code is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
+ *
6
+ * Copyright (c) Infonomic Company Limited
7
+ */
8
+
9
+ import { createServerFn } from '@tanstack/react-start'
10
+
11
+ import { assertAdminActor } from '@byline/admin'
12
+ import { ANALYTICS_ABILITIES } from '@byline/admin/analytics'
13
+ import { getAdminRequestContext } from '@byline/client/server'
14
+
15
+ export interface DeleteAnalyticsEventsInput {
16
+ from: string
17
+ to: string
18
+ visitorHash?: string
19
+ }
20
+
21
+ export const deleteAnalyticsEvents = createServerFn({ method: 'POST' })
22
+ .validator((input: DeleteAnalyticsEventsInput) => input)
23
+ .handler(async ({ data }): Promise<{ deleted: number }> => {
24
+ await assertAnalyticsMaintenance()
25
+ const { getAnalytics } = await import('@byline/analytics')
26
+ const from = parseInstant(data.from, 'from')
27
+ const to = parseInstant(data.to, 'to')
28
+ const deleted = await getAnalytics().deleteEvents({
29
+ from,
30
+ to,
31
+ ...(data.visitorHash == null ? {} : { visitorHash: data.visitorHash }),
32
+ })
33
+ return { deleted }
34
+ })
35
+
36
+ export const rebuildAnalyticsDay = createServerFn({ method: 'POST' })
37
+ .validator((input: { day: string }) => input)
38
+ .handler(async ({ data }): Promise<{ rebuilt: string }> => {
39
+ await assertAnalyticsMaintenance()
40
+ const { getAnalytics } = await import('@byline/analytics')
41
+ await getAnalytics().rebuildDay(data.day)
42
+ return { rebuilt: data.day }
43
+ })
44
+
45
+ async function assertAnalyticsMaintenance(): Promise<void> {
46
+ const context = await getAdminRequestContext()
47
+ assertAdminActor(context, ANALYTICS_ABILITIES.maintain)
48
+ }
49
+
50
+ function parseInstant(value: string, label: string): Date {
51
+ const date = new Date(value)
52
+ if (Number.isNaN(date.valueOf())) throw new Error(`analytics ${label} must be an ISO instant`)
53
+ return date
54
+ }
@@ -0,0 +1,86 @@
1
+ /**
2
+ * This Source Code is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
+ *
6
+ * Copyright (c) Infonomic Company Limited
7
+ */
8
+
9
+ import { createServerFn } from '@tanstack/react-start'
10
+
11
+ import { assertAdminActor, requireAdminActor } from '@byline/admin'
12
+ import { ANALYTICS_ABILITIES } from '@byline/admin/analytics'
13
+ import type {
14
+ AnalyticsCountryTotal,
15
+ AnalyticsDateRange,
16
+ AnalyticsEventKind,
17
+ AnalyticsPathTotal,
18
+ AnalyticsRankedTotals,
19
+ AnalyticsReferrerTotal,
20
+ AnalyticsReportCoverage,
21
+ AnalyticsSummary,
22
+ } from '@byline/analytics'
23
+ import { getAdminRequestContext } from '@byline/client/server'
24
+
25
+ export interface AnalyticsTopInput extends AnalyticsDateRange {
26
+ kind: AnalyticsEventKind
27
+ limit?: number
28
+ }
29
+
30
+ export interface AnalyticsRuntimeState {
31
+ enabled: boolean
32
+ }
33
+
34
+ export const getAnalyticsRuntime = createServerFn({ method: 'GET' }).handler(
35
+ async (): Promise<AnalyticsRuntimeState> => {
36
+ const context = await getAdminRequestContext()
37
+ requireAdminActor(context, 'reading analytics runtime state')
38
+ const { isAnalyticsRegistered } = await import('@byline/analytics')
39
+ return { enabled: isAnalyticsRegistered() }
40
+ }
41
+ )
42
+
43
+ export const getAnalyticsSummary = createServerFn({ method: 'GET' })
44
+ .validator((input: AnalyticsDateRange) => input)
45
+ .handler(async ({ data }): Promise<AnalyticsSummary> => {
46
+ await assertAnalyticsRead()
47
+ const { getAnalytics } = await import('@byline/analytics')
48
+ return getAnalytics().getSummary(data)
49
+ })
50
+
51
+ export const getAnalyticsReportCoverage = createServerFn({ method: 'GET' }).handler(
52
+ async (): Promise<AnalyticsReportCoverage> => {
53
+ await assertAnalyticsRead()
54
+ const { getAnalytics } = await import('@byline/analytics')
55
+ return getAnalytics().getReportCoverage()
56
+ }
57
+ )
58
+
59
+ export const getAnalyticsTop = createServerFn({ method: 'GET' })
60
+ .validator((input: AnalyticsTopInput) => input)
61
+ .handler(async ({ data }): Promise<AnalyticsRankedTotals<AnalyticsPathTotal>> => {
62
+ await assertAnalyticsRead()
63
+ const { getAnalytics } = await import('@byline/analytics')
64
+ return getAnalytics().getTopPaths(data)
65
+ })
66
+
67
+ export const getAnalyticsReferrers = createServerFn({ method: 'GET' })
68
+ .validator((input: AnalyticsDateRange & { limit?: number }) => input)
69
+ .handler(async ({ data }): Promise<AnalyticsRankedTotals<AnalyticsReferrerTotal>> => {
70
+ await assertAnalyticsRead()
71
+ const { getAnalytics } = await import('@byline/analytics')
72
+ return getAnalytics().getReferrers(data)
73
+ })
74
+
75
+ export const getAnalyticsCountries = createServerFn({ method: 'GET' })
76
+ .validator((input: AnalyticsDateRange) => input)
77
+ .handler(async ({ data }): Promise<AnalyticsCountryTotal[]> => {
78
+ await assertAnalyticsRead()
79
+ const { getAnalytics } = await import('@byline/analytics')
80
+ return getAnalytics().getCountries(data)
81
+ })
82
+
83
+ async function assertAnalyticsRead(): Promise<void> {
84
+ const context = await getAdminRequestContext()
85
+ assertAdminActor(context, ANALYTICS_ABILITIES.read)
86
+ }