@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
|
@@ -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 {
|
|
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 =
|
|
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 =
|
|
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
|
+
}
|
|
@@ -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
|
+
}
|