@byline/host-tanstack-start 2.5.2 → 2.6.1
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/admin-roles/container.js +38 -24
- package/dist/admin-shell/admin-roles/delete.js +9 -7
- package/dist/admin-shell/admin-roles/list.js +20 -16
- package/dist/admin-shell/admin-users/container.js +79 -56
- package/dist/admin-shell/admin-users/delete.js +10 -8
- package/dist/admin-shell/admin-users/list.js +27 -18
- package/dist/admin-shell/chrome/admin-app-bar.js +5 -2
- package/dist/admin-shell/chrome/breadcrumbs/breadcrumbs.js +3 -1
- package/dist/admin-shell/chrome/dashboard.js +13 -11
- package/dist/admin-shell/chrome/hamburger.js +3 -1
- package/dist/admin-shell/chrome/menu-drawer.js +7 -5
- package/dist/admin-shell/chrome/preview-toggle.js +5 -3
- package/dist/admin-shell/chrome/route-error.d.ts +3 -2
- package/dist/admin-shell/chrome/route-error.js +29 -22
- package/dist/admin-shell/chrome/sign-in-page.d.ts +16 -4
- package/dist/admin-shell/chrome/sign-in-page.js +38 -13
- package/dist/admin-shell/chrome/sign-in-page.module.js +1 -0
- package/dist/admin-shell/chrome/sign-in-page_module.css +8 -1
- package/dist/admin-shell/collections/api.js +6 -5
- package/dist/admin-shell/collections/create.js +12 -4
- package/dist/admin-shell/collections/edit.js +112 -37
- package/dist/admin-shell/collections/history.js +17 -12
- package/dist/admin-shell/collections/list.js +18 -13
- package/dist/admin-shell/collections/preview-link.d.ts +1 -10
- package/dist/admin-shell/collections/preview-link.js +9 -11
- package/dist/admin-shell/collections/resolve-preview-url.d.ts +34 -0
- package/dist/admin-shell/collections/resolve-preview-url.js +17 -0
- package/dist/admin-shell/collections/restore-version-modal.js +13 -14
- package/dist/admin-shell/collections/tanstack-navigation-guard.d.ts +1 -1
- package/dist/admin-shell/collections/view-menu.js +7 -5
- package/dist/i18n/index.d.ts +19 -0
- package/dist/i18n/index.js +4 -0
- package/dist/i18n/locale-cookie.d.ts +17 -0
- package/dist/i18n/locale-cookie.js +26 -0
- package/dist/i18n/locale-definitions.d.ts +29 -0
- package/dist/i18n/locale-definitions.js +27 -0
- package/dist/i18n/resolve-locale.d.ts +20 -0
- package/dist/i18n/resolve-locale.js +43 -0
- package/dist/i18n/server-translator.d.ts +33 -0
- package/dist/i18n/server-translator.js +19 -0
- package/dist/integrations/byline-admin-services.js +2 -0
- package/dist/integrations/byline-field-services.d.ts +3 -3
- package/dist/routes/create-admin-account-route.js +6 -3
- package/dist/routes/create-admin-dashboard-route.js +3 -1
- package/dist/routes/create-admin-layout-route.js +48 -25
- package/dist/routes/create-admin-permissions-route.js +4 -2
- package/dist/routes/create-admin-role-edit-route.js +5 -3
- package/dist/routes/create-admin-roles-list-route.js +4 -2
- package/dist/routes/create-admin-user-edit-route.js +5 -3
- package/dist/routes/create-admin-users-list-route.js +4 -2
- package/dist/routes/create-collection-api-route.js +5 -3
- package/dist/routes/create-collection-create-route.js +4 -2
- package/dist/routes/create-collection-edit-route.js +4 -2
- package/dist/routes/create-collection-history-route.js +5 -3
- package/dist/routes/create-collection-list-route.js +11 -5
- package/dist/routes/create-sign-in-route.js +10 -1
- package/dist/server-fns/admin-account/change-password.d.ts +1 -0
- package/dist/server-fns/admin-account/get.d.ts +1 -0
- package/dist/server-fns/admin-account/update.d.ts +1 -0
- package/dist/server-fns/admin-users/create.d.ts +1 -0
- package/dist/server-fns/admin-users/get.d.ts +1 -0
- package/dist/server-fns/admin-users/list.d.ts +1 -0
- package/dist/server-fns/admin-users/set-password.d.ts +1 -0
- package/dist/server-fns/admin-users/update.d.ts +1 -0
- package/dist/server-fns/auth/sign-in.js +18 -0
- package/dist/server-fns/i18n/get-active-locale.d.ts +8 -0
- package/dist/server-fns/i18n/get-active-locale.js +6 -0
- package/dist/server-fns/i18n/index.d.ts +10 -0
- package/dist/server-fns/i18n/index.js +2 -0
- package/dist/server-fns/i18n/set-locale.d.ts +25 -0
- package/dist/server-fns/i18n/set-locale.js +42 -0
- package/package.json +16 -7
- package/src/admin-shell/admin-roles/container.tsx +41 -31
- package/src/admin-shell/admin-roles/delete.tsx +10 -11
- package/src/admin-shell/admin-roles/list.tsx +29 -16
- package/src/admin-shell/admin-users/container.tsx +77 -50
- package/src/admin-shell/admin-users/delete.tsx +11 -12
- package/src/admin-shell/admin-users/list.tsx +39 -18
- package/src/admin-shell/chrome/admin-app-bar.tsx +5 -2
- package/src/admin-shell/chrome/breadcrumbs/breadcrumbs.tsx +3 -1
- package/src/admin-shell/chrome/dashboard.tsx +9 -3
- package/src/admin-shell/chrome/hamburger.tsx +3 -1
- package/src/admin-shell/chrome/menu-drawer.tsx +7 -5
- package/src/admin-shell/chrome/preview-toggle.tsx +6 -4
- package/src/admin-shell/chrome/route-error.tsx +39 -26
- package/src/admin-shell/chrome/sign-in-page.module.css +10 -1
- package/src/admin-shell/chrome/sign-in-page.tsx +46 -12
- package/src/admin-shell/collections/api.tsx +5 -1
- package/src/admin-shell/collections/create.tsx +10 -4
- package/src/admin-shell/collections/edit.tsx +79 -72
- package/src/admin-shell/collections/history.tsx +18 -12
- package/src/admin-shell/collections/list.tsx +25 -14
- package/src/admin-shell/collections/preview-link.tsx +20 -33
- package/src/admin-shell/collections/resolve-preview-url.test.node.ts +167 -0
- package/src/admin-shell/collections/resolve-preview-url.ts +67 -0
- package/src/admin-shell/collections/restore-version-modal.tsx +11 -12
- package/src/admin-shell/collections/tanstack-navigation-guard.ts +1 -1
- package/src/admin-shell/collections/view-menu.tsx +9 -5
- package/src/i18n/index.ts +26 -0
- package/src/i18n/locale-cookie.ts +68 -0
- package/src/i18n/locale-definitions.ts +48 -0
- package/src/i18n/resolve-locale.ts +96 -0
- package/src/i18n/server-translator.ts +60 -0
- package/src/integrations/byline-admin-services.ts +2 -0
- package/src/integrations/byline-field-services.ts +7 -3
- package/src/routes/create-admin-account-route.tsx +6 -4
- package/src/routes/create-admin-dashboard-route.tsx +5 -1
- package/src/routes/create-admin-layout-route.tsx +53 -20
- package/src/routes/create-admin-permissions-route.tsx +4 -2
- package/src/routes/create-admin-role-edit-route.tsx +5 -3
- package/src/routes/create-admin-roles-list-route.tsx +5 -2
- package/src/routes/create-admin-user-edit-route.tsx +5 -3
- package/src/routes/create-admin-users-list-route.tsx +4 -2
- package/src/routes/create-collection-api-route.tsx +5 -3
- package/src/routes/create-collection-create-route.tsx +7 -2
- package/src/routes/create-collection-edit-route.tsx +4 -2
- package/src/routes/create-collection-history-route.tsx +5 -3
- package/src/routes/create-collection-list-route.tsx +8 -10
- package/src/routes/create-sign-in-route.tsx +14 -1
- package/src/server-fns/auth/sign-in.ts +45 -0
- package/src/server-fns/i18n/get-active-locale.ts +26 -0
- package/src/server-fns/i18n/index.ts +11 -0
- package/src/server-fns/i18n/set-locale.ts +103 -0
|
@@ -0,0 +1,167 @@
|
|
|
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
|
+
* Tests for the preview-URL resolver's three-level cascade:
|
|
11
|
+
*
|
|
12
|
+
* 1. `adminConfig.preview.url` — wins outright
|
|
13
|
+
* 2. `definition.buildDocumentPath` — schema-side fallback
|
|
14
|
+
* 3. Generic `/${collectionPath}/${path}` — last-resort compose
|
|
15
|
+
*
|
|
16
|
+
* Also covers the branch-A posture (hook throws → fall through) and the
|
|
17
|
+
* "missing path means hide affordance" contract.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import {
|
|
21
|
+
type CollectionAdminConfig,
|
|
22
|
+
type CollectionDefinition,
|
|
23
|
+
defineServerConfig,
|
|
24
|
+
type PreviewDocument,
|
|
25
|
+
} from '@byline/core'
|
|
26
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
|
27
|
+
|
|
28
|
+
import { resolvePreviewUrl } from './resolve-preview-url.js'
|
|
29
|
+
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// Harness
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
function clearConfig(): void {
|
|
35
|
+
;(globalThis as any)[Symbol.for('__byline_server_config__')] = null
|
|
36
|
+
;(globalThis as any)[Symbol.for('__byline_client_config__')] = null
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function registerCollection(definition: Partial<CollectionDefinition> & { path: string }): void {
|
|
40
|
+
defineServerConfig({
|
|
41
|
+
collections: [
|
|
42
|
+
{
|
|
43
|
+
labels: { singular: 'Page', plural: 'Pages' },
|
|
44
|
+
fields: [{ name: 'title', type: 'text', label: 'Title' }],
|
|
45
|
+
useAsTitle: 'title',
|
|
46
|
+
...definition,
|
|
47
|
+
} as CollectionDefinition,
|
|
48
|
+
],
|
|
49
|
+
} as Parameters<typeof defineServerConfig>[0])
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const doc: PreviewDocument = {
|
|
53
|
+
id: 'doc-1',
|
|
54
|
+
path: 'about',
|
|
55
|
+
status: 'published',
|
|
56
|
+
fields: { title: 'About' },
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
// Suite
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
|
|
63
|
+
describe('resolvePreviewUrl', () => {
|
|
64
|
+
beforeEach(clearConfig)
|
|
65
|
+
afterEach(clearConfig)
|
|
66
|
+
|
|
67
|
+
// -------------------------------------------------------------------------
|
|
68
|
+
// Tier 1 — adminConfig.preview.url
|
|
69
|
+
// -------------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
describe('adminConfig.preview.url', () => {
|
|
72
|
+
it('takes precedence over the schema hook', () => {
|
|
73
|
+
registerCollection({
|
|
74
|
+
path: 'pages',
|
|
75
|
+
buildDocumentPath: () => '/schema-path',
|
|
76
|
+
})
|
|
77
|
+
const adminConfig = {
|
|
78
|
+
preview: { url: () => '/from-admin' },
|
|
79
|
+
} as unknown as CollectionAdminConfig
|
|
80
|
+
expect(resolvePreviewUrl(doc, 'pages', adminConfig, undefined)).toBe('/from-admin')
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('forwards the locale to the configured url builder', () => {
|
|
84
|
+
const calls: Array<{ locale: string | undefined }> = []
|
|
85
|
+
const adminConfig = {
|
|
86
|
+
preview: {
|
|
87
|
+
url: (_d: PreviewDocument, ctx: { locale?: string }) => {
|
|
88
|
+
calls.push({ locale: ctx.locale })
|
|
89
|
+
return '/anything'
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
} as unknown as CollectionAdminConfig
|
|
93
|
+
resolvePreviewUrl(doc, 'pages', adminConfig, 'fr')
|
|
94
|
+
expect(calls).toEqual([{ locale: 'fr' }])
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('returns null when the configured url builder returns null', () => {
|
|
98
|
+
const adminConfig = {
|
|
99
|
+
preview: { url: () => null },
|
|
100
|
+
} as unknown as CollectionAdminConfig
|
|
101
|
+
expect(resolvePreviewUrl(doc, 'pages', adminConfig, undefined)).toBeNull()
|
|
102
|
+
})
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
// -------------------------------------------------------------------------
|
|
106
|
+
// Tier 2 — schema-side buildDocumentPath
|
|
107
|
+
// -------------------------------------------------------------------------
|
|
108
|
+
|
|
109
|
+
describe('buildDocumentPath fallback', () => {
|
|
110
|
+
it('uses the schema hook when no adminConfig.preview is set', () => {
|
|
111
|
+
registerCollection({
|
|
112
|
+
path: 'pages',
|
|
113
|
+
buildDocumentPath: (d) => `/marketing/${d.path}`,
|
|
114
|
+
})
|
|
115
|
+
expect(resolvePreviewUrl(doc, 'pages', undefined, undefined)).toBe('/marketing/about')
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it('passes through null to hide the preview affordance', () => {
|
|
119
|
+
registerCollection({
|
|
120
|
+
path: 'pages',
|
|
121
|
+
buildDocumentPath: () => null,
|
|
122
|
+
})
|
|
123
|
+
expect(resolvePreviewUrl(doc, 'pages', undefined, undefined)).toBeNull()
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it('falls through to generic compose when the hook throws (branch-A posture)', () => {
|
|
127
|
+
registerCollection({
|
|
128
|
+
path: 'pages',
|
|
129
|
+
buildDocumentPath: () => {
|
|
130
|
+
throw new Error('boom')
|
|
131
|
+
},
|
|
132
|
+
})
|
|
133
|
+
expect(resolvePreviewUrl(doc, 'pages', undefined, undefined)).toBe('/pages/about')
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
it('falls through to generic compose when the hook returns a non-string non-null', () => {
|
|
137
|
+
registerCollection({
|
|
138
|
+
path: 'pages',
|
|
139
|
+
// @ts-expect-error — exercising defensive handling of malformed return values
|
|
140
|
+
buildDocumentPath: () => 42,
|
|
141
|
+
})
|
|
142
|
+
expect(resolvePreviewUrl(doc, 'pages', undefined, undefined)).toBe('/pages/about')
|
|
143
|
+
})
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
// -------------------------------------------------------------------------
|
|
147
|
+
// Tier 3 — generic compose
|
|
148
|
+
// -------------------------------------------------------------------------
|
|
149
|
+
|
|
150
|
+
describe('generic compose', () => {
|
|
151
|
+
it('uses /${collectionPath}/${path} when no schema hook is defined', () => {
|
|
152
|
+
registerCollection({ path: 'pages' })
|
|
153
|
+
expect(resolvePreviewUrl(doc, 'pages', undefined, undefined)).toBe('/pages/about')
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
it('works without any registered collection (fully unconfigured host)', () => {
|
|
157
|
+
// No registerCollection — getCollectionDefinition returns null,
|
|
158
|
+
// the cascade falls straight through to the generic compose.
|
|
159
|
+
expect(resolvePreviewUrl(doc, 'pages', undefined, undefined)).toBe('/pages/about')
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
it('returns null when the doc has no path yet', () => {
|
|
163
|
+
registerCollection({ path: 'pages' })
|
|
164
|
+
expect(resolvePreviewUrl({ ...doc, path: '' }, 'pages', undefined, undefined)).toBeNull()
|
|
165
|
+
})
|
|
166
|
+
})
|
|
167
|
+
})
|
|
@@ -0,0 +1,67 @@
|
|
|
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
|
+
* Pure (no React, no DOM) resolver for the admin Preview button's URL.
|
|
11
|
+
* Lives in its own module so it can be unit-tested without dragging the
|
|
12
|
+
* React UI graph of `preview-link.tsx` into a node-mode test run.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
type CollectionAdminConfig,
|
|
17
|
+
getCollectionDefinition,
|
|
18
|
+
type PreviewDocument,
|
|
19
|
+
} from '@byline/core'
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Resolve the preview URL for a doc against an admin config. Exported so
|
|
23
|
+
* other surfaces (list-row preview links in the future) can share the
|
|
24
|
+
* same fallback logic.
|
|
25
|
+
*
|
|
26
|
+
* Cascade:
|
|
27
|
+
* 1. `adminConfig.preview.url(doc, { locale })` — wins outright when
|
|
28
|
+
* configured. The host can do anything (locale prefix, query
|
|
29
|
+
* string, conditional return-null).
|
|
30
|
+
* 2. `CollectionDefinition.buildDocumentPath(doc, { collectionPath })`
|
|
31
|
+
* — the schema-side hook the richtext embed walker also reads.
|
|
32
|
+
* Locale-agnostic by contract; hosts that need a locale prefix
|
|
33
|
+
* keep their own `preview.url`.
|
|
34
|
+
* 3. Generic compose `/${collectionPath}/${doc.path}` — last-resort
|
|
35
|
+
* convention for collections with no schema-side hook.
|
|
36
|
+
*
|
|
37
|
+
* Returns:
|
|
38
|
+
* - `string` → URL to open
|
|
39
|
+
* - `null` → no preview URL meaningful for this doc; hide affordance
|
|
40
|
+
*/
|
|
41
|
+
export function resolvePreviewUrl(
|
|
42
|
+
doc: PreviewDocument,
|
|
43
|
+
collectionPath: string,
|
|
44
|
+
adminConfig: CollectionAdminConfig | undefined,
|
|
45
|
+
locale: string | undefined
|
|
46
|
+
): string | null {
|
|
47
|
+
if (adminConfig?.preview) {
|
|
48
|
+
return adminConfig.preview.url(doc, { locale })
|
|
49
|
+
}
|
|
50
|
+
// Schema-side default — same hook the richtext embed walker reads, so
|
|
51
|
+
// the public path and the admin Preview button agree by construction.
|
|
52
|
+
const definition = getCollectionDefinition(collectionPath)
|
|
53
|
+
if (definition?.buildDocumentPath != null) {
|
|
54
|
+
try {
|
|
55
|
+
const built = definition.buildDocumentPath(doc, { collectionPath })
|
|
56
|
+
if (typeof built === 'string') return built
|
|
57
|
+
if (built === null) return null
|
|
58
|
+
} catch {
|
|
59
|
+
// Fall through to generic compose — matches the embed walker's
|
|
60
|
+
// branch-A posture (don't take the render path down with the hook).
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
// Generic compose: collection lives at `/${collectionPath}/${path}`.
|
|
64
|
+
// Returns null when the doc has no path yet (unsaved, awaiting slug).
|
|
65
|
+
if (!doc.path) return null
|
|
66
|
+
return `/${collectionPath}/${doc.path}`
|
|
67
|
+
}
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
import { useState } from 'react'
|
|
21
21
|
import { useRouter } from '@tanstack/react-router'
|
|
22
22
|
|
|
23
|
+
import { useTranslation } from '@byline/i18n/react'
|
|
23
24
|
import { Alert, Button, LoaderEllipsis, Modal } from '@byline/ui/react'
|
|
24
25
|
import cx from 'classnames'
|
|
25
26
|
|
|
@@ -46,6 +47,7 @@ export function RestoreVersionModal({
|
|
|
46
47
|
}: RestoreVersionModalProps) {
|
|
47
48
|
const navigate = useNavigate()
|
|
48
49
|
const router = useRouter()
|
|
50
|
+
const { t } = useTranslation('byline-admin')
|
|
49
51
|
const [error, setError] = useState<string | null>(null)
|
|
50
52
|
const [pending, setPending] = useState(false)
|
|
51
53
|
|
|
@@ -66,13 +68,13 @@ export function RestoreVersionModal({
|
|
|
66
68
|
} catch (err) {
|
|
67
69
|
const code = getErrorCode(err)
|
|
68
70
|
if (code === 'ERR_INVALID_TRANSITION') {
|
|
69
|
-
setError('
|
|
71
|
+
setError(t('collections.restore.errors.alreadyCurrent'))
|
|
70
72
|
} else if (code === 'ERR_NOT_FOUND') {
|
|
71
|
-
setError('
|
|
73
|
+
setError(t('collections.restore.errors.notFound'))
|
|
72
74
|
} else if (code === 'ERR_FORBIDDEN' || code === 'ERR_UNAUTHENTICATED') {
|
|
73
|
-
setError('
|
|
75
|
+
setError(t('collections.restore.errors.forbidden'))
|
|
74
76
|
} else {
|
|
75
|
-
setError('
|
|
77
|
+
setError(t('collections.restore.errors.fallback'))
|
|
76
78
|
}
|
|
77
79
|
setPending(false)
|
|
78
80
|
}
|
|
@@ -87,16 +89,13 @@ export function RestoreVersionModal({
|
|
|
87
89
|
</Alert>
|
|
88
90
|
) : null}
|
|
89
91
|
<p className={cx('byline-coll-restore-row', styles.row)}>
|
|
90
|
-
<span className="muted">
|
|
92
|
+
<span className="muted">{t('collections.restore.versionLabel')}</span> {versionNumber}
|
|
91
93
|
</p>
|
|
92
94
|
<p className={cx('byline-coll-restore-row', styles.row)}>
|
|
93
|
-
<span className="muted">
|
|
95
|
+
<span className="muted">{t('collections.restore.createdLabel')}</span> {versionLabel}
|
|
94
96
|
</p>
|
|
95
97
|
<p className={cx('byline-coll-restore-warning', styles.warning)}>
|
|
96
|
-
|
|
97
|
-
{versionNumber}, and that draft will become the current version. The existing versions
|
|
98
|
-
(including any published version) are preserved in history. The restored draft will need
|
|
99
|
-
to be published through the normal workflow.
|
|
98
|
+
{t('collections.restore.warning', { version: versionNumber })}
|
|
100
99
|
</p>
|
|
101
100
|
</div>
|
|
102
101
|
<div className={cx('byline-coll-restore-actions', styles.actions)}>
|
|
@@ -117,7 +116,7 @@ export function RestoreVersionModal({
|
|
|
117
116
|
disabled={pending}
|
|
118
117
|
className={cx('byline-coll-restore-button', styles.button)}
|
|
119
118
|
>
|
|
120
|
-
|
|
119
|
+
{t('common.actions.cancel')}
|
|
121
120
|
</Button>
|
|
122
121
|
<Button
|
|
123
122
|
size="sm"
|
|
@@ -127,7 +126,7 @@ export function RestoreVersionModal({
|
|
|
127
126
|
disabled={pending}
|
|
128
127
|
className={cx('byline-coll-restore-button', styles.button)}
|
|
129
128
|
>
|
|
130
|
-
{pending === true ? <LoaderEllipsis size={42} /> : '
|
|
129
|
+
{pending === true ? <LoaderEllipsis size={42} /> : t('collections.restore.confirmButton')}
|
|
131
130
|
</Button>
|
|
132
131
|
</div>
|
|
133
132
|
</Modal.Content>
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
import { useCallback } from 'react'
|
|
17
17
|
import { useBlocker } from '@tanstack/react-router'
|
|
18
18
|
|
|
19
|
-
import type { NavigationGuardResult, UseNavigationGuard } from '@byline/
|
|
19
|
+
import type { NavigationGuardResult, UseNavigationGuard } from '@byline/admin/react'
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
22
|
* Navigation guard backed by TanStack Router's `useBlocker`.
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import { useEffect } from 'react'
|
|
10
10
|
|
|
11
11
|
import type { CollectionAdminConfig, PreviewDocument } from '@byline/core'
|
|
12
|
+
import { useTranslation } from '@byline/i18n/react'
|
|
12
13
|
import { Button, HistoryIcon, IconButton, Label, Select } from '@byline/ui/react'
|
|
13
14
|
import cx from 'classnames'
|
|
14
15
|
|
|
@@ -78,6 +79,7 @@ export const ViewMenu = ({
|
|
|
78
79
|
doc?: PreviewDocument
|
|
79
80
|
}) => {
|
|
80
81
|
const navigate = useNavigate()
|
|
82
|
+
const { t } = useTranslation('byline-admin')
|
|
81
83
|
|
|
82
84
|
// Edit view must never use 'all' locale — strip it from the URL and fall
|
|
83
85
|
// back to the default locale if it somehow arrives via navigation.
|
|
@@ -142,7 +144,7 @@ export const ViewMenu = ({
|
|
|
142
144
|
className={cx('muted byline-view-menu-label', styles.label)}
|
|
143
145
|
id="contentLocaleLabel"
|
|
144
146
|
htmlFor="contentLocale"
|
|
145
|
-
label=
|
|
147
|
+
label={t('collections.viewMenu.contentLocaleLabel')}
|
|
146
148
|
/>
|
|
147
149
|
<Select<string>
|
|
148
150
|
name="contentLocale"
|
|
@@ -152,7 +154,9 @@ export const ViewMenu = ({
|
|
|
152
154
|
variant="outlined"
|
|
153
155
|
value={locale ?? defaultContentLocale}
|
|
154
156
|
items={[
|
|
155
|
-
...(activeView !== 'edit'
|
|
157
|
+
...(activeView !== 'edit'
|
|
158
|
+
? [{ value: 'all', label: t('collections.viewMenu.localeAll') }]
|
|
159
|
+
: []),
|
|
156
160
|
...contentLocales.map((loc) => ({ value: loc.code, label: loc.label })),
|
|
157
161
|
]}
|
|
158
162
|
onValueChange={handleLocaleChange}
|
|
@@ -163,7 +167,7 @@ export const ViewMenu = ({
|
|
|
163
167
|
className={cx('muted byline-view-menu-label', styles.label)}
|
|
164
168
|
id="populateDepthLabel"
|
|
165
169
|
htmlFor="populateDepth"
|
|
166
|
-
label=
|
|
170
|
+
label={t('collections.viewMenu.depthLabel')}
|
|
167
171
|
/>
|
|
168
172
|
<Select<string>
|
|
169
173
|
name="populateDepth"
|
|
@@ -217,7 +221,7 @@ export const ViewMenu = ({
|
|
|
217
221
|
})
|
|
218
222
|
}
|
|
219
223
|
>
|
|
220
|
-
|
|
224
|
+
{t('common.actions.edit')}
|
|
221
225
|
</Button>
|
|
222
226
|
<Button
|
|
223
227
|
size="xs"
|
|
@@ -231,7 +235,7 @@ export const ViewMenu = ({
|
|
|
231
235
|
})
|
|
232
236
|
}
|
|
233
237
|
>
|
|
234
|
-
|
|
238
|
+
{t('collections.viewMenu.apiButton')}
|
|
235
239
|
</Button>
|
|
236
240
|
</div>
|
|
237
241
|
)
|
|
@@ -0,0 +1,26 @@
|
|
|
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
|
+
* `@byline/host-tanstack-start/i18n` — host-side glue between the
|
|
11
|
+
* admin interface translation registry and TanStack Start's request
|
|
12
|
+
* model. Cookie helpers, the per-request locale resolver, and the
|
|
13
|
+
* server-side `resolveServerTranslator` companion to the client
|
|
14
|
+
* `useTranslation` hook.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
export {
|
|
18
|
+
ADMIN_LOCALE_COOKIE,
|
|
19
|
+
clearAdminLocaleCookie,
|
|
20
|
+
readAdminLocaleCookie,
|
|
21
|
+
setAdminLocaleCookie,
|
|
22
|
+
} from './locale-cookie.js'
|
|
23
|
+
export { buildLocaleDefinitions } from './locale-definitions.js'
|
|
24
|
+
export { resolveRequestLocale } from './resolve-locale.js'
|
|
25
|
+
export { resolveServerTranslator } from './server-translator.js'
|
|
26
|
+
export type { ServerTranslator } from './server-translator.js'
|
|
@@ -0,0 +1,68 @@
|
|
|
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
|
+
* Cookie helpers for the admin interface locale preference.
|
|
11
|
+
*
|
|
12
|
+
* The cookie name is `byline_admin_lng` — follows the `byline_*` prefix
|
|
13
|
+
* convention shared with the other admin cookies (`byline_access_token`,
|
|
14
|
+
* `byline_refresh_token`, `byline_preview`). Deliberately distinct from
|
|
15
|
+
* any host-side front-end-site i18n cookie (the example webapp uses
|
|
16
|
+
* `lng` for its public-site switcher). The two systems are independent:
|
|
17
|
+
* an editor in a Spanish-language admin chrome routinely edits English /
|
|
18
|
+
* French / German content, and the host's public site may carry its own
|
|
19
|
+
* locale switcher in its own cookie. Sharing one cookie across both
|
|
20
|
+
* would cause cross-talk.
|
|
21
|
+
*
|
|
22
|
+
* Cookie attributes:
|
|
23
|
+
* - `httpOnly: false` — the client-side `<LanguageMenu>` reads the
|
|
24
|
+
* cookie to render the active row indicator; setting it httpOnly
|
|
25
|
+
* would force a server roundtrip on every page load just to know
|
|
26
|
+
* which locale the user chose.
|
|
27
|
+
* - `sameSite: 'lax'`
|
|
28
|
+
* - `secure: true` in production — https-only.
|
|
29
|
+
* - `path: '/'`
|
|
30
|
+
* - `maxAge: 365 days` — language preference is a long-lived choice.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
import { getCookie, setCookie } from '@tanstack/react-start/server'
|
|
34
|
+
|
|
35
|
+
export const ADMIN_LOCALE_COOKIE = 'byline_admin_lng'
|
|
36
|
+
const ADMIN_LOCALE_MAX_AGE_SECONDS = 60 * 60 * 24 * 365 // 365 days
|
|
37
|
+
|
|
38
|
+
const IS_PROD = process.env.NODE_ENV === 'production'
|
|
39
|
+
|
|
40
|
+
/** Read the current admin locale cookie, if any. */
|
|
41
|
+
export function readAdminLocaleCookie(): string | null {
|
|
42
|
+
return getCookie(ADMIN_LOCALE_COOKIE) ?? null
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Write the admin locale cookie. Caller is expected to have validated
|
|
47
|
+
* `locale` against the permitted `i18n.interface.locales` set first.
|
|
48
|
+
*/
|
|
49
|
+
export function setAdminLocaleCookie(locale: string): void {
|
|
50
|
+
setCookie(ADMIN_LOCALE_COOKIE, locale, {
|
|
51
|
+
httpOnly: false,
|
|
52
|
+
sameSite: 'lax',
|
|
53
|
+
secure: IS_PROD,
|
|
54
|
+
path: '/',
|
|
55
|
+
maxAge: ADMIN_LOCALE_MAX_AGE_SECONDS,
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Clear the cookie — used when a user opts back into browser-default detection. */
|
|
60
|
+
export function clearAdminLocaleCookie(): void {
|
|
61
|
+
setCookie(ADMIN_LOCALE_COOKIE, '', {
|
|
62
|
+
httpOnly: false,
|
|
63
|
+
sameSite: 'lax',
|
|
64
|
+
secure: IS_PROD,
|
|
65
|
+
path: '/',
|
|
66
|
+
maxAge: 0,
|
|
67
|
+
})
|
|
68
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
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 { LocaleDefinition } from '@byline/i18n'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Build a `LocaleDefinition[]` for the language switcher. Per-code
|
|
13
|
+
* resolution order:
|
|
14
|
+
*
|
|
15
|
+
* 1. An entry from the host's `i18n.interface.localeDefinitions`
|
|
16
|
+
* (matched by code). Wins outright — this is the path that lets a
|
|
17
|
+
* host author write `Français` instead of the lowercase
|
|
18
|
+
* `français` that CLDR's `Intl.DisplayNames` returns for romance
|
|
19
|
+
* languages.
|
|
20
|
+
* 2. `Intl.DisplayNames(code).of(code)` — produces a display name in
|
|
21
|
+
* each locale's own language using CLDR's data.
|
|
22
|
+
* 3. The raw code, as a last-resort fallback for exotic tags or
|
|
23
|
+
* runtimes that lack `Intl.DisplayNames`.
|
|
24
|
+
*
|
|
25
|
+
* Used by both the admin layout (post-auth) and the sign-in page
|
|
26
|
+
* (pre-auth) — anywhere `<LanguageMenu>` mounts.
|
|
27
|
+
*/
|
|
28
|
+
export function buildLocaleDefinitions(
|
|
29
|
+
codes: readonly string[],
|
|
30
|
+
configured: ReadonlyArray<{ code: string; nativeName: string }> | undefined
|
|
31
|
+
): LocaleDefinition[] {
|
|
32
|
+
const explicit = new Map((configured ?? []).map((d) => [d.code, d.nativeName]))
|
|
33
|
+
return codes.map((code) => {
|
|
34
|
+
const explicitName = explicit.get(code)
|
|
35
|
+
if (explicitName != null) {
|
|
36
|
+
return { code, nativeName: explicitName }
|
|
37
|
+
}
|
|
38
|
+
let nativeName = code
|
|
39
|
+
try {
|
|
40
|
+
const dn = new Intl.DisplayNames([code], { type: 'language' })
|
|
41
|
+
nativeName = dn.of(code) ?? code
|
|
42
|
+
} catch {
|
|
43
|
+
// Intl.DisplayNames is available in Node 18+ and every modern
|
|
44
|
+
// browser. Defensive catch covers exotic codes or sandbox quirks.
|
|
45
|
+
}
|
|
46
|
+
return { code, nativeName }
|
|
47
|
+
})
|
|
48
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
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
|
+
* Server-side locale resolution for the admin interface.
|
|
11
|
+
*
|
|
12
|
+
* Combines `@byline/i18n`'s pure `resolveInterfaceLocale` cascade with
|
|
13
|
+
* the host's request-scoped signals — the `byline_admin_lng` cookie, the
|
|
14
|
+
* `Accept-Language` header, and (when an admin session is present)
|
|
15
|
+
* `admin_users.preferred_locale` on the authenticated actor.
|
|
16
|
+
*
|
|
17
|
+
* Idempotent: calling this twice in one request produces the same
|
|
18
|
+
* answer. SSR and the hydrated client therefore resolve to the same
|
|
19
|
+
* locale by construction — no "locale flicker" between server render
|
|
20
|
+
* and client takeover.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { getRequestHeader } from '@tanstack/react-start/server'
|
|
24
|
+
|
|
25
|
+
import { AuthError, AuthErrorCodes } from '@byline/auth'
|
|
26
|
+
import type { LocaleCode } from '@byline/i18n'
|
|
27
|
+
import { resolveInterfaceLocale } from '@byline/i18n'
|
|
28
|
+
|
|
29
|
+
import { getAdminRequestContext } from '../auth/auth-context.js'
|
|
30
|
+
import { bylineCore } from '../integrations/byline-core.js'
|
|
31
|
+
import { readAdminLocaleCookie } from './locale-cookie.js'
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Resolve the locale for the current request. Skips the
|
|
35
|
+
* `admin_users.preferred_locale` tier when no admin session is present —
|
|
36
|
+
* this is the right behaviour for pre-auth surfaces (sign-in page).
|
|
37
|
+
*
|
|
38
|
+
* Bypasses the auth resolver when `skipActorLookup` is true. Useful for
|
|
39
|
+
* very early-boot surfaces where the actor lookup itself is expensive
|
|
40
|
+
* or unwanted (rare; default to letting the cascade do its work).
|
|
41
|
+
*/
|
|
42
|
+
export async function resolveRequestLocale(options?: {
|
|
43
|
+
skipActorLookup?: boolean
|
|
44
|
+
}): Promise<LocaleCode> {
|
|
45
|
+
const core = bylineCore()
|
|
46
|
+
const { interface: ifaceConfig } = core.config.i18n
|
|
47
|
+
|
|
48
|
+
let preferred: string | null = null
|
|
49
|
+
if (!options?.skipActorLookup) {
|
|
50
|
+
preferred = await readPreferredLocaleFromActor()
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return resolveInterfaceLocale({
|
|
54
|
+
locales: ifaceConfig.locales,
|
|
55
|
+
defaultLocale: ifaceConfig.defaultLocale,
|
|
56
|
+
preferred,
|
|
57
|
+
cookie: readAdminLocaleCookie(),
|
|
58
|
+
acceptLanguage: readAcceptLanguage(),
|
|
59
|
+
})
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function readPreferredLocaleFromActor(): Promise<string | null> {
|
|
63
|
+
try {
|
|
64
|
+
const context = await getAdminRequestContext()
|
|
65
|
+
// The actor type isn't structurally typed for `preferred_locale`
|
|
66
|
+
// here — read it through the admin store to keep the resolver
|
|
67
|
+
// resilient to actor-shape evolution.
|
|
68
|
+
const adminStore = bylineCore().adminStore
|
|
69
|
+
if (adminStore == null) return null
|
|
70
|
+
const actor = context.actor
|
|
71
|
+
// Best-effort: the AdminAuth actor carries `id`. Reading the row
|
|
72
|
+
// directly from the repo costs one query per request that resolves
|
|
73
|
+
// a locale, which is fine for SSR; high-traffic paths can opt out
|
|
74
|
+
// via `skipActorLookup`.
|
|
75
|
+
if (actor == null || typeof actor !== 'object' || !('id' in actor)) return null
|
|
76
|
+
const id = (actor as { id: unknown }).id
|
|
77
|
+
if (typeof id !== 'string') return null
|
|
78
|
+
const row = await adminStore.adminUsers.getById(id)
|
|
79
|
+
return row?.preferred_locale ?? null
|
|
80
|
+
} catch (err) {
|
|
81
|
+
if (err instanceof AuthError && err.code === AuthErrorCodes.UNAUTHENTICATED) {
|
|
82
|
+
return null
|
|
83
|
+
}
|
|
84
|
+
// Any other failure (transport, refresh) → fall through to the
|
|
85
|
+
// cookie / Accept-Language tiers rather than crashing the SSR.
|
|
86
|
+
return null
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function readAcceptLanguage(): string | null {
|
|
91
|
+
try {
|
|
92
|
+
return getRequestHeader('accept-language') ?? null
|
|
93
|
+
} catch {
|
|
94
|
+
return null
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
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
|
+
* `resolveServerTranslator(namespace)` — server-side companion to the
|
|
11
|
+
* client `useTranslation(namespace)` hook.
|
|
12
|
+
*
|
|
13
|
+
* Resolves the request locale via the same cascade the client hydrates
|
|
14
|
+
* against (preferred → cookie → Accept-Language → default), constructs
|
|
15
|
+
* a formatter bound to the registered translation bundle, and returns
|
|
16
|
+
* a `{ t, locale }` for the requested namespace. The shape mirrors
|
|
17
|
+
* `useTranslation` so callers can swap surfaces (loader vs component)
|
|
18
|
+
* without rethinking the API.
|
|
19
|
+
*
|
|
20
|
+
* Typical use:
|
|
21
|
+
*
|
|
22
|
+
* import { resolveServerTranslator } from '@byline/host-tanstack-start/i18n'
|
|
23
|
+
*
|
|
24
|
+
* export const sendInviteFn = createServerFn(...).handler(async () => {
|
|
25
|
+
* const { t } = await resolveServerTranslator('byline-admin')
|
|
26
|
+
* return { subject: t('email.invite.subject') }
|
|
27
|
+
* })
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import { createFormatter, type LocaleCode } from '@byline/i18n'
|
|
31
|
+
|
|
32
|
+
import { bylineCore } from '../integrations/byline-core.js'
|
|
33
|
+
import { resolveRequestLocale } from './resolve-locale.js'
|
|
34
|
+
|
|
35
|
+
export interface ServerTranslator {
|
|
36
|
+
t: (key: string, values?: Record<string, string | number | boolean | Date | null>) => string
|
|
37
|
+
locale: LocaleCode
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function resolveServerTranslator(namespace: string): Promise<ServerTranslator> {
|
|
41
|
+
const core = bylineCore()
|
|
42
|
+
const { interface: ifaceConfig, translations: bundle } = core.config.i18n
|
|
43
|
+
if (bundle == null) {
|
|
44
|
+
throw new Error(
|
|
45
|
+
'[resolveServerTranslator] no translation bundle is registered. ' +
|
|
46
|
+
'Pass `translations: adminTranslations({ en: true })` (or a merged bundle) ' +
|
|
47
|
+
'on `i18n` in your config.'
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
const activeLocale = await resolveRequestLocale()
|
|
51
|
+
const formatter = createFormatter({
|
|
52
|
+
bundle,
|
|
53
|
+
activeLocale,
|
|
54
|
+
defaultLocale: ifaceConfig.defaultLocale,
|
|
55
|
+
})
|
|
56
|
+
return {
|
|
57
|
+
t: (key, values) => formatter.t(namespace, key, values),
|
|
58
|
+
locale: activeLocale,
|
|
59
|
+
}
|
|
60
|
+
}
|