@byline/admin 4.11.2 → 4.12.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/fields/blocks/blocks-field.js +2 -2
- package/dist/fields/field-renderer.d.ts +1 -1
- package/dist/fields/field-renderer.js +2 -2
- package/dist/fields/group/group-field.d.ts +1 -1
- package/dist/fields/relation/relation-picker.d.ts +1 -1
- package/dist/fields/relation/relation-picker.js +2 -2
- package/dist/fields/relation/relation-summary.js +1 -2
- package/dist/forms/form-renderer.js +2 -2
- package/dist/modules/admin-account/components/preferences.js +6 -6
- package/dist/modules/admin-users/repository.d.ts +1 -1
- package/dist/modules/auth/components/sign-in-form.d.ts +3 -5
- package/dist/modules/auth/components/sign-in-form.js +3 -3
- package/dist/modules/auth/safe-redirect.d.ts +2 -2
- package/dist/modules/auth/safe-redirect.js +2 -2
- package/dist/services/admin-services-types.d.ts +3 -3
- package/package.json +5 -5
- package/src/fields/blocks/blocks-field.tsx +4 -4
- package/src/fields/field-renderer.tsx +4 -4
- package/src/fields/group/group-field.tsx +1 -1
- package/src/fields/relation/relation-picker.tsx +3 -3
- package/src/fields/relation/relation-summary.tsx +1 -2
- package/src/forms/form-renderer.tsx +2 -2
- package/src/forms/path-widget.tsx +1 -1
- package/src/modules/admin-account/components/preferences.tsx +8 -8
- package/src/modules/admin-users/repository.ts +1 -1
- package/src/modules/auth/components/sign-in-form.tsx +5 -11
- package/src/modules/auth/safe-redirect.test.node.ts +5 -10
- package/src/modules/auth/safe-redirect.ts +2 -5
- package/src/modules/auth/sign-in-form-props.test.node.ts +1 -5
- package/src/services/admin-services-types.ts +3 -3
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect, useMemo, useState } from "react";
|
|
3
|
-
import {
|
|
3
|
+
import { getAdminConfig } from "@byline/core";
|
|
4
4
|
import { useTranslation } from "@byline/i18n/react";
|
|
5
5
|
import { Card, CloseIcon, DraggableSortable, IconButton, Modal, PlusIcon, moveItem } from "@byline/ui/react";
|
|
6
6
|
import clsx from "clsx";
|
|
@@ -22,7 +22,7 @@ const BlocksField = ({ field, defaultValue, path, contentLocale })=>{
|
|
|
22
22
|
]);
|
|
23
23
|
const blockAdminByType = useMemo(()=>{
|
|
24
24
|
const map = new Map();
|
|
25
|
-
for (const entry of
|
|
25
|
+
for (const entry of getAdminConfig().blockAdmin ?? [])map.set(entry.blockType, entry);
|
|
26
26
|
return map;
|
|
27
27
|
}, []);
|
|
28
28
|
const [selectedBlockName, setSelectedBlockName] = useState(()=>availableBlocks[0]?.blockType ?? '');
|
|
@@ -26,7 +26,7 @@ interface FieldRendererProps {
|
|
|
26
26
|
/**
|
|
27
27
|
* Per-field rich-text editor component override from the admin config.
|
|
28
28
|
* Takes precedence over the globally registered
|
|
29
|
-
* `
|
|
29
|
+
* `AdminConfig.fields.richText.editor` for this single field.
|
|
30
30
|
* Ignored when `field.type !== 'richText'`.
|
|
31
31
|
*/
|
|
32
32
|
editor?: RichTextEditorComponent;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
2
|
+
import { getAdminConfig } from "@byline/core";
|
|
3
3
|
import clsx from "clsx";
|
|
4
4
|
import { useFormContext } from "../forms/form-context.js";
|
|
5
5
|
import { ArrayField } from "./array/array-field.js";
|
|
@@ -98,7 +98,7 @@ const FieldRenderer = ({ field, defaultValue: initialDefault, basePath, disableS
|
|
|
98
98
|
});
|
|
99
99
|
case 'richText':
|
|
100
100
|
{
|
|
101
|
-
const RichTextEditor = editor ??
|
|
101
|
+
const RichTextEditor = editor ?? getAdminConfig().fields?.richText?.editor;
|
|
102
102
|
if (!RichTextEditor) throw new Error("No richText editor registered. Install @byline/richtext-lexical and set `fields.richText.editor` in your admin config.");
|
|
103
103
|
return /*#__PURE__*/ jsx(RichTextEditor, {
|
|
104
104
|
field: hideLabel ? {
|
|
@@ -30,7 +30,7 @@ interface GroupFieldProps {
|
|
|
30
30
|
* Per-child-field admin overrides (`components` slots, richtext `editor`),
|
|
31
31
|
* keyed by dotted, index-free schema paths relative to this group
|
|
32
32
|
* ('caption', 'faq.answer'). Threaded by `BlocksField` from the site-wide
|
|
33
|
-
* `
|
|
33
|
+
* `AdminConfig.blockAdmin` registry (block children render through a
|
|
34
34
|
* synthesized group) and by `FieldRenderer` for plain schema groups, whose
|
|
35
35
|
* map arrives pre-sliced from the collection admin config. Exact-name
|
|
36
36
|
* entries apply to the child itself; deeper entries are re-sliced and
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import type { CollectionDefinition } from '@byline/core';
|
|
9
9
|
/**
|
|
10
10
|
* Row rendering strategy, in priority order:
|
|
11
|
-
* 1. `CollectionAdminConfig.
|
|
11
|
+
* 1. `CollectionAdminConfig.itemView` — a ColumnDefinition[] from the target
|
|
12
12
|
* admin config. Each row renders the declared columns side-by-side,
|
|
13
13
|
* reusing any column formatters (thumbnail, date, etc).
|
|
14
14
|
* 2. Explicit `displayField` prop on this component (forwarded from
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useCallback, useEffect, useState } from "react";
|
|
3
|
-
import { getCollectionAdminConfig
|
|
3
|
+
import { getCollectionAdminConfig } from "@byline/core";
|
|
4
4
|
import { useTranslation } from "@byline/i18n/react";
|
|
5
5
|
import { Button, CheckIcon, LoaderRing, Modal, Search } from "@byline/ui/react";
|
|
6
6
|
import clsx from "clsx";
|
|
@@ -21,7 +21,7 @@ const RelationPicker = ({ targetCollectionPath, targetDefinition, displayField,
|
|
|
21
21
|
const [collectionId, setCollectionId] = useState(null);
|
|
22
22
|
const { getCollectionDocuments } = useBylineFieldServices();
|
|
23
23
|
const targetAdminConfig = getCollectionAdminConfig(targetCollectionPath);
|
|
24
|
-
const pickerColumns =
|
|
24
|
+
const pickerColumns = targetAdminConfig?.itemView;
|
|
25
25
|
useEffect(()=>{
|
|
26
26
|
if (isOpen) {
|
|
27
27
|
setQuery('');
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { resolveItemViewColumns } from "@byline/core";
|
|
3
2
|
import clsx from "clsx";
|
|
4
3
|
import { PickerCell, resolveFallbackDisplayField, resolveRowLabel } from "./relation-display.js";
|
|
5
4
|
import relation_summary_module from "./relation-summary.module.js";
|
|
6
5
|
function RelationSummary({ targetDefinition, targetAdminConfig, displayField, value, cachedRecord }) {
|
|
7
|
-
const pickerColumns =
|
|
6
|
+
const pickerColumns = targetAdminConfig?.itemView;
|
|
8
7
|
if (false === value._resolved) return /*#__PURE__*/ jsxs("div", {
|
|
9
8
|
className: clsx('byline-relation-summary-stack', relation_summary_module.stack),
|
|
10
9
|
children: [
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
4
|
-
import {
|
|
4
|
+
import { getAdminConfig } from "@byline/core";
|
|
5
5
|
import { useTranslation } from "@byline/i18n/react";
|
|
6
6
|
import { Alert, Button, ComboButton } from "@byline/ui/react";
|
|
7
7
|
import clsx from "clsx";
|
|
@@ -34,7 +34,7 @@ const FormContent = ({ mode, fields, onSubmit, onCancel, onStatusChange, onUnpub
|
|
|
34
34
|
const [pendingSystemFieldsSubmit, setPendingSystemFieldsSubmit] = useState(null);
|
|
35
35
|
const [contentLocale, setContentLocale] = useState(initialLocale ?? defaultLocale);
|
|
36
36
|
const { uploadField } = useBylineFieldServices();
|
|
37
|
-
const pathSlugifier =
|
|
37
|
+
const pathSlugifier = getAdminConfig().slugifier;
|
|
38
38
|
const pathSourceLocked = useMemo(()=>{
|
|
39
39
|
if (!useAsPath) return false;
|
|
40
40
|
const source = fields.find((f)=>f.name === useAsPath);
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useMemo, useState } from "react";
|
|
4
4
|
import { revalidateLogic, useForm } from "@tanstack/react-form-start";
|
|
5
|
-
import {
|
|
5
|
+
import { getAdminConfig } from "@byline/core";
|
|
6
6
|
import { useTranslation } from "@byline/i18n/react";
|
|
7
7
|
import { Alert, Button, LoaderEllipsis, Select } from "@byline/ui/react";
|
|
8
8
|
import clsx from "clsx";
|
|
@@ -22,16 +22,16 @@ function defaultsFrom(account) {
|
|
|
22
22
|
}
|
|
23
23
|
function Preferences({ account, onClose, onSuccess }) {
|
|
24
24
|
const { t } = useTranslation('byline-admin');
|
|
25
|
-
const {
|
|
25
|
+
const { setAdminLocale } = useBylineAdminServices();
|
|
26
26
|
const [formError, setFormError] = useState(null);
|
|
27
27
|
const [successMessage, setSuccessMessage] = useState(null);
|
|
28
28
|
const localeItems = useMemo(()=>{
|
|
29
|
-
const { i18n } =
|
|
30
|
-
const definitionByCode = new Map((i18n.
|
|
29
|
+
const { i18n } = getAdminConfig();
|
|
30
|
+
const definitionByCode = new Map((i18n.admin.localeDefinitions ?? []).map((d)=>[
|
|
31
31
|
d.code,
|
|
32
32
|
d.nativeName
|
|
33
33
|
]));
|
|
34
|
-
const items = i18n.
|
|
34
|
+
const items = i18n.admin.locales.map((code)=>({
|
|
35
35
|
value: code,
|
|
36
36
|
label: definitionByCode.get(code) ?? code
|
|
37
37
|
}));
|
|
@@ -60,7 +60,7 @@ function Preferences({ account, onClose, onSuccess }) {
|
|
|
60
60
|
const nextLocale = value.locale === AUTO_VALUE ? null : value.locale;
|
|
61
61
|
if (nextLocale === (account.preferred_locale ?? null)) return void setSuccessMessage(t('common.feedback.noChanges'));
|
|
62
62
|
try {
|
|
63
|
-
const result = await
|
|
63
|
+
const result = await setAdminLocale({
|
|
64
64
|
data: {
|
|
65
65
|
locale: nextLocale
|
|
66
66
|
}
|
|
@@ -52,7 +52,7 @@ export interface AdminUserRow {
|
|
|
52
52
|
* Admin interface locale preference. `null` means "use the detection
|
|
53
53
|
* cascade" (cookie → Accept-Language → defaultLocale). Stored as a
|
|
54
54
|
* BCP 47 code; validated at the command layer against the host's
|
|
55
|
-
* `i18n.
|
|
55
|
+
* `i18n.admin.locales`.
|
|
56
56
|
*/
|
|
57
57
|
preferred_locale: string | null;
|
|
58
58
|
created_at: Date;
|
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
export interface SignInFormProps {
|
|
2
2
|
/** Host-validated root-relative destination after successful sign-in. */
|
|
3
3
|
redirectTo?: string;
|
|
4
|
-
/** @deprecated Use `redirectTo`. */
|
|
5
|
-
callbackUrl?: string;
|
|
6
4
|
/**
|
|
7
5
|
* Optional plain "Home" link rendered on the left of the action row.
|
|
8
|
-
* Typically the host's
|
|
9
|
-
* navigate back to the public site without typing the URL.
|
|
6
|
+
* Typically the host's client-safe canonical site URL so signed-out admins
|
|
7
|
+
* can navigate back to the public site without typing the URL.
|
|
10
8
|
*/
|
|
11
9
|
homeUrl?: string;
|
|
12
10
|
}
|
|
13
|
-
export declare function SignInForm({ redirectTo,
|
|
11
|
+
export declare function SignInForm({ redirectTo, homeUrl }: SignInFormProps): import("react").JSX.Element;
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useState } from "react";
|
|
4
|
-
import {
|
|
4
|
+
import { getAdminConfig } from "@byline/core";
|
|
5
5
|
import { useTranslation } from "@byline/i18n/react";
|
|
6
6
|
import { Alert, Button, Card, Input, LoaderEllipsis } from "@byline/ui/react";
|
|
7
7
|
import clsx from "clsx";
|
|
8
8
|
import { useBylineAdminServices } from "../../../services/admin-services-context.js";
|
|
9
9
|
import { normalizeRootRelativeRedirect, resolveSignInFormRedirect } from "../safe-redirect.js";
|
|
10
10
|
import sign_in_form_module from "./sign-in-form.module.js";
|
|
11
|
-
function SignInForm({ redirectTo,
|
|
11
|
+
function SignInForm({ redirectTo, homeUrl }) {
|
|
12
12
|
const { adminSignIn } = useBylineAdminServices();
|
|
13
13
|
const { t } = useTranslation('byline-admin');
|
|
14
14
|
const [email, setEmail] = useState('');
|
|
@@ -19,7 +19,7 @@ function SignInForm({ redirectTo, callbackUrl, homeUrl }) {
|
|
|
19
19
|
event.preventDefault();
|
|
20
20
|
if (pending) return;
|
|
21
21
|
if (0 === email.trim().length || 0 === password.length) return void setError(t('auth.signIn.errors.empty'));
|
|
22
|
-
const destination = resolveSignInFormRedirect(redirectTo,
|
|
22
|
+
const destination = resolveSignInFormRedirect(redirectTo, getAdminConfig().routes.admin);
|
|
23
23
|
setPending(true);
|
|
24
24
|
setError(null);
|
|
25
25
|
try {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
/** Accept only canonical, unencoded, root-relative same-origin redirect paths. */
|
|
2
2
|
export declare function normalizeRootRelativeRedirect(value: string): string | undefined;
|
|
3
|
-
/** Resolve the
|
|
4
|
-
export declare function resolveSignInFormRedirect(redirectTo: string | undefined,
|
|
3
|
+
/** Resolve the requested destination, then a trusted fallback. */
|
|
4
|
+
export declare function resolveSignInFormRedirect(redirectTo: string | undefined, fallback: string | (() => string)): string;
|
|
@@ -11,8 +11,8 @@ function safe_redirect_normalizeRootRelativeRedirect(value) {
|
|
|
11
11
|
if ('https://byline.invalid' !== url.origin) return;
|
|
12
12
|
return `${url.pathname}${url.search}${url.hash}`;
|
|
13
13
|
}
|
|
14
|
-
function resolveSignInFormRedirect(redirectTo,
|
|
15
|
-
const requested =
|
|
14
|
+
function resolveSignInFormRedirect(redirectTo, fallback) {
|
|
15
|
+
const requested = redirectTo ? safe_redirect_normalizeRootRelativeRedirect(redirectTo) : void 0;
|
|
16
16
|
if (requested) return requested;
|
|
17
17
|
const defaultPath = 'function' == typeof fallback ? fallback() : fallback;
|
|
18
18
|
return safe_redirect_normalizeRootRelativeRedirect(defaultPath) ?? '/';
|
|
@@ -50,7 +50,7 @@ export type SignInResult = unknown;
|
|
|
50
50
|
export type UpdateAccountInput = UpdateAccountRequest;
|
|
51
51
|
/** Same shape as `ChangeAccountPasswordRequest` from `@byline/admin/admin-account`. */
|
|
52
52
|
export type ChangeAccountPasswordInput = ChangeAccountPasswordRequest;
|
|
53
|
-
export interface
|
|
53
|
+
export interface SetAdminLocaleInput {
|
|
54
54
|
/** BCP 47 tag, or `null` to clear the preference and re-engage detection. */
|
|
55
55
|
locale: string | null;
|
|
56
56
|
}
|
|
@@ -60,7 +60,7 @@ export interface SetInterfaceLocaleInput {
|
|
|
60
60
|
* the account page); `null` on pre-auth surfaces (the sign-in page
|
|
61
61
|
* locale dropdown) where the cookie write is the only effect.
|
|
62
62
|
*/
|
|
63
|
-
export interface
|
|
63
|
+
export interface SetAdminLocaleResult {
|
|
64
64
|
ok: true;
|
|
65
65
|
locale: string | null;
|
|
66
66
|
account: AccountResponse | null;
|
|
@@ -121,7 +121,7 @@ export interface BylineAdminServices {
|
|
|
121
121
|
adminSignIn: AdminServiceCall<SignInInput, SignInResult>;
|
|
122
122
|
updateAccount: AdminServiceCall<UpdateAccountInput, AccountResponse>;
|
|
123
123
|
changeAccountPassword: AdminServiceCall<ChangeAccountPasswordInput, AccountResponse>;
|
|
124
|
-
|
|
124
|
+
setAdminLocale: AdminServiceCall<SetAdminLocaleInput, SetAdminLocaleResult>;
|
|
125
125
|
createAdminUser: AdminServiceCall<CreateAdminUserInput, AdminUserResponse>;
|
|
126
126
|
updateAdminUser: AdminServiceCall<UpdateAdminUserInput, AdminUserResponse>;
|
|
127
127
|
setAdminUserPassword: AdminServiceCall<SetAdminUserPasswordInput, AdminUserResponse>;
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@byline/admin",
|
|
3
3
|
"private": false,
|
|
4
4
|
"license": "MPL-2.0",
|
|
5
|
-
"version": "4.
|
|
5
|
+
"version": "4.12.0",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": ">=20.9.0"
|
|
8
8
|
},
|
|
@@ -173,10 +173,10 @@
|
|
|
173
173
|
"react-diff-viewer-continued": "^4.4.0",
|
|
174
174
|
"uuid": "^14.0.1",
|
|
175
175
|
"zod": "^4.4.3",
|
|
176
|
-
"@byline/auth": "4.
|
|
177
|
-
"@byline/
|
|
178
|
-
"@byline/core": "4.
|
|
179
|
-
"@byline/
|
|
176
|
+
"@byline/auth": "4.12.0",
|
|
177
|
+
"@byline/ui": "4.12.0",
|
|
178
|
+
"@byline/core": "4.12.0",
|
|
179
|
+
"@byline/i18n": "4.12.0"
|
|
180
180
|
},
|
|
181
181
|
"peerDependencies": {
|
|
182
182
|
"react": "^19.0.0",
|
|
@@ -14,7 +14,7 @@ import type {
|
|
|
14
14
|
Field,
|
|
15
15
|
GroupField as GroupFieldType,
|
|
16
16
|
} from '@byline/core'
|
|
17
|
-
import {
|
|
17
|
+
import { getAdminConfig } from '@byline/core'
|
|
18
18
|
import { useTranslation } from '@byline/i18n/react'
|
|
19
19
|
import {
|
|
20
20
|
Card,
|
|
@@ -65,14 +65,14 @@ export const BlocksField = ({
|
|
|
65
65
|
|
|
66
66
|
const availableBlocks = useMemo(() => field.blocks ?? [], [field.blocks])
|
|
67
67
|
|
|
68
|
-
// Site-wide per-block admin config (`
|
|
68
|
+
// Site-wide per-block admin config (`AdminConfig.blockAdmin`), resolved by
|
|
69
69
|
// blockType. Registry-based on purpose: blocks are cross-collection units,
|
|
70
70
|
// so their admin config applies wherever the block renders — no threading
|
|
71
71
|
// from the collection admin config is needed (mirrors how FieldRenderer
|
|
72
|
-
// resolves the global richText editor from
|
|
72
|
+
// resolves the global richText editor from admin config).
|
|
73
73
|
const blockAdminByType = useMemo(() => {
|
|
74
74
|
const map = new Map<string, BlockAdminConfig>()
|
|
75
|
-
for (const entry of
|
|
75
|
+
for (const entry of getAdminConfig().blockAdmin ?? []) {
|
|
76
76
|
map.set(entry.blockType, entry)
|
|
77
77
|
}
|
|
78
78
|
return map
|
|
@@ -15,7 +15,7 @@ import type {
|
|
|
15
15
|
GroupField as GroupFieldType,
|
|
16
16
|
RichTextEditorComponent,
|
|
17
17
|
} from '@byline/core'
|
|
18
|
-
import {
|
|
18
|
+
import { getAdminConfig } from '@byline/core'
|
|
19
19
|
import cx from 'clsx'
|
|
20
20
|
|
|
21
21
|
import { useFormContext } from '../forms/form-context'
|
|
@@ -63,7 +63,7 @@ interface FieldRendererProps {
|
|
|
63
63
|
/**
|
|
64
64
|
* Per-field rich-text editor component override from the admin config.
|
|
65
65
|
* Takes precedence over the globally registered
|
|
66
|
-
* `
|
|
66
|
+
* `AdminConfig.fields.richText.editor` for this single field.
|
|
67
67
|
* Ignored when `field.type !== 'richText'`.
|
|
68
68
|
*/
|
|
69
69
|
editor?: RichTextEditorComponent
|
|
@@ -183,11 +183,11 @@ export const FieldRenderer = ({
|
|
|
183
183
|
)
|
|
184
184
|
case 'richText': {
|
|
185
185
|
// Admin-side per-field override takes precedence over the globally
|
|
186
|
-
// registered editor (`
|
|
186
|
+
// registered editor (`AdminConfig.fields.richText.editor`). The
|
|
187
187
|
// override travels via `FieldAdminConfig.fields.<name>.editor` —
|
|
188
188
|
// see admin-types — so React component references stay out of the
|
|
189
189
|
// schema graph that's loaded by the server bootstrap.
|
|
190
|
-
const RichTextEditor = editor ??
|
|
190
|
+
const RichTextEditor = editor ?? getAdminConfig().fields?.richText?.editor
|
|
191
191
|
if (!RichTextEditor) {
|
|
192
192
|
throw new Error(
|
|
193
193
|
'No richText editor registered. Install @byline/richtext-lexical and set ' +
|
|
@@ -53,7 +53,7 @@ interface GroupFieldProps {
|
|
|
53
53
|
* Per-child-field admin overrides (`components` slots, richtext `editor`),
|
|
54
54
|
* keyed by dotted, index-free schema paths relative to this group
|
|
55
55
|
* ('caption', 'faq.answer'). Threaded by `BlocksField` from the site-wide
|
|
56
|
-
* `
|
|
56
|
+
* `AdminConfig.blockAdmin` registry (block children render through a
|
|
57
57
|
* synthesized group) and by `FieldRenderer` for plain schema groups, whose
|
|
58
58
|
* map arrives pre-sliced from the collection admin config. Exact-name
|
|
59
59
|
* entries apply to the child itself; deeper entries are re-sliced and
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import { useCallback, useEffect, useState } from 'react'
|
|
10
10
|
|
|
11
11
|
import type { CollectionAdminConfig, CollectionDefinition } from '@byline/core'
|
|
12
|
-
import { getCollectionAdminConfig
|
|
12
|
+
import { getCollectionAdminConfig } from '@byline/core'
|
|
13
13
|
import { useTranslation } from '@byline/i18n/react'
|
|
14
14
|
import { Button, CheckIcon, LoaderRing, Modal, Search } from '@byline/ui/react'
|
|
15
15
|
import cx from 'clsx'
|
|
@@ -29,7 +29,7 @@ import styles from './relation-picker.module.css'
|
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
31
|
* Row rendering strategy, in priority order:
|
|
32
|
-
* 1. `CollectionAdminConfig.
|
|
32
|
+
* 1. `CollectionAdminConfig.itemView` — a ColumnDefinition[] from the target
|
|
33
33
|
* admin config. Each row renders the declared columns side-by-side,
|
|
34
34
|
* reusing any column formatters (thumbnail, date, etc).
|
|
35
35
|
* 2. Explicit `displayField` prop on this component (forwarded from
|
|
@@ -138,7 +138,7 @@ export const RelationPicker = ({
|
|
|
138
138
|
|
|
139
139
|
const targetAdminConfig: CollectionAdminConfig | null =
|
|
140
140
|
getCollectionAdminConfig(targetCollectionPath)
|
|
141
|
-
const pickerColumns =
|
|
141
|
+
const pickerColumns = targetAdminConfig?.itemView
|
|
142
142
|
|
|
143
143
|
// Reset local state each time the modal opens so prior queries don't leak.
|
|
144
144
|
useEffect(() => {
|
|
@@ -7,7 +7,6 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import type { CollectionAdminConfig, CollectionDefinition } from '@byline/core'
|
|
10
|
-
import { resolveItemViewColumns } from '@byline/core'
|
|
11
10
|
import cx from 'clsx'
|
|
12
11
|
|
|
13
12
|
import { PickerCell, resolveFallbackDisplayField, resolveRowLabel } from './relation-display'
|
|
@@ -63,7 +62,7 @@ export function RelationSummary({
|
|
|
63
62
|
value,
|
|
64
63
|
cachedRecord,
|
|
65
64
|
}: RelationSummaryProps) {
|
|
66
|
-
const pickerColumns =
|
|
65
|
+
const pickerColumns = targetAdminConfig?.itemView
|
|
67
66
|
|
|
68
67
|
// Unresolved (deleted target).
|
|
69
68
|
if (value._resolved === false) {
|
|
@@ -18,7 +18,7 @@ import type {
|
|
|
18
18
|
TabSetDefinition,
|
|
19
19
|
WorkflowStatus,
|
|
20
20
|
} from '@byline/core'
|
|
21
|
-
import {
|
|
21
|
+
import { getAdminConfig } from '@byline/core'
|
|
22
22
|
import type { DocumentPatch } from '@byline/core/patches'
|
|
23
23
|
import { useTranslation } from '@byline/i18n/react'
|
|
24
24
|
import { Alert, Button, ComboButton } from '@byline/ui/react'
|
|
@@ -249,7 +249,7 @@ const FormContent = ({
|
|
|
249
249
|
// server-assigned `counter` or a read-only field, its value can't be
|
|
250
250
|
// reproduced or changed through the form, so the widget suppresses its
|
|
251
251
|
// source-derived preview and "Regenerate" affordance.
|
|
252
|
-
const pathSlugifier =
|
|
252
|
+
const pathSlugifier = getAdminConfig().slugifier
|
|
253
253
|
const pathSourceLocked = useMemo(() => {
|
|
254
254
|
if (!useAsPath) return false
|
|
255
255
|
const source = fields.find((f) => f.name === useAsPath)
|
|
@@ -99,7 +99,7 @@ export const PathWidget = ({
|
|
|
99
99
|
|
|
100
100
|
// The installation slugifier, or the built-in default. Server-side path
|
|
101
101
|
// derivation uses `ServerConfig.slugifier`; this must be the same function
|
|
102
|
-
// (registered via `
|
|
102
|
+
// (registered via `AdminConfig.slugifier`) or the preview will disagree
|
|
103
103
|
// with what the server persists.
|
|
104
104
|
const runSlugify = slugifier ?? slugify
|
|
105
105
|
|
|
@@ -18,8 +18,8 @@
|
|
|
18
18
|
* setting.
|
|
19
19
|
*
|
|
20
20
|
* Locale write surface mirrors the chrome-bar `<LanguageMenu>` — both
|
|
21
|
-
* route through `
|
|
22
|
-
* `
|
|
21
|
+
* route through `setAdminLocale` (the service) →
|
|
22
|
+
* `setAdminLocaleFn` (the host server fn) → the shared
|
|
23
23
|
* cookie + `admin_users.preferred_locale` writes. The dropdown carries
|
|
24
24
|
* an explicit "Use browser default" entry that maps to `null`
|
|
25
25
|
* server-side, clearing the column and re-engaging the detection
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
import { useMemo, useState } from 'react'
|
|
37
37
|
import { revalidateLogic, useForm } from '@tanstack/react-form-start'
|
|
38
38
|
|
|
39
|
-
import {
|
|
39
|
+
import { getAdminConfig } from '@byline/core'
|
|
40
40
|
import { useTranslation } from '@byline/i18n/react'
|
|
41
41
|
import { Alert, Button, LoaderEllipsis, Select } from '@byline/ui/react'
|
|
42
42
|
import cx from 'clsx'
|
|
@@ -76,7 +76,7 @@ interface PreferencesProps {
|
|
|
76
76
|
|
|
77
77
|
export function Preferences({ account, onClose, onSuccess }: PreferencesProps) {
|
|
78
78
|
const { t } = useTranslation('byline-admin')
|
|
79
|
-
const {
|
|
79
|
+
const { setAdminLocale } = useBylineAdminServices()
|
|
80
80
|
const [formError, setFormError] = useState<string | null>(null)
|
|
81
81
|
const [successMessage, setSuccessMessage] = useState<string | null>(null)
|
|
82
82
|
|
|
@@ -85,11 +85,11 @@ export function Preferences({ account, onClose, onSuccess }: PreferencesProps) {
|
|
|
85
85
|
// ("Français" vs CLDR's lowercase "français"); when it's not set,
|
|
86
86
|
// fall back to the raw code so the form still functions.
|
|
87
87
|
const localeItems = useMemo(() => {
|
|
88
|
-
const { i18n } =
|
|
88
|
+
const { i18n } = getAdminConfig()
|
|
89
89
|
const definitionByCode = new Map(
|
|
90
|
-
(i18n.
|
|
90
|
+
(i18n.admin.localeDefinitions ?? []).map((d) => [d.code, d.nativeName])
|
|
91
91
|
)
|
|
92
|
-
const items = i18n.
|
|
92
|
+
const items = i18n.admin.locales.map((code) => ({
|
|
93
93
|
value: code,
|
|
94
94
|
label: definitionByCode.get(code) ?? code,
|
|
95
95
|
}))
|
|
@@ -114,7 +114,7 @@ export function Preferences({ account, onClose, onSuccess }: PreferencesProps) {
|
|
|
114
114
|
return
|
|
115
115
|
}
|
|
116
116
|
try {
|
|
117
|
-
const result = await
|
|
117
|
+
const result = await setAdminLocale({ data: { locale: nextLocale } })
|
|
118
118
|
// Pre-auth path returns `account: null`; the form is only
|
|
119
119
|
// reachable behind an admin session, so `account` should be
|
|
120
120
|
// populated. Treat absence as a defensive no-op.
|
|
@@ -54,7 +54,7 @@ export interface AdminUserRow {
|
|
|
54
54
|
* Admin interface locale preference. `null` means "use the detection
|
|
55
55
|
* cascade" (cookie → Accept-Language → defaultLocale). Stored as a
|
|
56
56
|
* BCP 47 code; validated at the command layer against the host's
|
|
57
|
-
* `i18n.
|
|
57
|
+
* `i18n.admin.locales`.
|
|
58
58
|
*/
|
|
59
59
|
preferred_locale: string | null
|
|
60
60
|
created_at: Date
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
|
|
26
26
|
import { type FormEvent, useState } from 'react'
|
|
27
27
|
|
|
28
|
-
import {
|
|
28
|
+
import { getAdminConfig } from '@byline/core'
|
|
29
29
|
import { useTranslation } from '@byline/i18n/react'
|
|
30
30
|
import { Alert, Button, Card, Input, LoaderEllipsis } from '@byline/ui/react'
|
|
31
31
|
import cx from 'clsx'
|
|
@@ -37,17 +37,15 @@ import styles from './sign-in-form.module.css'
|
|
|
37
37
|
export interface SignInFormProps {
|
|
38
38
|
/** Host-validated root-relative destination after successful sign-in. */
|
|
39
39
|
redirectTo?: string
|
|
40
|
-
/** @deprecated Use `redirectTo`. */
|
|
41
|
-
callbackUrl?: string
|
|
42
40
|
/**
|
|
43
41
|
* Optional plain "Home" link rendered on the left of the action row.
|
|
44
|
-
* Typically the host's
|
|
45
|
-
* navigate back to the public site without typing the URL.
|
|
42
|
+
* Typically the host's client-safe canonical site URL so signed-out admins
|
|
43
|
+
* can navigate back to the public site without typing the URL.
|
|
46
44
|
*/
|
|
47
45
|
homeUrl?: string
|
|
48
46
|
}
|
|
49
47
|
|
|
50
|
-
export function SignInForm({ redirectTo,
|
|
48
|
+
export function SignInForm({ redirectTo, homeUrl }: SignInFormProps) {
|
|
51
49
|
const { adminSignIn } = useBylineAdminServices()
|
|
52
50
|
const { t } = useTranslation('byline-admin')
|
|
53
51
|
const [email, setEmail] = useState('')
|
|
@@ -63,11 +61,7 @@ export function SignInForm({ redirectTo, callbackUrl, homeUrl }: SignInFormProps
|
|
|
63
61
|
return
|
|
64
62
|
}
|
|
65
63
|
|
|
66
|
-
const destination = resolveSignInFormRedirect(
|
|
67
|
-
redirectTo,
|
|
68
|
-
callbackUrl,
|
|
69
|
-
getClientConfig().routes.admin
|
|
70
|
-
)
|
|
64
|
+
const destination = resolveSignInFormRedirect(redirectTo, getAdminConfig().routes.admin)
|
|
71
65
|
|
|
72
66
|
setPending(true)
|
|
73
67
|
setError(null)
|
|
@@ -31,22 +31,17 @@ describe('normalizeRootRelativeRedirect', () => {
|
|
|
31
31
|
})
|
|
32
32
|
|
|
33
33
|
describe('resolveSignInFormRedirect', () => {
|
|
34
|
-
it('
|
|
35
|
-
expect(resolveSignInFormRedirect('/cms/account', '/cms
|
|
34
|
+
it('uses a safe redirectTo without evaluating the fallback', () => {
|
|
35
|
+
expect(resolveSignInFormRedirect('/cms/account', '/cms')).toBe('/cms/account')
|
|
36
36
|
expect(
|
|
37
|
-
resolveSignInFormRedirect('/cms/account',
|
|
37
|
+
resolveSignInFormRedirect('/cms/account', () => {
|
|
38
38
|
throw new Error('fallback must be lazy')
|
|
39
39
|
})
|
|
40
40
|
).toBe('/cms/account')
|
|
41
41
|
})
|
|
42
42
|
|
|
43
|
-
it('supports a safe deprecated callbackUrl when redirectTo is absent or unsafe', () => {
|
|
44
|
-
expect(resolveSignInFormRedirect(undefined, '/cms/users', '/cms')).toBe('/cms/users')
|
|
45
|
-
expect(resolveSignInFormRedirect('https://evil.test', '/cms/users', '/cms')).toBe('/cms/users')
|
|
46
|
-
})
|
|
47
|
-
|
|
48
43
|
it('uses the safe configured fallback without permitting an open redirect', () => {
|
|
49
|
-
expect(resolveSignInFormRedirect('//evil.test', '
|
|
50
|
-
expect(resolveSignInFormRedirect(undefined,
|
|
44
|
+
expect(resolveSignInFormRedirect('//evil.test', '/cms')).toBe('/cms')
|
|
45
|
+
expect(resolveSignInFormRedirect(undefined, 'https://evil.test')).toBe('/')
|
|
51
46
|
})
|
|
52
47
|
})
|
|
@@ -17,15 +17,12 @@ export function normalizeRootRelativeRedirect(value: string): string | undefined
|
|
|
17
17
|
return `${url.pathname}${url.search}${url.hash}`
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
/** Resolve the
|
|
20
|
+
/** Resolve the requested destination, then a trusted fallback. */
|
|
21
21
|
export function resolveSignInFormRedirect(
|
|
22
22
|
redirectTo: string | undefined,
|
|
23
|
-
callbackUrl: string | undefined,
|
|
24
23
|
fallback: string | (() => string)
|
|
25
24
|
): string {
|
|
26
|
-
const requested =
|
|
27
|
-
(redirectTo ? normalizeRootRelativeRedirect(redirectTo) : undefined) ??
|
|
28
|
-
(callbackUrl ? normalizeRootRelativeRedirect(callbackUrl) : undefined)
|
|
25
|
+
const requested = redirectTo ? normalizeRootRelativeRedirect(redirectTo) : undefined
|
|
29
26
|
if (requested) return requested
|
|
30
27
|
|
|
31
28
|
const defaultPath = typeof fallback === 'function' ? fallback() : fallback
|
|
@@ -3,11 +3,7 @@ import { describe, expectTypeOf, it } from 'vitest'
|
|
|
3
3
|
import type { SignInFormProps } from './components/sign-in-form.js'
|
|
4
4
|
|
|
5
5
|
describe('SignInFormProps', () => {
|
|
6
|
-
it('
|
|
7
|
-
expectTypeOf<{ callbackUrl: string }>().toMatchTypeOf<SignInFormProps>()
|
|
8
|
-
})
|
|
9
|
-
|
|
10
|
-
it('accepts the preferred redirectTo prop and no destination prop', () => {
|
|
6
|
+
it('accepts redirectTo and no destination prop', () => {
|
|
11
7
|
expectTypeOf<{ redirectTo: string }>().toMatchTypeOf<SignInFormProps>()
|
|
12
8
|
expectTypeOf<Record<string, never>>().toMatchTypeOf<SignInFormProps>()
|
|
13
9
|
})
|
|
@@ -67,7 +67,7 @@ export type UpdateAccountInput = UpdateAccountRequest
|
|
|
67
67
|
/** Same shape as `ChangeAccountPasswordRequest` from `@byline/admin/admin-account`. */
|
|
68
68
|
export type ChangeAccountPasswordInput = ChangeAccountPasswordRequest
|
|
69
69
|
|
|
70
|
-
export interface
|
|
70
|
+
export interface SetAdminLocaleInput {
|
|
71
71
|
/** BCP 47 tag, or `null` to clear the preference and re-engage detection. */
|
|
72
72
|
locale: string | null
|
|
73
73
|
}
|
|
@@ -78,7 +78,7 @@ export interface SetInterfaceLocaleInput {
|
|
|
78
78
|
* the account page); `null` on pre-auth surfaces (the sign-in page
|
|
79
79
|
* locale dropdown) where the cookie write is the only effect.
|
|
80
80
|
*/
|
|
81
|
-
export interface
|
|
81
|
+
export interface SetAdminLocaleResult {
|
|
82
82
|
ok: true
|
|
83
83
|
locale: string | null
|
|
84
84
|
account: AccountResponse | null
|
|
@@ -159,7 +159,7 @@ export interface BylineAdminServices {
|
|
|
159
159
|
// Account self-service
|
|
160
160
|
updateAccount: AdminServiceCall<UpdateAccountInput, AccountResponse>
|
|
161
161
|
changeAccountPassword: AdminServiceCall<ChangeAccountPasswordInput, AccountResponse>
|
|
162
|
-
|
|
162
|
+
setAdminLocale: AdminServiceCall<SetAdminLocaleInput, SetAdminLocaleResult>
|
|
163
163
|
|
|
164
164
|
// Admin user writes (page-container reads stay in the host for now)
|
|
165
165
|
createAdminUser: AdminServiceCall<CreateAdminUserInput, AdminUserResponse>
|