@byline/admin 4.11.1 → 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.
Files changed (30) hide show
  1. package/dist/fields/blocks/blocks-field.js +2 -2
  2. package/dist/fields/field-renderer.d.ts +1 -1
  3. package/dist/fields/field-renderer.js +2 -2
  4. package/dist/fields/group/group-field.d.ts +1 -1
  5. package/dist/fields/relation/relation-picker.d.ts +1 -1
  6. package/dist/fields/relation/relation-picker.js +2 -2
  7. package/dist/fields/relation/relation-summary.js +1 -2
  8. package/dist/forms/form-renderer.js +2 -2
  9. package/dist/modules/admin-account/components/preferences.js +6 -6
  10. package/dist/modules/admin-users/repository.d.ts +1 -1
  11. package/dist/modules/auth/components/sign-in-form.d.ts +3 -5
  12. package/dist/modules/auth/components/sign-in-form.js +3 -3
  13. package/dist/modules/auth/safe-redirect.d.ts +2 -2
  14. package/dist/modules/auth/safe-redirect.js +2 -2
  15. package/dist/services/admin-services-types.d.ts +3 -3
  16. package/package.json +14 -14
  17. package/src/fields/blocks/blocks-field.tsx +4 -4
  18. package/src/fields/field-renderer.tsx +4 -4
  19. package/src/fields/group/group-field.tsx +1 -1
  20. package/src/fields/relation/relation-picker.tsx +3 -3
  21. package/src/fields/relation/relation-summary.tsx +1 -2
  22. package/src/forms/form-renderer.tsx +2 -2
  23. package/src/forms/path-widget.tsx +1 -1
  24. package/src/modules/admin-account/components/preferences.tsx +8 -8
  25. package/src/modules/admin-users/repository.ts +1 -1
  26. package/src/modules/auth/components/sign-in-form.tsx +5 -11
  27. package/src/modules/auth/safe-redirect.test.node.ts +5 -10
  28. package/src/modules/auth/safe-redirect.ts +2 -5
  29. package/src/modules/auth/sign-in-form-props.test.node.ts +1 -5
  30. 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 { getClientConfig } from "@byline/core";
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 getClientConfig().blockAdmin ?? [])map.set(entry.blockType, entry);
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
- * `ClientConfig.fields.richText.editor` for this single field.
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 { getClientConfig } from "@byline/core";
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 ?? getClientConfig().fields?.richText?.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
- * `ClientConfig.blockAdmin` registry (block children render through a
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.picker` — a ColumnDefinition[] from the target
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, resolveItemViewColumns } from "@byline/core";
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 = resolveItemViewColumns(targetAdminConfig);
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 = resolveItemViewColumns(targetAdminConfig);
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 { getClientConfig } from "@byline/core";
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 = getClientConfig().slugifier;
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 { getClientConfig } from "@byline/core";
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 { setInterfaceLocale } = useBylineAdminServices();
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 } = getClientConfig();
30
- const definitionByCode = new Map((i18n.interface.localeDefinitions ?? []).map((d)=>[
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.interface.locales.map((code)=>({
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 setInterfaceLocale({
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.interface.locales`.
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 configured `serverURL` so signed-out admins can
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, callbackUrl, homeUrl }: SignInFormProps): import("react").JSX.Element;
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 { getClientConfig } from "@byline/core";
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, callbackUrl, homeUrl }) {
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, callbackUrl, getClientConfig().routes.admin);
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 new prop first, then the deprecated prop, then a trusted fallback. */
4
- export declare function resolveSignInFormRedirect(redirectTo: string | undefined, callbackUrl: string | undefined, fallback: string | (() => string)): string;
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, callbackUrl, fallback) {
15
- const requested = (redirectTo ? safe_redirect_normalizeRootRelativeRedirect(redirectTo) : void 0) ?? (callbackUrl ? safe_redirect_normalizeRootRelativeRedirect(callbackUrl) : void 0);
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 SetInterfaceLocaleInput {
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 SetInterfaceLocaleResult {
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
- setInterfaceLocale: AdminServiceCall<SetInterfaceLocaleInput, SetInterfaceLocaleResult>;
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.11.1",
5
+ "version": "4.12.0",
6
6
  "engines": {
7
7
  "node": ">=20.9.0"
8
8
  },
@@ -165,32 +165,32 @@
165
165
  "@codemirror/lang-yaml": "^6.1.3",
166
166
  "@codemirror/language": "^6.12.4",
167
167
  "@codemirror/state": "^6.7.1",
168
- "@codemirror/view": "^6.43.6",
168
+ "@codemirror/view": "^6.43.7",
169
169
  "@lezer/highlight": "^1.2.3",
170
- "@tanstack/react-form-start": "^1.33.2",
170
+ "@tanstack/react-form-start": "^1.33.3",
171
171
  "clsx": "^2.1.1",
172
- "jose": "^6.2.3",
172
+ "jose": "^6.2.6",
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.11.1",
177
- "@byline/i18n": "4.11.1",
178
- "@byline/ui": "4.11.1",
179
- "@byline/core": "4.11.1"
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",
183
183
  "react-dom": "^19.0.0"
184
184
  },
185
185
  "devDependencies": {
186
- "@biomejs/biome": "2.5.4",
186
+ "@biomejs/biome": "2.5.6",
187
187
  "@rsbuild/plugin-react": "^2.1.0",
188
188
  "@rslib/core": "^0.23.2",
189
- "@types/node": "^26.1.1",
190
- "@types/react": "19.2.17",
191
- "@types/react-dom": "19.2.3",
192
- "react": "^19.2.7",
193
- "react-dom": "^19.2.7",
189
+ "@types/node": "^26.1.2",
190
+ "@types/react": "19.2.18",
191
+ "@types/react-dom": "19.2.4",
192
+ "react": "^19.2.8",
193
+ "react-dom": "^19.2.8",
194
194
  "rimraf": "^6.1.3",
195
195
  "tsx": "^4.23.1",
196
196
  "typescript": "^7.0.2",
@@ -14,7 +14,7 @@ import type {
14
14
  Field,
15
15
  GroupField as GroupFieldType,
16
16
  } from '@byline/core'
17
- import { getClientConfig } from '@byline/core'
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 (`ClientConfig.blockAdmin`), resolved by
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 client config).
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 getClientConfig().blockAdmin ?? []) {
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 { getClientConfig } from '@byline/core'
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
- * `ClientConfig.fields.richText.editor` for this single field.
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 (`ClientConfig.fields.richText.editor`). The
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 ?? getClientConfig().fields?.richText?.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
- * `ClientConfig.blockAdmin` registry (block children render through a
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, resolveItemViewColumns } from '@byline/core'
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.picker` — a ColumnDefinition[] from the target
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 = resolveItemViewColumns(targetAdminConfig)
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 = resolveItemViewColumns(targetAdminConfig)
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 { getClientConfig } from '@byline/core'
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 = getClientConfig().slugifier
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 `ClientConfig.slugifier`) or the preview will disagree
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 `setInterfaceLocale` (the service) →
22
- * `setInterfaceLocaleFn` (the host server fn) → the shared
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 { getClientConfig } from '@byline/core'
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 { setInterfaceLocale } = useBylineAdminServices()
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 } = getClientConfig()
88
+ const { i18n } = getAdminConfig()
89
89
  const definitionByCode = new Map(
90
- (i18n.interface.localeDefinitions ?? []).map((d) => [d.code, d.nativeName])
90
+ (i18n.admin.localeDefinitions ?? []).map((d) => [d.code, d.nativeName])
91
91
  )
92
- const items = i18n.interface.locales.map((code) => ({
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 setInterfaceLocale({ data: { locale: nextLocale } })
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.interface.locales`.
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 { getClientConfig } from '@byline/core'
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 configured `serverURL` so signed-out admins can
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, callbackUrl, homeUrl }: SignInFormProps) {
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('prefers a safe redirectTo over the deprecated callbackUrl', () => {
35
- expect(resolveSignInFormRedirect('/cms/account', '/cms/users', '/cms')).toBe('/cms/account')
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', undefined, () => {
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', 'https://evil.test', '/cms')).toBe('/cms')
50
- expect(resolveSignInFormRedirect(undefined, undefined, 'https://evil.test')).toBe('/')
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 new prop first, then the deprecated prop, then a trusted fallback. */
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('keeps legacy callbackUrl-only callers type-compatible', () => {
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 SetInterfaceLocaleInput {
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 SetInterfaceLocaleResult {
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
- setInterfaceLocale: AdminServiceCall<SetInterfaceLocaleInput, SetInterfaceLocaleResult>
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>