@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,199 @@
|
|
|
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 React from 'react'
|
|
10
|
+
import { act } from 'react'
|
|
11
|
+
|
|
12
|
+
import { defineAdminConfig, defineSingleton, type MultiCollectionDefinition } from '@byline/core'
|
|
13
|
+
import { createRoot } from 'react-dom/client'
|
|
14
|
+
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
15
|
+
|
|
16
|
+
const abilityState = vi.hoisted(() => ({
|
|
17
|
+
isSuperAdmin: false,
|
|
18
|
+
abilities: [] as string[],
|
|
19
|
+
}))
|
|
20
|
+
|
|
21
|
+
vi.mock('../../integrations/abilities.jsx', () => ({
|
|
22
|
+
useAbilities: () => ({
|
|
23
|
+
isSuperAdmin: abilityState.isSuperAdmin,
|
|
24
|
+
abilities: abilityState.abilities,
|
|
25
|
+
has: () => false,
|
|
26
|
+
hasAny: () => false,
|
|
27
|
+
}),
|
|
28
|
+
}))
|
|
29
|
+
|
|
30
|
+
vi.mock('@byline/i18n/react', () => ({
|
|
31
|
+
useTranslation: () => ({
|
|
32
|
+
t: (key: string, values?: Record<string, string | number>) => {
|
|
33
|
+
if (key === 'dashboard.totalCount') return `${values?.count} total`
|
|
34
|
+
if (key === 'dashboard.collectionDescription') return `Manage ${values?.label}`
|
|
35
|
+
return key
|
|
36
|
+
},
|
|
37
|
+
}),
|
|
38
|
+
}))
|
|
39
|
+
|
|
40
|
+
vi.mock('@byline/ui/react', () => {
|
|
41
|
+
type Props = { children?: React.ReactNode; className?: string }
|
|
42
|
+
const element = (tag: keyof React.JSX.IntrinsicElements) =>
|
|
43
|
+
function Element({ children, ...props }: Props) {
|
|
44
|
+
const Component = tag
|
|
45
|
+
return <Component {...props}>{children}</Component>
|
|
46
|
+
}
|
|
47
|
+
const Card = element('article') as ReturnType<typeof element> & {
|
|
48
|
+
Header: ReturnType<typeof element>
|
|
49
|
+
Title: ReturnType<typeof element>
|
|
50
|
+
Description: ReturnType<typeof element>
|
|
51
|
+
Content: ReturnType<typeof element>
|
|
52
|
+
}
|
|
53
|
+
Card.Header = element('header')
|
|
54
|
+
Card.Title = element('h3')
|
|
55
|
+
Card.Description = element('p')
|
|
56
|
+
Card.Content = element('div')
|
|
57
|
+
return { Card, Container: element('div'), Section: element('section') }
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
vi.mock('./loose-router.js', () => ({
|
|
61
|
+
Link: ({
|
|
62
|
+
to = '',
|
|
63
|
+
params = {},
|
|
64
|
+
search: _search,
|
|
65
|
+
children,
|
|
66
|
+
...props
|
|
67
|
+
}: React.AnchorHTMLAttributes<HTMLAnchorElement> & {
|
|
68
|
+
to?: string
|
|
69
|
+
params?: Record<string, string>
|
|
70
|
+
search?: unknown
|
|
71
|
+
}) => {
|
|
72
|
+
let href = to
|
|
73
|
+
for (const [name, value] of Object.entries(params)) {
|
|
74
|
+
href = href.replace(`$${name}`, value)
|
|
75
|
+
}
|
|
76
|
+
return (
|
|
77
|
+
<a href={href} {...props}>
|
|
78
|
+
{children}
|
|
79
|
+
</a>
|
|
80
|
+
)
|
|
81
|
+
},
|
|
82
|
+
}))
|
|
83
|
+
|
|
84
|
+
import { AdminDashboard } from './dashboard.js'
|
|
85
|
+
|
|
86
|
+
const articles: MultiCollectionDefinition = {
|
|
87
|
+
path: 'articles',
|
|
88
|
+
labels: { singular: 'Article', plural: 'Articles' },
|
|
89
|
+
fields: [{ name: 'title', label: 'Title', type: 'text' }],
|
|
90
|
+
showStats: true,
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const settings = defineSingleton({
|
|
94
|
+
path: 'site-settings',
|
|
95
|
+
label: 'Site settings',
|
|
96
|
+
fields: [{ name: 'title', label: 'Title', type: 'text' }],
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
function renderDashboard(statsMap: Record<string, Array<{ status: string; count: number }>> = {}) {
|
|
100
|
+
const container = document.createElement('div')
|
|
101
|
+
document.body.append(container)
|
|
102
|
+
const root = createRoot(container)
|
|
103
|
+
act(() => root.render(<AdminDashboard statsMap={statsMap} />))
|
|
104
|
+
return {
|
|
105
|
+
container,
|
|
106
|
+
dispose() {
|
|
107
|
+
act(() => root.unmount())
|
|
108
|
+
container.remove()
|
|
109
|
+
},
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
describe('AdminDashboard document resources', () => {
|
|
114
|
+
beforeAll(() => {
|
|
115
|
+
;(
|
|
116
|
+
globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
|
|
117
|
+
).IS_REACT_ACT_ENVIRONMENT = true
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
afterAll(() => {
|
|
121
|
+
delete (globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean })
|
|
122
|
+
.IS_REACT_ACT_ENVIRONMENT
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
beforeEach(() => {
|
|
126
|
+
abilityState.isSuperAdmin = false
|
|
127
|
+
abilityState.abilities = []
|
|
128
|
+
defineAdminConfig({
|
|
129
|
+
routes: { admin: '/internal/cms' },
|
|
130
|
+
collections: [articles, settings],
|
|
131
|
+
admin: [
|
|
132
|
+
{ slug: articles.path, group: 'content' },
|
|
133
|
+
{ singleton: true, slug: settings.path, group: 'content' },
|
|
134
|
+
],
|
|
135
|
+
collectionGroups: [{ name: 'content', label: 'Content' }],
|
|
136
|
+
i18n: {
|
|
137
|
+
admin: { defaultLocale: 'en', locales: [] },
|
|
138
|
+
content: { defaultLocale: 'en', locales: [] },
|
|
139
|
+
},
|
|
140
|
+
})
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
it('shows a singleton only for its kind-aware read ability', () => {
|
|
144
|
+
abilityState.abilities = ['singletons.site-settings.read']
|
|
145
|
+
const allowed = renderDashboard()
|
|
146
|
+
expect(allowed.container.textContent).toContain('Site settings')
|
|
147
|
+
expect(allowed.container.textContent).not.toContain('Articles')
|
|
148
|
+
allowed.dispose()
|
|
149
|
+
|
|
150
|
+
abilityState.abilities = ['collections.site-settings.read']
|
|
151
|
+
const staleNamespace = renderDashboard()
|
|
152
|
+
expect(staleNamespace.container.textContent).not.toContain('Site settings')
|
|
153
|
+
staleNamespace.dispose()
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
it('shows both resource kinds to a super admin in registry order', () => {
|
|
157
|
+
abilityState.isSuperAdmin = true
|
|
158
|
+
const view = renderDashboard({ articles: [{ status: 'draft', count: 3 }] })
|
|
159
|
+
|
|
160
|
+
const titles = [...view.container.querySelectorAll('.byline-dashboard-title-text')].map(
|
|
161
|
+
(element) => element.textContent
|
|
162
|
+
)
|
|
163
|
+
expect(titles).toEqual(['Articles', 'Site settings'])
|
|
164
|
+
expect(view.container.querySelector('.byline-dashboard-group-heading')?.textContent).toBe(
|
|
165
|
+
'Content'
|
|
166
|
+
)
|
|
167
|
+
view.dispose()
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
it('links a singleton under the custom admin base without collection stats', () => {
|
|
171
|
+
abilityState.isSuperAdmin = true
|
|
172
|
+
const view = renderDashboard({
|
|
173
|
+
articles: [{ status: 'draft', count: 3 }],
|
|
174
|
+
'site-settings': [{ status: 'published', count: 99 }],
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
const link = view.container.querySelector<HTMLAnchorElement>(
|
|
178
|
+
'a[href="/internal/cms/singletons/site-settings"]'
|
|
179
|
+
)
|
|
180
|
+
const card = link?.closest('article')
|
|
181
|
+
expect(link).not.toBeNull()
|
|
182
|
+
expect(card?.querySelector('.byline-dashboard-stat-tile')).toBeNull()
|
|
183
|
+
expect(card?.querySelector('.byline-dashboard-title-meta')).toBeNull()
|
|
184
|
+
expect(card?.textContent).not.toContain('99')
|
|
185
|
+
expect(card?.textContent?.match(/Manage Site settings/g)).toHaveLength(1)
|
|
186
|
+
expect(
|
|
187
|
+
view.container.querySelector('a[href="/internal/cms/collections/articles"]')
|
|
188
|
+
).not.toBeNull()
|
|
189
|
+
expect(view.container.querySelector('.byline-dashboard-stat-tile')).not.toBeNull()
|
|
190
|
+
view.dispose()
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
it('hides an update-only singleton role from the dashboard', () => {
|
|
194
|
+
abilityState.abilities = ['singletons.site-settings.update']
|
|
195
|
+
const view = renderDashboard()
|
|
196
|
+
expect(view.container.textContent).not.toContain('Site settings')
|
|
197
|
+
view.dispose()
|
|
198
|
+
})
|
|
199
|
+
})
|
|
@@ -6,12 +6,13 @@
|
|
|
6
6
|
* Copyright (c) Infonomic Company Limited
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import type {
|
|
9
|
+
import type { MultiCollectionDefinition, SingletonDefinition, WorkflowStatus } from '@byline/core'
|
|
10
10
|
import {
|
|
11
11
|
filterReadableCollections,
|
|
12
12
|
getAdminConfig,
|
|
13
13
|
getWorkflowStatuses,
|
|
14
14
|
groupCollectionsForAdmin,
|
|
15
|
+
isSingleton,
|
|
15
16
|
} from '@byline/core'
|
|
16
17
|
import { useTranslation } from '@byline/i18n/react'
|
|
17
18
|
import { Card, Container, Section } from '@byline/ui/react'
|
|
@@ -68,7 +69,7 @@ function CollectionCard({
|
|
|
68
69
|
collection,
|
|
69
70
|
stats,
|
|
70
71
|
}: {
|
|
71
|
-
collection:
|
|
72
|
+
collection: MultiCollectionDefinition
|
|
72
73
|
stats: CollectionStatusCount[] | undefined
|
|
73
74
|
}) {
|
|
74
75
|
const { t } = useTranslation('byline-admin')
|
|
@@ -134,6 +135,35 @@ function CollectionCard({
|
|
|
134
135
|
)
|
|
135
136
|
}
|
|
136
137
|
|
|
138
|
+
function SingletonCard({ singleton }: { singleton: SingletonDefinition }) {
|
|
139
|
+
const { t } = useTranslation('byline-admin')
|
|
140
|
+
const destination = getAdminRoutePath('singletons', '$singleton')
|
|
141
|
+
const params = { singleton: singleton.path }
|
|
142
|
+
|
|
143
|
+
return (
|
|
144
|
+
<Card>
|
|
145
|
+
<Link
|
|
146
|
+
to={destination}
|
|
147
|
+
params={params}
|
|
148
|
+
className={cx('byline-dashboard-card-link', styles.cardLink)}
|
|
149
|
+
>
|
|
150
|
+
<Card.Header>
|
|
151
|
+
<div className={cx('byline-dashboard-card-header', styles.cardHeader)}>
|
|
152
|
+
<Card.Title className={cx('byline-dashboard-card-title', styles.cardTitle)}>
|
|
153
|
+
<span className={cx('byline-dashboard-title-text', styles.titleText)}>
|
|
154
|
+
{singleton.label}
|
|
155
|
+
</span>
|
|
156
|
+
</Card.Title>
|
|
157
|
+
<Card.Description className="muted">
|
|
158
|
+
{t('dashboard.collectionDescription', { label: singleton.label })}
|
|
159
|
+
</Card.Description>
|
|
160
|
+
</div>
|
|
161
|
+
</Card.Header>
|
|
162
|
+
</Link>
|
|
163
|
+
</Card>
|
|
164
|
+
)
|
|
165
|
+
}
|
|
166
|
+
|
|
137
167
|
interface AdminDashboardProps {
|
|
138
168
|
statsMap: Record<string, CollectionStatusCount[]>
|
|
139
169
|
}
|
|
@@ -173,13 +203,17 @@ export function AdminDashboard({ statsMap }: AdminDashboardProps) {
|
|
|
173
203
|
</h2>
|
|
174
204
|
)}
|
|
175
205
|
<div className={cx('byline-dashboard-grid', styles.grid)}>
|
|
176
|
-
{bucket.collections.map((
|
|
177
|
-
|
|
178
|
-
key={
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
206
|
+
{bucket.collections.map((resource) =>
|
|
207
|
+
isSingleton(resource) ? (
|
|
208
|
+
<SingletonCard key={resource.path} singleton={resource} />
|
|
209
|
+
) : (
|
|
210
|
+
<CollectionCard
|
|
211
|
+
key={resource.path}
|
|
212
|
+
collection={resource}
|
|
213
|
+
stats={statsMap[resource.path]}
|
|
214
|
+
/>
|
|
215
|
+
)
|
|
216
|
+
)}
|
|
183
217
|
</div>
|
|
184
218
|
</section>
|
|
185
219
|
))}
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* Copyright (c) Infonomic Company Limited
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import { useCallback } from 'react'
|
|
9
10
|
import { useRouterState } from '@tanstack/react-router'
|
|
10
11
|
|
|
11
12
|
import type { PaginationProps } from '@byline/ui/react'
|
|
@@ -18,6 +19,7 @@ import {
|
|
|
18
19
|
} from '@byline/ui/react'
|
|
19
20
|
|
|
20
21
|
import { Link } from './loose-router.js'
|
|
22
|
+
import { scrollWindowToTop } from './scroll-to-top.js'
|
|
21
23
|
|
|
22
24
|
interface RouterPageProps extends PaginationProps {
|
|
23
25
|
smoothScrollToTop?: boolean
|
|
@@ -27,6 +29,13 @@ interface RouterPageProps extends PaginationProps {
|
|
|
27
29
|
* A convenience pager wrapped around uikit's Pagination with `asChild`
|
|
28
30
|
* targets that delegate to TanStack Router's `<Link>`. `asChild` merges
|
|
29
31
|
* the existing button styles into the rendered Link.
|
|
32
|
+
*
|
|
33
|
+
* `smoothScrollToTop` is for a pager sitting *below* a long list: turning a
|
|
34
|
+
* page there should carry the reader back to the top rather than leaving them
|
|
35
|
+
* at the foot of a fresh page. It suppresses the router's own instant
|
|
36
|
+
* `resetScroll` and animates instead — so the component owes an actual
|
|
37
|
+
* scroll, which was the missing half: the flag disabled the reset and put
|
|
38
|
+
* nothing in its place, so a bottom pager did not scroll at all.
|
|
30
39
|
*/
|
|
31
40
|
export function RouterPager({
|
|
32
41
|
className,
|
|
@@ -36,6 +45,14 @@ export function RouterPager({
|
|
|
36
45
|
}: RouterPageProps): React.JSX.Element {
|
|
37
46
|
const location = useRouterState({ select: (s) => s.location })
|
|
38
47
|
|
|
48
|
+
// Fires alongside navigation rather than after it: the scroll and the new
|
|
49
|
+
// page load run together, so the list is in place by the time the reader
|
|
50
|
+
// arrives at the top.
|
|
51
|
+
const handleScrollToTop = useCallback(() => {
|
|
52
|
+
if (smoothScrollToTop !== true) return
|
|
53
|
+
scrollWindowToTop()
|
|
54
|
+
}, [smoothScrollToTop])
|
|
55
|
+
|
|
39
56
|
return (
|
|
40
57
|
<Pagination variant="dashboard" {...rest}>
|
|
41
58
|
<Pagination.Root className={className} ariaLabel={ariaLabel}>
|
|
@@ -51,6 +68,7 @@ export function RouterPager({
|
|
|
51
68
|
) : (
|
|
52
69
|
<Link
|
|
53
70
|
resetScroll={smoothScrollToTop !== true}
|
|
71
|
+
onClick={handleScrollToTop}
|
|
54
72
|
to={location.pathname as never}
|
|
55
73
|
search={params}
|
|
56
74
|
/>
|
|
@@ -76,6 +94,7 @@ export function RouterPager({
|
|
|
76
94
|
) : (
|
|
77
95
|
<Link
|
|
78
96
|
resetScroll={smoothScrollToTop !== true}
|
|
97
|
+
onClick={handleScrollToTop}
|
|
79
98
|
to={location.pathname as never}
|
|
80
99
|
search={params}
|
|
81
100
|
/>
|
|
@@ -103,6 +122,7 @@ export function RouterPager({
|
|
|
103
122
|
) : (
|
|
104
123
|
<Link
|
|
105
124
|
resetScroll={smoothScrollToTop !== true}
|
|
125
|
+
onClick={handleScrollToTop}
|
|
106
126
|
to={location.pathname as never}
|
|
107
127
|
search={params}
|
|
108
128
|
/>
|
|
@@ -130,6 +150,7 @@ export function RouterPager({
|
|
|
130
150
|
) : (
|
|
131
151
|
<Link
|
|
132
152
|
resetScroll={smoothScrollToTop !== true}
|
|
153
|
+
onClick={handleScrollToTop}
|
|
133
154
|
to={location.pathname as never}
|
|
134
155
|
search={params}
|
|
135
156
|
/>
|
|
@@ -156,6 +177,7 @@ export function RouterPager({
|
|
|
156
177
|
) : (
|
|
157
178
|
<Link
|
|
158
179
|
resetScroll={smoothScrollToTop !== true}
|
|
180
|
+
onClick={handleScrollToTop}
|
|
159
181
|
to={location.pathname as never}
|
|
160
182
|
search={params}
|
|
161
183
|
/>
|
|
@@ -0,0 +1,56 @@
|
|
|
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 { afterEach, describe, expect, it, vi } from 'vitest'
|
|
10
|
+
|
|
11
|
+
import { scrollWindowToTop } from './scroll-to-top.js'
|
|
12
|
+
|
|
13
|
+
function mockReducedMotion(matches: boolean) {
|
|
14
|
+
vi.stubGlobal(
|
|
15
|
+
'matchMedia',
|
|
16
|
+
vi.fn((query: string) => ({ matches, media: query })) as unknown as typeof window.matchMedia
|
|
17
|
+
)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
vi.unstubAllGlobals()
|
|
22
|
+
vi.restoreAllMocks()
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
describe('scrollWindowToTop', () => {
|
|
26
|
+
it('scrolls to the top of the page', () => {
|
|
27
|
+
const scrollTo = vi.fn()
|
|
28
|
+
vi.stubGlobal('scrollTo', scrollTo)
|
|
29
|
+
mockReducedMotion(false)
|
|
30
|
+
|
|
31
|
+
scrollWindowToTop()
|
|
32
|
+
|
|
33
|
+
expect(scrollTo).toHaveBeenCalledWith({ top: 0, behavior: 'smooth' })
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('skips the animation when the reader asks for reduced motion', () => {
|
|
37
|
+
const scrollTo = vi.fn()
|
|
38
|
+
vi.stubGlobal('scrollTo', scrollTo)
|
|
39
|
+
mockReducedMotion(true)
|
|
40
|
+
|
|
41
|
+
scrollWindowToTop()
|
|
42
|
+
|
|
43
|
+
// Still goes to the top — only the travel is dropped.
|
|
44
|
+
expect(scrollTo).toHaveBeenCalledWith({ top: 0, behavior: 'auto' })
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
/** Older browsers and some test environments have no `matchMedia`. */
|
|
48
|
+
it('still scrolls when matchMedia is unavailable', () => {
|
|
49
|
+
const scrollTo = vi.fn()
|
|
50
|
+
vi.stubGlobal('scrollTo', scrollTo)
|
|
51
|
+
vi.stubGlobal('matchMedia', undefined)
|
|
52
|
+
|
|
53
|
+
expect(() => scrollWindowToTop()).not.toThrow()
|
|
54
|
+
expect(scrollTo).toHaveBeenCalledWith({ top: 0, behavior: 'smooth' })
|
|
55
|
+
})
|
|
56
|
+
})
|
|
@@ -0,0 +1,28 @@
|
|
|
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
|
+
/**
|
|
10
|
+
* Return the reader to the top of the page.
|
|
11
|
+
*
|
|
12
|
+
* Separate from `RouterPager` so the behaviour can be asserted without
|
|
13
|
+
* mounting a router. `smoothScrollToTop` suppresses TanStack's instant
|
|
14
|
+
* `resetScroll` in favour of an animated one, which makes a missing
|
|
15
|
+
* implementation silent — the page simply does not move. That is what
|
|
16
|
+
* happened to every pager carrying the flag.
|
|
17
|
+
*
|
|
18
|
+
* Honours `prefers-reduced-motion`: those readers still arrive at the top,
|
|
19
|
+
* without the travel.
|
|
20
|
+
*/
|
|
21
|
+
export function scrollWindowToTop(): void {
|
|
22
|
+
if (typeof window === 'undefined') return
|
|
23
|
+
|
|
24
|
+
// `matchMedia` is absent in some test and server-ish environments; treating
|
|
25
|
+
// that as "no preference expressed" keeps the scroll working either way.
|
|
26
|
+
const prefersReducedMotion = window.matchMedia?.('(prefers-reduced-motion: reduce)').matches
|
|
27
|
+
window.scrollTo({ top: 0, behavior: prefersReducedMotion === true ? 'auto' : 'smooth' })
|
|
28
|
+
}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* Copyright (c) Infonomic Company Limited
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import type {
|
|
9
|
+
import type { MultiCollectionDefinition } from '@byline/core'
|
|
10
10
|
import type { AnyCollectionSchemaTypes } from '@byline/core/zod-schemas'
|
|
11
11
|
import { useTranslation } from '@byline/i18n/react'
|
|
12
12
|
import { Container, Section } from '@byline/ui/react'
|
|
@@ -32,7 +32,7 @@ export const ApiView = ({
|
|
|
32
32
|
contentLocales,
|
|
33
33
|
defaultContentLocale,
|
|
34
34
|
}: {
|
|
35
|
-
collectionDefinition:
|
|
35
|
+
collectionDefinition: MultiCollectionDefinition
|
|
36
36
|
initialData: AnyCollectionSchemaTypes['UpdateType']
|
|
37
37
|
/** Currently active locale from the route search param (undefined → all). */
|
|
38
38
|
locale?: string
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import { useState } from 'react'
|
|
10
10
|
|
|
11
11
|
import { FormRenderer } from '@byline/admin/react'
|
|
12
|
-
import type { CollectionAdminConfig,
|
|
12
|
+
import type { CollectionAdminConfig, MultiCollectionDefinition } from '@byline/core'
|
|
13
13
|
import { useTranslation } from '@byline/i18n/react'
|
|
14
14
|
import { Container, Section, useToastManager } from '@byline/ui/react'
|
|
15
15
|
|
|
@@ -30,7 +30,7 @@ export const CreateView = ({
|
|
|
30
30
|
initialData,
|
|
31
31
|
from,
|
|
32
32
|
}: {
|
|
33
|
-
collectionDefinition:
|
|
33
|
+
collectionDefinition: MultiCollectionDefinition
|
|
34
34
|
adminConfig?: CollectionAdminConfig
|
|
35
35
|
// biome-ignore lint/suspicious/noExplicitAny: shape is collection-specific
|
|
36
36
|
initialData?: Record<string, any>
|
|
@@ -105,6 +105,10 @@ export const CreateView = ({
|
|
|
105
105
|
status: 'failed',
|
|
106
106
|
message: description,
|
|
107
107
|
})
|
|
108
|
+
|
|
109
|
+
// Rethrow so FormRenderer keeps the form dirty. The toast above has
|
|
110
|
+
// already told the editor what went wrong.
|
|
111
|
+
throw err
|
|
108
112
|
}
|
|
109
113
|
}
|
|
110
114
|
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import { useState } from 'react'
|
|
10
10
|
|
|
11
11
|
import { FormRenderer } from '@byline/admin/react'
|
|
12
|
-
import type { CollectionAdminConfig,
|
|
12
|
+
import type { CollectionAdminConfig, MultiCollectionDefinition } from '@byline/core'
|
|
13
13
|
import {
|
|
14
14
|
getDefaultStatus,
|
|
15
15
|
getWorkflow,
|
|
@@ -56,7 +56,7 @@ export const EditView = ({
|
|
|
56
56
|
defaultContentLocale,
|
|
57
57
|
returnSearch,
|
|
58
58
|
}: {
|
|
59
|
-
collectionDefinition:
|
|
59
|
+
collectionDefinition: MultiCollectionDefinition
|
|
60
60
|
adminConfig?: CollectionAdminConfig
|
|
61
61
|
initialData: AnyCollectionSchemaTypes['UpdateType']
|
|
62
62
|
locale?: string
|
|
@@ -623,6 +623,10 @@ export const EditView = ({
|
|
|
623
623
|
})
|
|
624
624
|
|
|
625
625
|
setEditState({ status: 'failed', message: description })
|
|
626
|
+
|
|
627
|
+
// Rethrow so FormRenderer keeps the form dirty. The toast above has
|
|
628
|
+
// already told the editor what went wrong.
|
|
629
|
+
throw err
|
|
626
630
|
}
|
|
627
631
|
}
|
|
628
632
|
|