@byline/host-tanstack-start 4.16.0 → 4.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/admin-shell/chrome/admin-menu-drawer.js +14 -14
- package/dist/admin-shell/chrome/dashboard.js +41 -5
- package/dist/admin-shell/chrome/dashboard.test.d.ts +8 -0
- package/dist/admin-shell/chrome/router-pager.d.ts +7 -0
- package/dist/admin-shell/chrome/router-pager.js +13 -0
- package/dist/admin-shell/chrome/scroll-to-top.d.ts +20 -0
- package/dist/admin-shell/chrome/scroll-to-top.js +9 -0
- package/dist/admin-shell/collections/api.d.ts +2 -2
- package/dist/admin-shell/collections/create.d.ts +2 -2
- package/dist/admin-shell/collections/create.js +1 -0
- package/dist/admin-shell/collections/edit.d.ts +2 -2
- package/dist/admin-shell/collections/edit.js +1 -0
- package/dist/admin-shell/collections/history.d.ts +4 -12
- package/dist/admin-shell/collections/history.js +112 -303
- package/dist/admin-shell/collections/reindex-button.d.ts +1 -1
- package/dist/admin-shell/collections/reindex-button.js +5 -1
- package/dist/admin-shell/collections/restore-version-modal.d.ts +4 -4
- package/dist/admin-shell/collections/restore-version-modal.js +8 -24
- package/dist/admin-shell/collections/version-history.d.ts +72 -0
- package/dist/admin-shell/collections/version-history.js +259 -0
- package/dist/admin-shell/singletons/history.d.ts +19 -0
- package/dist/admin-shell/singletons/history.js +170 -0
- package/dist/admin-shell/singletons/history.test.d.ts +8 -0
- package/dist/admin-shell/singletons/preview-link.d.ts +12 -0
- package/dist/admin-shell/singletons/preview-link.js +54 -0
- package/dist/admin-shell/singletons/view-menu.d.ts +13 -0
- package/dist/admin-shell/singletons/view-menu.js +116 -0
- package/dist/admin-shell/singletons/view.d.ts +18 -0
- package/dist/admin-shell/singletons/view.js +259 -0
- package/dist/admin-shell/singletons/view.test.d.ts +8 -0
- package/dist/integrations/api-utils.d.ts +18 -4
- package/dist/integrations/api-utils.js +10 -3
- package/dist/routes/create-admin-dashboard-route.js +2 -2
- package/dist/routes/create-collection-api-route.js +3 -3
- package/dist/routes/create-collection-create-route.js +4 -3
- package/dist/routes/create-collection-edit-route.js +4 -3
- package/dist/routes/create-collection-history-route.js +4 -3
- package/dist/routes/create-collection-list-route.js +4 -3
- package/dist/routes/create-singleton-history-route.d.ts +8 -0
- package/dist/routes/create-singleton-history-route.js +99 -0
- package/dist/routes/create-singleton-route.d.ts +35 -0
- package/dist/routes/create-singleton-route.js +130 -0
- package/dist/routes/get-multi-collection-definition.d.ts +10 -0
- package/dist/routes/get-multi-collection-definition.js +6 -0
- package/dist/routes/get-singleton-definition.d.ts +10 -0
- package/dist/routes/get-singleton-definition.js +6 -0
- package/dist/routes/index.d.ts +2 -0
- package/dist/routes/index.js +2 -0
- package/dist/server-fns/actors.d.ts +16 -0
- package/dist/server-fns/actors.js +25 -0
- package/dist/server-fns/admin-document-presentation.d.ts +30 -0
- package/dist/server-fns/admin-document-presentation.js +32 -0
- package/dist/server-fns/collections/actors.d.ts +1 -10
- package/dist/server-fns/collections/actors.js +1 -25
- package/dist/server-fns/collections/get.js +9 -26
- package/dist/server-fns/collections/reorder.js +1 -1
- package/dist/server-fns/collections/scheduled-publication.js +2 -2
- package/dist/server-fns/collections/upload.js +3 -3
- package/dist/server-fns/collections/utils.d.ts +1 -7
- package/dist/server-fns/collections/utils.js +1 -4
- package/dist/server-fns/serialise.d.ts +14 -0
- package/dist/server-fns/serialise.js +4 -0
- package/dist/server-fns/singleton-document-read.d.ts +20 -0
- package/dist/server-fns/singleton-document-read.js +10 -0
- package/dist/server-fns/singletons/cancel-scheduled-publish.d.ts +12 -0
- package/dist/server-fns/singletons/cancel-scheduled-publish.js +7 -0
- package/dist/server-fns/singletons/change-status.d.ts +14 -0
- package/dist/server-fns/singletons/change-status.js +7 -0
- package/dist/server-fns/singletons/confirm-scheduled-publish.d.ts +14 -0
- package/dist/server-fns/singletons/confirm-scheduled-publish.js +9 -0
- package/dist/server-fns/singletons/copy-to-locale.d.ts +18 -0
- package/dist/server-fns/singletons/copy-to-locale.js +11 -0
- package/dist/server-fns/singletons/find-by-version.d.ts +16 -0
- package/dist/server-fns/singletons/find-by-version.js +9 -0
- package/dist/server-fns/singletons/get-scheduled-publish.d.ts +12 -0
- package/dist/server-fns/singletons/get-scheduled-publish.js +7 -0
- package/dist/server-fns/singletons/get.d.ts +24 -0
- package/dist/server-fns/singletons/get.js +6 -0
- package/dist/server-fns/singletons/history.d.ts +31 -0
- package/dist/server-fns/singletons/history.js +22 -0
- package/dist/server-fns/singletons/index.d.ts +19 -0
- package/dist/server-fns/singletons/index.js +12 -0
- package/dist/server-fns/singletons/restore-version.d.ts +14 -0
- package/dist/server-fns/singletons/restore-version.js +7 -0
- package/dist/server-fns/singletons/schedule-publish.d.ts +16 -0
- package/dist/server-fns/singletons/schedule-publish.js +10 -0
- package/dist/server-fns/singletons/unpublish.d.ts +12 -0
- package/dist/server-fns/singletons/unpublish.js +7 -0
- package/dist/server-fns/singletons/update.d.ts +14 -0
- package/dist/server-fns/singletons/update.js +14 -0
- package/dist/vitest-jsdom.test.d.ts +8 -0
- package/package.json +24 -18
- package/src/admin-shell/chrome/admin-menu-drawer.tsx +13 -13
- package/src/admin-shell/chrome/dashboard.test.tsx +199 -0
- package/src/admin-shell/chrome/dashboard.tsx +43 -9
- package/src/admin-shell/chrome/router-pager.tsx +22 -0
- package/src/admin-shell/chrome/scroll-to-top.test.ts +56 -0
- package/src/admin-shell/chrome/scroll-to-top.ts +28 -0
- package/src/admin-shell/collections/api.tsx +2 -2
- package/src/admin-shell/collections/create.tsx +6 -2
- package/src/admin-shell/collections/edit.tsx +6 -2
- package/src/admin-shell/collections/history.module.css +1 -1
- package/src/admin-shell/collections/history.tsx +162 -442
- package/src/admin-shell/collections/reindex-button.tsx +4 -2
- package/src/admin-shell/collections/restore-version-modal.tsx +19 -25
- package/src/admin-shell/collections/version-history.tsx +349 -0
- package/src/admin-shell/singletons/history.test.tsx +407 -0
- package/src/admin-shell/singletons/history.tsx +185 -0
- package/src/admin-shell/singletons/preview-link.tsx +75 -0
- package/src/admin-shell/singletons/view-menu.tsx +136 -0
- package/src/admin-shell/singletons/view.test.tsx +400 -0
- package/src/admin-shell/singletons/view.tsx +351 -0
- package/src/integrations/api-utils.ts +34 -9
- package/src/routes/create-admin-dashboard-route.test.node.ts +70 -0
- package/src/routes/create-admin-dashboard-route.tsx +10 -2
- package/src/routes/create-collection-api-route.tsx +4 -4
- package/src/routes/create-collection-create-route.tsx +5 -8
- package/src/routes/create-collection-edit-route.tsx +5 -4
- package/src/routes/create-collection-history-route.tsx +5 -8
- package/src/routes/create-collection-list-route.tsx +5 -8
- package/src/routes/create-singleton-history-route.test.node.ts +148 -0
- package/src/routes/create-singleton-history-route.tsx +116 -0
- package/src/routes/create-singleton-route.test.node.ts +172 -0
- package/src/routes/create-singleton-route.tsx +176 -0
- package/src/routes/get-multi-collection-definition.ts +16 -0
- package/src/routes/get-singleton-definition.ts +16 -0
- package/src/routes/index.ts +2 -0
- package/src/server-fns/actors.ts +43 -0
- package/src/server-fns/admin-document-presentation.ts +78 -0
- package/src/server-fns/collections/actors.ts +1 -52
- package/src/server-fns/collections/get.ts +10 -49
- package/src/server-fns/collections/reorder.ts +1 -1
- package/src/server-fns/collections/scheduled-publication.ts +7 -1
- package/src/server-fns/collections/upload-singleton.test.node.ts +164 -0
- package/src/server-fns/collections/upload.ts +7 -3
- package/src/server-fns/collections/utils.ts +1 -9
- package/src/server-fns/serialise.ts +17 -0
- package/src/server-fns/singleton-document-read.ts +32 -0
- package/src/server-fns/singletons/cancel-scheduled-publish.ts +19 -0
- package/src/server-fns/singletons/change-status.ts +19 -0
- package/src/server-fns/singletons/confirm-scheduled-publish.ts +23 -0
- package/src/server-fns/singletons/copy-to-locale.ts +32 -0
- package/src/server-fns/singletons/find-by-version.ts +23 -0
- package/src/server-fns/singletons/get-scheduled-publish.ts +19 -0
- package/src/server-fns/singletons/get.ts +32 -0
- package/src/server-fns/singletons/history.ts +41 -0
- package/src/server-fns/singletons/index.ts +20 -0
- package/src/server-fns/singletons/restore-version.ts +19 -0
- package/src/server-fns/singletons/schedule-publish.ts +24 -0
- package/src/server-fns/singletons/singletons.test.node.ts +298 -0
- package/src/server-fns/singletons/unpublish.ts +19 -0
- package/src/server-fns/singletons/update.ts +31 -0
- package/src/vitest-jsdom.test.tsx +27 -0
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* This Source Code is subject to the terms of the Mozilla Public
|
|
5
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
7
|
+
*
|
|
8
|
+
* Copyright (c) Infonomic Company Limited
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { useRouter } from '@tanstack/react-router'
|
|
12
|
+
|
|
13
|
+
import { FormRenderer } from '@byline/admin/react'
|
|
14
|
+
import type { SingletonAdminConfig, SingletonDefinition } from '@byline/core'
|
|
15
|
+
import {
|
|
16
|
+
ErrorCodes,
|
|
17
|
+
getDefaultStatus,
|
|
18
|
+
getWorkflow,
|
|
19
|
+
getWorkflowStatuses,
|
|
20
|
+
validateStatusTransition,
|
|
21
|
+
} from '@byline/core'
|
|
22
|
+
import { useTranslation } from '@byline/i18n/react'
|
|
23
|
+
import { Container, Section, useToastManager } from '@byline/ui/react'
|
|
24
|
+
|
|
25
|
+
import { getAdminRoutePath } from '../../routes/admin-path.js'
|
|
26
|
+
import {
|
|
27
|
+
cancelSingletonScheduledPublish,
|
|
28
|
+
changeSingletonStatus,
|
|
29
|
+
confirmSingletonScheduledPublish,
|
|
30
|
+
copySingletonToLocale,
|
|
31
|
+
scheduleSingletonPublish,
|
|
32
|
+
unpublishSingleton,
|
|
33
|
+
updateSingleton,
|
|
34
|
+
} from '../../server-fns/singletons/index.js'
|
|
35
|
+
import { useNavigate } from '../chrome/loose-router.js'
|
|
36
|
+
import { useTanStackNavigationGuard } from '../collections/tanstack-navigation-guard.js'
|
|
37
|
+
import { SingletonViewMenu } from './view-menu.js'
|
|
38
|
+
import type { SerializedDocumentPublishSchedule } from '../../server-fns/collections/index.js'
|
|
39
|
+
import type { ContentLocaleOption } from '../collections/view-menu.js'
|
|
40
|
+
|
|
41
|
+
type SingletonDocument = Record<string, any> & {
|
|
42
|
+
id: string
|
|
43
|
+
status: string
|
|
44
|
+
fields: Record<string, any>
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function errorCode(error: unknown): string | null {
|
|
48
|
+
return typeof (error as { code?: unknown })?.code === 'string'
|
|
49
|
+
? (error as { code: string }).code
|
|
50
|
+
: null
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function SingletonView({
|
|
54
|
+
singletonDefinition,
|
|
55
|
+
adminConfig,
|
|
56
|
+
document,
|
|
57
|
+
initialData,
|
|
58
|
+
locale,
|
|
59
|
+
contentLocales,
|
|
60
|
+
defaultContentLocale,
|
|
61
|
+
}: {
|
|
62
|
+
singletonDefinition: SingletonDefinition
|
|
63
|
+
adminConfig?: SingletonAdminConfig
|
|
64
|
+
document: SingletonDocument | null
|
|
65
|
+
/** Schema defaults resolved by the server-side route loader for an empty slot. */
|
|
66
|
+
initialData?: Record<string, any>
|
|
67
|
+
locale?: string
|
|
68
|
+
contentLocales: ReadonlyArray<ContentLocaleOption>
|
|
69
|
+
defaultContentLocale: string
|
|
70
|
+
}) {
|
|
71
|
+
const router = useRouter()
|
|
72
|
+
const navigate = useNavigate()
|
|
73
|
+
const toastManager = useToastManager()
|
|
74
|
+
const { t } = useTranslation('byline-admin')
|
|
75
|
+
const { path, label, fields } = singletonDefinition
|
|
76
|
+
|
|
77
|
+
const workflowStatuses = getWorkflowStatuses(singletonDefinition)
|
|
78
|
+
const currentStatus = document?.status ?? getDefaultStatus(singletonDefinition)
|
|
79
|
+
const currentIndex = workflowStatuses.findIndex((status) => status.name === currentStatus)
|
|
80
|
+
const nextStatus =
|
|
81
|
+
currentIndex !== -1 && currentIndex < workflowStatuses.length - 1
|
|
82
|
+
? workflowStatuses[currentIndex + 1]
|
|
83
|
+
: undefined
|
|
84
|
+
const publishedVersion = document?._publishedVersion ?? null
|
|
85
|
+
const restoreWarnings = document?._restoreWarnings as string[] | undefined
|
|
86
|
+
const scheduledPublicationEnabled = document?._scheduledPublicationEnabled === true
|
|
87
|
+
const canSchedulePublication = document?._canSchedulePublication === true
|
|
88
|
+
const scheduledPublication = (document?._scheduledPublish ??
|
|
89
|
+
null) as SerializedDocumentPublishSchedule | null
|
|
90
|
+
const publishTransitionValid =
|
|
91
|
+
document != null &&
|
|
92
|
+
currentStatus !== 'published' &&
|
|
93
|
+
workflowStatuses.length > 1 &&
|
|
94
|
+
validateStatusTransition(getWorkflow(singletonDefinition), currentStatus, 'published').valid
|
|
95
|
+
const canArmSchedule =
|
|
96
|
+
scheduledPublicationEnabled && canSchedulePublication && publishTransitionValid
|
|
97
|
+
|
|
98
|
+
const reload = async () => {
|
|
99
|
+
// Invalidation is load-bearing after the first save: the refreshed loader
|
|
100
|
+
// supplies the mapped document id/version, flips FormRenderer to edit mode,
|
|
101
|
+
// and unlocks fields that require a saved document.
|
|
102
|
+
await router.invalidate()
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const toast = (title: string, description: string, intent: 'success' | 'warning' | 'danger') => {
|
|
106
|
+
toastManager.add({
|
|
107
|
+
title,
|
|
108
|
+
description,
|
|
109
|
+
data: { intent, iconType: intent, icon: true, close: true },
|
|
110
|
+
})
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const notifyScheduleSuspended = () => {
|
|
114
|
+
if (scheduledPublication?.state !== 'armed') return
|
|
115
|
+
toast(
|
|
116
|
+
t('scheduledPublication.toast.suspendedTitle'),
|
|
117
|
+
t('scheduledPublication.toast.suspendedDescription'),
|
|
118
|
+
'warning'
|
|
119
|
+
)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const handleSubmit = async ({ data }: { data: Record<string, any> }) => {
|
|
123
|
+
try {
|
|
124
|
+
await updateSingleton({
|
|
125
|
+
data: {
|
|
126
|
+
singleton: path,
|
|
127
|
+
data,
|
|
128
|
+
locale: locale ?? defaultContentLocale,
|
|
129
|
+
expectedVersionId: document?.versionId,
|
|
130
|
+
},
|
|
131
|
+
})
|
|
132
|
+
if (document != null) notifyScheduleSuspended()
|
|
133
|
+
toast(
|
|
134
|
+
t('singletons.edit.updateTitle', { label }),
|
|
135
|
+
t('singletons.edit.updatedDescription', { label: label.toLowerCase() }),
|
|
136
|
+
'success'
|
|
137
|
+
)
|
|
138
|
+
await reload()
|
|
139
|
+
} catch (err) {
|
|
140
|
+
const code = errorCode(err)
|
|
141
|
+
const description =
|
|
142
|
+
code === ErrorCodes.CONFLICT
|
|
143
|
+
? t('singletons.edit.conflictDescription')
|
|
144
|
+
: code === ErrorCodes.NOT_FOUND
|
|
145
|
+
? t('singletons.edit.notConfiguredDescription')
|
|
146
|
+
: t('singletons.edit.updateFailedDescription', { label: label.toLowerCase() })
|
|
147
|
+
toast(t('singletons.edit.updateTitle', { label }), description, 'danger')
|
|
148
|
+
|
|
149
|
+
// FormRenderer commits its clean baseline only when this promise
|
|
150
|
+
// resolves. Rethrow so a failed singleton save leaves the editor dirty
|
|
151
|
+
// and its navigation guard active.
|
|
152
|
+
throw err
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const handleStatusChange = async (status: string) => {
|
|
157
|
+
try {
|
|
158
|
+
await changeSingletonStatus({ data: { singleton: path, status } })
|
|
159
|
+
toast(
|
|
160
|
+
t('collections.edit.statusUpdateTitle', { label }),
|
|
161
|
+
t('collections.edit.statusChangedDescription', { status }),
|
|
162
|
+
'success'
|
|
163
|
+
)
|
|
164
|
+
await reload()
|
|
165
|
+
} catch (err) {
|
|
166
|
+
toast(
|
|
167
|
+
t('collections.edit.statusUpdateTitle', { label }),
|
|
168
|
+
t('collections.edit.statusChangeFailedDescription', { message: (err as Error).message }),
|
|
169
|
+
'danger'
|
|
170
|
+
)
|
|
171
|
+
throw err
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const handleUnpublish = async () => {
|
|
176
|
+
try {
|
|
177
|
+
await unpublishSingleton({ data: { singleton: path } })
|
|
178
|
+
toast(
|
|
179
|
+
t('collections.edit.unpublishTitle', { label }),
|
|
180
|
+
t('collections.edit.unpublishedDescription'),
|
|
181
|
+
'success'
|
|
182
|
+
)
|
|
183
|
+
await reload()
|
|
184
|
+
} catch (err) {
|
|
185
|
+
toast(
|
|
186
|
+
t('collections.edit.unpublishTitle', { label }),
|
|
187
|
+
t('collections.edit.unpublishFailedDescription', { message: (err as Error).message }),
|
|
188
|
+
'danger'
|
|
189
|
+
)
|
|
190
|
+
throw err
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const handleCopyToLocale = async ({
|
|
195
|
+
targetLocale,
|
|
196
|
+
overwrite,
|
|
197
|
+
}: {
|
|
198
|
+
targetLocale: string
|
|
199
|
+
overwrite: boolean
|
|
200
|
+
}) => {
|
|
201
|
+
try {
|
|
202
|
+
await copySingletonToLocale({
|
|
203
|
+
data: {
|
|
204
|
+
singleton: path,
|
|
205
|
+
sourceLocale: locale ?? defaultContentLocale,
|
|
206
|
+
targetLocale,
|
|
207
|
+
overwrite,
|
|
208
|
+
},
|
|
209
|
+
})
|
|
210
|
+
const sourceLocale = locale ?? defaultContentLocale
|
|
211
|
+
const sourceLabel =
|
|
212
|
+
contentLocales.find((entry) => entry.code === sourceLocale)?.label ?? sourceLocale
|
|
213
|
+
const targetLabel =
|
|
214
|
+
contentLocales.find((entry) => entry.code === targetLocale)?.label ?? targetLocale
|
|
215
|
+
toast(
|
|
216
|
+
t('collections.edit.copyToLocaleTitle', { label }),
|
|
217
|
+
t('collections.edit.copiedSuccessMessage', {
|
|
218
|
+
source: sourceLabel,
|
|
219
|
+
target: targetLabel,
|
|
220
|
+
}),
|
|
221
|
+
'success'
|
|
222
|
+
)
|
|
223
|
+
notifyScheduleSuspended()
|
|
224
|
+
navigate({
|
|
225
|
+
to: getAdminRoutePath('singletons', '$singleton'),
|
|
226
|
+
params: { singleton: path },
|
|
227
|
+
search: { locale: targetLocale },
|
|
228
|
+
})
|
|
229
|
+
} catch (err) {
|
|
230
|
+
toast(
|
|
231
|
+
t('collections.edit.copyToLocaleTitle', { label }),
|
|
232
|
+
t('collections.edit.copyFailedDescription', { message: (err as Error).message }),
|
|
233
|
+
'danger'
|
|
234
|
+
)
|
|
235
|
+
throw err
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const handleSchedulePublication = async ({ publishAt }: { publishAt: string }) => {
|
|
240
|
+
try {
|
|
241
|
+
await scheduleSingletonPublish({
|
|
242
|
+
data: { singleton: path, publishAt, expectedVersionId: String(document?.versionId) },
|
|
243
|
+
})
|
|
244
|
+
toast(
|
|
245
|
+
t('scheduledPublication.toast.scheduledTitle'),
|
|
246
|
+
t('scheduledPublication.toast.scheduledDescription'),
|
|
247
|
+
'success'
|
|
248
|
+
)
|
|
249
|
+
await reload()
|
|
250
|
+
} catch (err) {
|
|
251
|
+
toast(t('scheduledPublication.toast.failedTitle'), (err as Error).message, 'danger')
|
|
252
|
+
throw err
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const handleConfirmScheduledPublication = async () => {
|
|
257
|
+
try {
|
|
258
|
+
await confirmSingletonScheduledPublish({
|
|
259
|
+
data: { singleton: path, expectedVersionId: String(document?.versionId) },
|
|
260
|
+
})
|
|
261
|
+
toast(
|
|
262
|
+
t('scheduledPublication.toast.confirmedTitle'),
|
|
263
|
+
t('scheduledPublication.toast.confirmedDescription'),
|
|
264
|
+
'success'
|
|
265
|
+
)
|
|
266
|
+
await reload()
|
|
267
|
+
} catch (err) {
|
|
268
|
+
toast(t('scheduledPublication.toast.failedTitle'), (err as Error).message, 'danger')
|
|
269
|
+
throw err
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const handleCancelScheduledPublication = async () => {
|
|
274
|
+
try {
|
|
275
|
+
await cancelSingletonScheduledPublish({ data: { singleton: path } })
|
|
276
|
+
toast(
|
|
277
|
+
t('scheduledPublication.toast.cancelledTitle'),
|
|
278
|
+
t('scheduledPublication.toast.cancelledDescription'),
|
|
279
|
+
'success'
|
|
280
|
+
)
|
|
281
|
+
await reload()
|
|
282
|
+
} catch (err) {
|
|
283
|
+
toast(t('scheduledPublication.toast.failedTitle'), (err as Error).message, 'danger')
|
|
284
|
+
throw err
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return (
|
|
289
|
+
<Section>
|
|
290
|
+
<Container>
|
|
291
|
+
<FormRenderer
|
|
292
|
+
mode={document == null ? 'create' : 'edit'}
|
|
293
|
+
fields={fields}
|
|
294
|
+
onSubmit={handleSubmit}
|
|
295
|
+
initialData={document ?? (initialData ? { fields: initialData } : undefined)}
|
|
296
|
+
adminConfig={adminConfig}
|
|
297
|
+
heading={label}
|
|
298
|
+
showPath={false}
|
|
299
|
+
initialLocale={locale}
|
|
300
|
+
defaultLocale={defaultContentLocale}
|
|
301
|
+
onLocaleChange={
|
|
302
|
+
document == null
|
|
303
|
+
? undefined
|
|
304
|
+
: (nextLocale) =>
|
|
305
|
+
navigate({
|
|
306
|
+
to: getAdminRoutePath('singletons', '$singleton'),
|
|
307
|
+
params: { singleton: path },
|
|
308
|
+
search: { locale: nextLocale },
|
|
309
|
+
})
|
|
310
|
+
}
|
|
311
|
+
useNavigationGuard={useTanStackNavigationGuard}
|
|
312
|
+
headerSlot={
|
|
313
|
+
document == null ? undefined : (
|
|
314
|
+
<SingletonViewMenu
|
|
315
|
+
singleton={path}
|
|
316
|
+
activeView="edit"
|
|
317
|
+
locale={locale}
|
|
318
|
+
contentLocales={contentLocales}
|
|
319
|
+
defaultContentLocale={defaultContentLocale}
|
|
320
|
+
adminConfig={adminConfig}
|
|
321
|
+
doc={document}
|
|
322
|
+
/>
|
|
323
|
+
)
|
|
324
|
+
}
|
|
325
|
+
onStatusChange={document == null ? undefined : handleStatusChange}
|
|
326
|
+
onUnpublish={document != null && publishedVersion ? handleUnpublish : undefined}
|
|
327
|
+
scheduledPublication={document == null ? null : scheduledPublication}
|
|
328
|
+
onSchedulePublication={canArmSchedule ? handleSchedulePublication : undefined}
|
|
329
|
+
onConfirmScheduledPublication={
|
|
330
|
+
canArmSchedule && scheduledPublication?.state === 'needs_reconfirm'
|
|
331
|
+
? handleConfirmScheduledPublication
|
|
332
|
+
: undefined
|
|
333
|
+
}
|
|
334
|
+
onCancelScheduledPublication={
|
|
335
|
+
document != null && scheduledPublication != null
|
|
336
|
+
? handleCancelScheduledPublication
|
|
337
|
+
: undefined
|
|
338
|
+
}
|
|
339
|
+
onCopyToLocale={document == null ? undefined : handleCopyToLocale}
|
|
340
|
+
contentLocales={document == null ? undefined : contentLocales}
|
|
341
|
+
publishedVersion={document == null ? null : publishedVersion}
|
|
342
|
+
restoreWarnings={restoreWarnings}
|
|
343
|
+
nextStatus={document == null ? undefined : nextStatus}
|
|
344
|
+
workflowStatuses={document == null ? undefined : workflowStatuses}
|
|
345
|
+
onCancel={() => navigate({ to: getAdminRoutePath() })}
|
|
346
|
+
collectionPath={path}
|
|
347
|
+
/>
|
|
348
|
+
</Container>
|
|
349
|
+
</Section>
|
|
350
|
+
)
|
|
351
|
+
}
|
|
@@ -16,12 +16,16 @@
|
|
|
16
16
|
// src/server.ts (TanStack Start server entry point) before any
|
|
17
17
|
// requests are handled. No need to import it here.
|
|
18
18
|
|
|
19
|
-
import type {
|
|
20
|
-
|
|
19
|
+
import type {
|
|
20
|
+
CollectionDefinition,
|
|
21
|
+
CollectionRecord,
|
|
22
|
+
MultiCollectionDefinition,
|
|
23
|
+
} from '@byline/core'
|
|
24
|
+
import { getCollectionDefinition, isSingleton } from '@byline/core'
|
|
21
25
|
|
|
22
26
|
import { bylineCore } from './byline-core.js'
|
|
23
27
|
|
|
24
|
-
export interface
|
|
28
|
+
export interface EnsuredDocumentResource {
|
|
25
29
|
definition: CollectionDefinition
|
|
26
30
|
collection: {
|
|
27
31
|
id: string
|
|
@@ -31,21 +35,28 @@ export interface EnsuredCollection {
|
|
|
31
35
|
}
|
|
32
36
|
}
|
|
33
37
|
|
|
38
|
+
export interface EnsuredCollection extends EnsuredDocumentResource {
|
|
39
|
+
definition: MultiCollectionDefinition
|
|
40
|
+
}
|
|
41
|
+
|
|
34
42
|
/**
|
|
35
|
-
* Resolve
|
|
43
|
+
* Resolve either registered document-resource kind against the reconciled
|
|
44
|
+
* collection record.
|
|
36
45
|
*
|
|
37
46
|
* Collections are reconciled with the database once at startup by
|
|
38
47
|
* `initBylineCore()` (see `packages/core/src/services/collection-bootstrap.ts`).
|
|
39
48
|
* This helper is a per-request cache lookup against the resulting in-memory
|
|
40
49
|
* registry — no DB I/O, no hash work.
|
|
41
50
|
*
|
|
42
|
-
*
|
|
51
|
+
* This broader seam is reserved for operations such as field upload whose core
|
|
52
|
+
* service is explicitly kind-aware; collection route families continue to use
|
|
53
|
+
* `ensureCollection` below.
|
|
43
54
|
*/
|
|
44
|
-
export async function
|
|
55
|
+
export async function ensureDocumentResource(
|
|
56
|
+
path: string
|
|
57
|
+
): Promise<EnsuredDocumentResource | null> {
|
|
45
58
|
const definition = getCollectionDefinition(path)
|
|
46
|
-
if (definition == null)
|
|
47
|
-
return null
|
|
48
|
-
}
|
|
59
|
+
if (definition == null) return null
|
|
49
60
|
|
|
50
61
|
let record: CollectionRecord
|
|
51
62
|
try {
|
|
@@ -64,3 +75,17 @@ export async function ensureCollection(path: string): Promise<EnsuredCollection
|
|
|
64
75
|
},
|
|
65
76
|
}
|
|
66
77
|
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Narrow the shared document-resource resolution to a multi-document
|
|
81
|
+
* collection for a collection-scoped admin API request.
|
|
82
|
+
*
|
|
83
|
+
* Returns `null` when the path is unregistered or belongs to a singleton, so
|
|
84
|
+
* collection route families cannot accidentally admit the other resource kind.
|
|
85
|
+
*/
|
|
86
|
+
export async function ensureCollection(path: string): Promise<EnsuredCollection | null> {
|
|
87
|
+
const resource = await ensureDocumentResource(path)
|
|
88
|
+
return resource != null && !isSingleton(resource.definition)
|
|
89
|
+
? { ...resource, definition: resource.definition }
|
|
90
|
+
: null
|
|
91
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
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
|
+
getCollectionStats: vi.fn(),
|
|
13
|
+
collections: [
|
|
14
|
+
{ path: 'articles', labels: { singular: 'Article', plural: 'Articles' }, showStats: true },
|
|
15
|
+
{
|
|
16
|
+
singleton: true,
|
|
17
|
+
path: 'site-settings',
|
|
18
|
+
label: 'Site settings',
|
|
19
|
+
// Deliberately collection-like: kind must win even for malformed input.
|
|
20
|
+
showStats: true,
|
|
21
|
+
},
|
|
22
|
+
],
|
|
23
|
+
}))
|
|
24
|
+
|
|
25
|
+
vi.mock('@tanstack/react-router', () => ({
|
|
26
|
+
createFileRoute: () => (options: Record<string, unknown>) => ({ options }),
|
|
27
|
+
}))
|
|
28
|
+
|
|
29
|
+
vi.mock('@byline/core', () => ({
|
|
30
|
+
filterReadableCollections: (collections: unknown[]) => collections,
|
|
31
|
+
getAdminConfig: () => ({ collections: mocks.collections }),
|
|
32
|
+
isSingleton: (definition: { singleton?: boolean }) => definition.singleton === true,
|
|
33
|
+
}))
|
|
34
|
+
|
|
35
|
+
vi.mock('../server-fns/collections/index.js', () => ({
|
|
36
|
+
getCollectionStats: mocks.getCollectionStats,
|
|
37
|
+
}))
|
|
38
|
+
vi.mock('../admin-shell/chrome/breadcrumbs/breadcrumbs-client.js', () => ({
|
|
39
|
+
BreadcrumbsClient: () => null,
|
|
40
|
+
}))
|
|
41
|
+
vi.mock('../admin-shell/chrome/dashboard.js', () => ({ AdminDashboard: () => null }))
|
|
42
|
+
vi.mock('@byline/i18n/react', () => ({ useTranslation: () => ({ t: (key: string) => key }) }))
|
|
43
|
+
vi.mock('./admin-path.js', () => ({ getAdminRoutePath: () => '/admin' }))
|
|
44
|
+
|
|
45
|
+
import { createAdminDashboardRoute } from './create-admin-dashboard-route.js'
|
|
46
|
+
|
|
47
|
+
type DashboardLoader = (input: {
|
|
48
|
+
context: { user: { is_super_admin: boolean; abilities: string[] } }
|
|
49
|
+
}) => Promise<{ statsMap: Record<string, Array<{ status: string; count: number }>> }>
|
|
50
|
+
|
|
51
|
+
describe('admin dashboard route loader', () => {
|
|
52
|
+
beforeEach(() => {
|
|
53
|
+
vi.clearAllMocks()
|
|
54
|
+
mocks.getCollectionStats.mockResolvedValue([{ status: 'published', count: 4 }])
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('never requests collection statistics for a singleton resource', async () => {
|
|
58
|
+
const route = createAdminDashboardRoute('/admin/') as {
|
|
59
|
+
options: { loader: DashboardLoader }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
await expect(
|
|
63
|
+
route.options.loader({ context: { user: { is_super_admin: true, abilities: [] } } })
|
|
64
|
+
).resolves.toEqual({ statsMap: { articles: [{ status: 'published', count: 4 }] } })
|
|
65
|
+
|
|
66
|
+
expect(mocks.getCollectionStats).toHaveBeenCalledOnce()
|
|
67
|
+
expect(mocks.getCollectionStats).toHaveBeenCalledWith('articles')
|
|
68
|
+
expect(mocks.getCollectionStats).not.toHaveBeenCalledWith('site-settings')
|
|
69
|
+
})
|
|
70
|
+
})
|
|
@@ -8,7 +8,12 @@
|
|
|
8
8
|
|
|
9
9
|
import { createFileRoute } from '@tanstack/react-router'
|
|
10
10
|
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
filterReadableCollections,
|
|
13
|
+
getAdminConfig,
|
|
14
|
+
isSingleton,
|
|
15
|
+
type MultiCollectionDefinition,
|
|
16
|
+
} from '@byline/core'
|
|
12
17
|
import { useTranslation } from '@byline/i18n/react'
|
|
13
18
|
|
|
14
19
|
import { BreadcrumbsClient } from '../admin-shell/chrome/breadcrumbs/breadcrumbs-client.js'
|
|
@@ -40,7 +45,10 @@ export function createAdminDashboardRoute(path: string) {
|
|
|
40
45
|
|
|
41
46
|
await Promise.all(
|
|
42
47
|
visible
|
|
43
|
-
.filter(
|
|
48
|
+
.filter(
|
|
49
|
+
(resource): resource is MultiCollectionDefinition =>
|
|
50
|
+
!isSingleton(resource) && resource.showStats === true
|
|
51
|
+
)
|
|
44
52
|
.map(async (c) => {
|
|
45
53
|
try {
|
|
46
54
|
statsMap[c.path] = await getCollectionStats(c.path)
|
|
@@ -8,8 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import { createFileRoute, notFound } from '@tanstack/react-router'
|
|
10
10
|
|
|
11
|
-
import type {
|
|
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 =
|
|
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 =
|
|
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 {
|
|
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 =
|
|
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 =
|
|
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 {
|
|
13
|
-
import { getCollectionAdminConfig
|
|
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 =
|
|
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 =
|
|
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 {
|
|
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 =
|
|
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 =
|
|
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()
|