@byline/admin 3.17.0 → 3.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/forms/form-renderer.js +13 -1
- package/dist/forms/path-widget.d.ts +19 -1
- package/dist/forms/path-widget.js +10 -6
- package/dist/modules/admin-account/errors.d.ts +2 -2
- package/dist/modules/admin-activity/abilities.d.ts +1 -1
- package/dist/modules/admin-permissions/abilities.d.ts +2 -2
- package/dist/modules/admin-permissions/errors.d.ts +2 -2
- package/dist/modules/admin-permissions/schemas.d.ts +4 -4
- package/dist/modules/admin-roles/abilities.d.ts +4 -4
- package/dist/modules/admin-roles/errors.d.ts +4 -4
- package/dist/modules/admin-users/abilities.d.ts +5 -5
- package/dist/modules/admin-users/errors.d.ts +5 -5
- package/dist/modules/admin-users/schemas.d.ts +6 -6
- package/dist/vendor/noble-argon2/blake2.js +17 -17
- package/dist/widgets/source-locale-badge/source-locale-badge.d.ts +4 -18
- package/package.json +16 -16
- package/src/forms/form-renderer.tsx +16 -0
- package/src/forms/path-widget.test.tsx +33 -0
- package/src/forms/path-widget.tsx +33 -5
- package/src/forms/upload-executor.ts +4 -4
|
@@ -1,6 +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
5
|
import { useTranslation } from "@byline/i18n/react";
|
|
5
6
|
import { Alert, Button, ComboButton } from "@byline/ui/react";
|
|
6
7
|
import classnames from "classnames";
|
|
@@ -32,6 +33,15 @@ const FormContent = ({ mode, fields, onSubmit, onCancel, onStatusChange, onUnpub
|
|
|
32
33
|
const [pendingSystemFieldsSubmit, setPendingSystemFieldsSubmit] = useState(null);
|
|
33
34
|
const [contentLocale, setContentLocale] = useState(initialLocale ?? defaultLocale);
|
|
34
35
|
const { uploadField } = useBylineFieldServices();
|
|
36
|
+
const pathSlugifier = getClientConfig().slugifier;
|
|
37
|
+
const pathSourceLocked = useMemo(()=>{
|
|
38
|
+
if (!useAsPath) return false;
|
|
39
|
+
const source = fields.find((f)=>f.name === useAsPath);
|
|
40
|
+
return null != source && ('counter' === source.type || true === source.readOnly);
|
|
41
|
+
}, [
|
|
42
|
+
useAsPath,
|
|
43
|
+
fields
|
|
44
|
+
]);
|
|
35
45
|
useEffect(()=>{
|
|
36
46
|
if (initialLocale) setContentLocale(initialLocale);
|
|
37
47
|
}, [
|
|
@@ -345,7 +355,9 @@ const FormContent = ({ mode, fields, onSubmit, onCancel, onStatusChange, onUnpub
|
|
|
345
355
|
collectionPath: collectionPath ?? '',
|
|
346
356
|
defaultLocale: defaultLocale,
|
|
347
357
|
activeLocale: contentLocale,
|
|
348
|
-
mode: mode
|
|
358
|
+
mode: mode,
|
|
359
|
+
slugifier: pathSlugifier,
|
|
360
|
+
sourceLocked: pathSourceLocked
|
|
349
361
|
}),
|
|
350
362
|
tree && 'edit' === mode && 'string' == typeof initialData?.id && /*#__PURE__*/ jsx(TreePlacementWidget, {
|
|
351
363
|
collectionPath: collectionPath ?? '',
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { SlugifierFn } from '@byline/core';
|
|
1
2
|
export interface PathWidgetProps {
|
|
2
3
|
/** The collection's `useAsPath` source field name, when configured. */
|
|
3
4
|
useAsPath: string | undefined;
|
|
@@ -16,6 +17,23 @@ export interface PathWidgetProps {
|
|
|
16
17
|
activeLocale: string;
|
|
17
18
|
/** `'create'` shows the live derived preview as placeholder text. */
|
|
18
19
|
mode: 'create' | 'edit';
|
|
20
|
+
/**
|
|
21
|
+
* Installation slugifier used to derive the live preview. Must match the
|
|
22
|
+
* server-side `ServerConfig.slugifier` so the preview agrees with what is
|
|
23
|
+
* persisted. Defaults to the built-in `slugify` when omitted — callers
|
|
24
|
+
* that keep the default slugifier need not pass this.
|
|
25
|
+
*/
|
|
26
|
+
slugifier?: SlugifierFn;
|
|
27
|
+
/**
|
|
28
|
+
* When `true`, the `useAsPath` source field's value is not editable through
|
|
29
|
+
* this form (e.g. it is an allocator-assigned `counter`, or an otherwise
|
|
30
|
+
* read-only field). Its value is either server-assigned — and so not
|
|
31
|
+
* reproducible client-side — or simply cannot change, which means the
|
|
32
|
+
* source-derived live preview and the "Regenerate" affordance are
|
|
33
|
+
* meaningless. When set, the widget suppresses both and just shows the
|
|
34
|
+
* persisted path. Defaults to `false`.
|
|
35
|
+
*/
|
|
36
|
+
sourceLocked?: boolean;
|
|
19
37
|
}
|
|
20
38
|
/**
|
|
21
39
|
* System-managed `path` widget.
|
|
@@ -33,4 +51,4 @@ export interface PathWidgetProps {
|
|
|
33
51
|
* Stable override handles: `.byline-form-path`, `.byline-form-path-header`,
|
|
34
52
|
* `.byline-form-path-regenerate`.
|
|
35
53
|
*/
|
|
36
|
-
export declare const PathWidget: ({ useAsPath, collectionPath, defaultLocale, activeLocale, mode, }: PathWidgetProps) => import("react").JSX.Element;
|
|
54
|
+
export declare const PathWidget: ({ useAsPath, collectionPath, defaultLocale, activeLocale, mode, slugifier, sourceLocked, }: PathWidgetProps) => import("react").JSX.Element;
|
|
@@ -12,25 +12,28 @@ function coerceToString(value) {
|
|
|
12
12
|
if (value instanceof Date) return value.toISOString();
|
|
13
13
|
return String(value);
|
|
14
14
|
}
|
|
15
|
-
const PathWidget = ({ useAsPath, collectionPath, defaultLocale, activeLocale, mode })=>{
|
|
15
|
+
const PathWidget = ({ useAsPath, collectionPath, defaultLocale, activeLocale, mode, slugifier, sourceLocked = false })=>{
|
|
16
16
|
const { setSystemPath } = useFormContext();
|
|
17
17
|
const { t } = useTranslation('byline-admin');
|
|
18
18
|
const systemPath = useSystemPath();
|
|
19
19
|
const sourceValue = useFieldValue(useAsPath ?? '');
|
|
20
|
+
const runSlugify = slugifier ?? slugify;
|
|
20
21
|
const isReadOnly = activeLocale !== defaultLocale;
|
|
21
22
|
const livePreview = useMemo(()=>{
|
|
22
|
-
if (!useAsPath) return '';
|
|
23
|
+
if (!useAsPath || sourceLocked) return '';
|
|
23
24
|
const asString = coerceToString(sourceValue);
|
|
24
25
|
if (0 === asString.length) return '';
|
|
25
|
-
return
|
|
26
|
+
return runSlugify(asString, {
|
|
26
27
|
locale: defaultLocale,
|
|
27
28
|
collectionPath
|
|
28
29
|
});
|
|
29
30
|
}, [
|
|
30
31
|
useAsPath,
|
|
32
|
+
sourceLocked,
|
|
31
33
|
sourceValue,
|
|
32
34
|
defaultLocale,
|
|
33
|
-
collectionPath
|
|
35
|
+
collectionPath,
|
|
36
|
+
runSlugify
|
|
34
37
|
]);
|
|
35
38
|
const inputValue = systemPath ?? '';
|
|
36
39
|
const handleChange = useCallback((next)=>{
|
|
@@ -46,14 +49,15 @@ const PathWidget = ({ useAsPath, collectionPath, defaultLocale, activeLocale, mo
|
|
|
46
49
|
]);
|
|
47
50
|
const formatted = useMemo(()=>{
|
|
48
51
|
if (0 === inputValue.length) return '';
|
|
49
|
-
return
|
|
52
|
+
return runSlugify(inputValue, {
|
|
50
53
|
locale: defaultLocale,
|
|
51
54
|
collectionPath
|
|
52
55
|
});
|
|
53
56
|
}, [
|
|
54
57
|
inputValue,
|
|
55
58
|
defaultLocale,
|
|
56
|
-
collectionPath
|
|
59
|
+
collectionPath,
|
|
60
|
+
runSlugify
|
|
57
61
|
]);
|
|
58
62
|
const validationHint = inputValue.length > 0 && formatted !== inputValue ? t('pathWidget.suggestedHint', {
|
|
59
63
|
formatted
|
|
@@ -20,8 +20,8 @@
|
|
|
20
20
|
* code per condition.
|
|
21
21
|
*/
|
|
22
22
|
export declare const AdminAccountErrorCodes: {
|
|
23
|
-
readonly NOT_FOUND:
|
|
24
|
-
readonly INVALID_CURRENT_PASSWORD:
|
|
23
|
+
readonly NOT_FOUND: 'admin.account.notFound';
|
|
24
|
+
readonly INVALID_CURRENT_PASSWORD: 'admin.account.invalidCurrentPassword';
|
|
25
25
|
};
|
|
26
26
|
export type AdminAccountErrorCode = (typeof AdminAccountErrorCodes)[keyof typeof AdminAccountErrorCodes];
|
|
27
27
|
export interface AdminAccountErrorOptions {
|
|
@@ -20,7 +20,7 @@ import type { AbilityRegistry } from '@byline/auth';
|
|
|
20
20
|
* append-only and is written by the lifecycle write-points, never edited.
|
|
21
21
|
*/
|
|
22
22
|
export declare const ADMIN_ACTIVITY_ABILITIES: {
|
|
23
|
-
readonly read:
|
|
23
|
+
readonly read: 'admin.activity.read';
|
|
24
24
|
};
|
|
25
25
|
export type AdminActivityAbilityKey = (typeof ADMIN_ACTIVITY_ABILITIES)[keyof typeof ADMIN_ACTIVITY_ABILITIES];
|
|
26
26
|
/**
|
|
@@ -19,8 +19,8 @@ import type { AbilityRegistry } from '@byline/auth';
|
|
|
19
19
|
* every permission-managing role.
|
|
20
20
|
*/
|
|
21
21
|
export declare const ADMIN_PERMISSIONS_ABILITIES: {
|
|
22
|
-
readonly read:
|
|
23
|
-
readonly update:
|
|
22
|
+
readonly read: 'admin.permissions.read';
|
|
23
|
+
readonly update: 'admin.permissions.update';
|
|
24
24
|
};
|
|
25
25
|
export type AdminPermissionsAbilityKey = (typeof ADMIN_PERMISSIONS_ABILITIES)[keyof typeof ADMIN_PERMISSIONS_ABILITIES];
|
|
26
26
|
export declare function registerAdminPermissionsAbilities(registry: AbilityRegistry): void;
|
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
* inspector is read-only and never throws either of these.
|
|
15
15
|
*/
|
|
16
16
|
export declare const AdminPermissionsErrorCodes: {
|
|
17
|
-
readonly ROLE_NOT_FOUND:
|
|
18
|
-
readonly ABILITY_UNREGISTERED:
|
|
17
|
+
readonly ROLE_NOT_FOUND: 'admin.permissions.roleNotFound';
|
|
18
|
+
readonly ABILITY_UNREGISTERED: 'admin.permissions.abilityUnregistered';
|
|
19
19
|
};
|
|
20
20
|
export type AdminPermissionsErrorCode = (typeof AdminPermissionsErrorCodes)[keyof typeof AdminPermissionsErrorCodes];
|
|
21
21
|
export interface AdminPermissionsErrorOptions {
|
|
@@ -29,8 +29,8 @@ export declare const abilityDescriptorResponseSchema: z.ZodObject<{
|
|
|
29
29
|
source: z.ZodNullable<z.ZodEnum<{
|
|
30
30
|
admin: "admin";
|
|
31
31
|
collection: "collection";
|
|
32
|
-
plugin: "plugin";
|
|
33
32
|
core: "core";
|
|
33
|
+
plugin: "plugin";
|
|
34
34
|
}>>;
|
|
35
35
|
}, z.core.$strip>;
|
|
36
36
|
export type AbilityDescriptorResponse = z.infer<typeof abilityDescriptorResponseSchema>;
|
|
@@ -44,8 +44,8 @@ export declare const abilityGroupResponseSchema: z.ZodObject<{
|
|
|
44
44
|
source: z.ZodNullable<z.ZodEnum<{
|
|
45
45
|
admin: "admin";
|
|
46
46
|
collection: "collection";
|
|
47
|
-
plugin: "plugin";
|
|
48
47
|
core: "core";
|
|
48
|
+
plugin: "plugin";
|
|
49
49
|
}>>;
|
|
50
50
|
}, z.core.$strip>>;
|
|
51
51
|
}, z.core.$strip>;
|
|
@@ -63,8 +63,8 @@ export declare const listRegisteredAbilitiesResponseSchema: z.ZodObject<{
|
|
|
63
63
|
source: z.ZodNullable<z.ZodEnum<{
|
|
64
64
|
admin: "admin";
|
|
65
65
|
collection: "collection";
|
|
66
|
-
plugin: "plugin";
|
|
67
66
|
core: "core";
|
|
67
|
+
plugin: "plugin";
|
|
68
68
|
}>>;
|
|
69
69
|
}, z.core.$strip>>;
|
|
70
70
|
groups: z.ZodArray<z.ZodObject<{
|
|
@@ -77,8 +77,8 @@ export declare const listRegisteredAbilitiesResponseSchema: z.ZodObject<{
|
|
|
77
77
|
source: z.ZodNullable<z.ZodEnum<{
|
|
78
78
|
admin: "admin";
|
|
79
79
|
collection: "collection";
|
|
80
|
-
plugin: "plugin";
|
|
81
80
|
core: "core";
|
|
81
|
+
plugin: "plugin";
|
|
82
82
|
}>>;
|
|
83
83
|
}, z.core.$strip>>;
|
|
84
84
|
}, z.core.$strip>>;
|
|
@@ -18,10 +18,10 @@ import type { AbilityRegistry } from '@byline/auth';
|
|
|
18
18
|
* keys there.
|
|
19
19
|
*/
|
|
20
20
|
export declare const ADMIN_ROLES_ABILITIES: {
|
|
21
|
-
readonly read:
|
|
22
|
-
readonly create:
|
|
23
|
-
readonly update:
|
|
24
|
-
readonly delete:
|
|
21
|
+
readonly read: 'admin.roles.read';
|
|
22
|
+
readonly create: 'admin.roles.create';
|
|
23
|
+
readonly update: 'admin.roles.update';
|
|
24
|
+
readonly delete: 'admin.roles.delete';
|
|
25
25
|
};
|
|
26
26
|
export type AdminRolesAbilityKey = (typeof ADMIN_ROLES_ABILITIES)[keyof typeof ADMIN_ROLES_ABILITIES];
|
|
27
27
|
/**
|
|
@@ -13,10 +13,10 @@
|
|
|
13
13
|
* ability keys in logs and admin UI messages.
|
|
14
14
|
*/
|
|
15
15
|
export declare const AdminRolesErrorCodes: {
|
|
16
|
-
readonly NOT_FOUND:
|
|
17
|
-
readonly MACHINE_NAME_IN_USE:
|
|
18
|
-
readonly VERSION_CONFLICT:
|
|
19
|
-
readonly USER_NOT_FOUND:
|
|
16
|
+
readonly NOT_FOUND: 'admin.roles.notFound';
|
|
17
|
+
readonly MACHINE_NAME_IN_USE: 'admin.roles.machineNameInUse';
|
|
18
|
+
readonly VERSION_CONFLICT: 'admin.roles.versionConflict';
|
|
19
|
+
readonly USER_NOT_FOUND: 'admin.roles.userNotFound';
|
|
20
20
|
};
|
|
21
21
|
export type AdminRolesErrorCode = (typeof AdminRolesErrorCodes)[keyof typeof AdminRolesErrorCodes];
|
|
22
22
|
export interface AdminRolesErrorOptions {
|
|
@@ -25,11 +25,11 @@ import type { AbilityRegistry } from '@byline/auth';
|
|
|
25
25
|
* are strictly for administering other admin users.
|
|
26
26
|
*/
|
|
27
27
|
export declare const ADMIN_USERS_ABILITIES: {
|
|
28
|
-
readonly read:
|
|
29
|
-
readonly create:
|
|
30
|
-
readonly update:
|
|
31
|
-
readonly delete:
|
|
32
|
-
readonly changePassword:
|
|
28
|
+
readonly read: 'admin.users.read';
|
|
29
|
+
readonly create: 'admin.users.create';
|
|
30
|
+
readonly update: 'admin.users.update';
|
|
31
|
+
readonly delete: 'admin.users.delete';
|
|
32
|
+
readonly changePassword: 'admin.users.changePassword';
|
|
33
33
|
};
|
|
34
34
|
export type AdminUsersAbilityKey = (typeof ADMIN_USERS_ABILITIES)[keyof typeof ADMIN_USERS_ABILITIES];
|
|
35
35
|
/**
|
|
@@ -18,11 +18,11 @@
|
|
|
18
18
|
* alongside the matching ability keys in logs and admin UI messages.
|
|
19
19
|
*/
|
|
20
20
|
export declare const AdminUsersErrorCodes: {
|
|
21
|
-
readonly NOT_FOUND:
|
|
22
|
-
readonly EMAIL_IN_USE:
|
|
23
|
-
readonly SELF_DELETE_FORBIDDEN:
|
|
24
|
-
readonly SELF_DISABLE_FORBIDDEN:
|
|
25
|
-
readonly VERSION_CONFLICT:
|
|
21
|
+
readonly NOT_FOUND: 'admin.users.notFound';
|
|
22
|
+
readonly EMAIL_IN_USE: 'admin.users.emailInUse';
|
|
23
|
+
readonly SELF_DELETE_FORBIDDEN: 'admin.users.selfDeleteForbidden';
|
|
24
|
+
readonly SELF_DISABLE_FORBIDDEN: 'admin.users.selfDisableForbidden';
|
|
25
|
+
readonly VERSION_CONFLICT: 'admin.users.versionConflict';
|
|
26
26
|
};
|
|
27
27
|
export type AdminUsersErrorCode = (typeof AdminUsersErrorCodes)[keyof typeof AdminUsersErrorCodes];
|
|
28
28
|
export interface AdminUsersErrorOptions {
|
|
@@ -11,12 +11,12 @@ export declare const listAdminUsersRequestSchema: z.ZodObject<{
|
|
|
11
11
|
pageSize: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
|
|
12
12
|
query: z.ZodOptional<z.ZodString>;
|
|
13
13
|
order: z.ZodDefault<z.ZodOptional<z.ZodEnum<{
|
|
14
|
+
created_at: "created_at";
|
|
14
15
|
email: "email";
|
|
15
|
-
given_name: "given_name";
|
|
16
16
|
family_name: "family_name";
|
|
17
|
-
|
|
18
|
-
created_at: "created_at";
|
|
17
|
+
given_name: "given_name";
|
|
19
18
|
updated_at: "updated_at";
|
|
19
|
+
username: "username";
|
|
20
20
|
}>>>;
|
|
21
21
|
desc: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
22
22
|
}, z.core.$strip>;
|
|
@@ -121,12 +121,12 @@ export declare const adminUserListResponseSchema: z.ZodObject<{
|
|
|
121
121
|
page_size: z.ZodNumber;
|
|
122
122
|
query: z.ZodString;
|
|
123
123
|
order: z.ZodEnum<{
|
|
124
|
+
created_at: "created_at";
|
|
124
125
|
email: "email";
|
|
125
|
-
given_name: "given_name";
|
|
126
126
|
family_name: "family_name";
|
|
127
|
-
|
|
128
|
-
created_at: "created_at";
|
|
127
|
+
given_name: "given_name";
|
|
129
128
|
updated_at: "updated_at";
|
|
129
|
+
username: "username";
|
|
130
130
|
}>;
|
|
131
131
|
desc: z.ZodBoolean;
|
|
132
132
|
}, z.core.$strip>;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { BSIGMA, G1s, G2s } from "./_blake.js";
|
|
2
2
|
import { SHA256_IV } from "./_md.js";
|
|
3
3
|
import { abytes, aexists, anumber, aoutput, clean, createHasher, swap32IfBE, swap8IfBE, u32 } from "./utils.js";
|
|
4
|
-
import * as
|
|
4
|
+
import * as __rspack_external__u64_js_5917676b from "./_u64.js";
|
|
5
5
|
const B2B_IV = /* @__PURE__ */ Uint32Array.from([
|
|
6
6
|
0xf3bcc908,
|
|
7
7
|
0x6a09e667,
|
|
@@ -27,25 +27,25 @@ function G1b(a, b, c, d, msg, x) {
|
|
|
27
27
|
let Bl = BBUF[2 * b], Bh = BBUF[2 * b + 1];
|
|
28
28
|
let Cl = BBUF[2 * c], Ch = BBUF[2 * c + 1];
|
|
29
29
|
let Dl = BBUF[2 * d], Dh = BBUF[2 * d + 1];
|
|
30
|
-
let ll =
|
|
31
|
-
Ah =
|
|
30
|
+
let ll = __rspack_external__u64_js_5917676b.add3L(Al, Bl, Xl);
|
|
31
|
+
Ah = __rspack_external__u64_js_5917676b.add3H(ll, Ah, Bh, Xh);
|
|
32
32
|
Al = 0 | ll;
|
|
33
33
|
({ Dh, Dl } = {
|
|
34
34
|
Dh: Dh ^ Ah,
|
|
35
35
|
Dl: Dl ^ Al
|
|
36
36
|
});
|
|
37
37
|
({ Dh, Dl } = {
|
|
38
|
-
Dh:
|
|
39
|
-
Dl:
|
|
38
|
+
Dh: __rspack_external__u64_js_5917676b.rotr32H(Dh, Dl),
|
|
39
|
+
Dl: __rspack_external__u64_js_5917676b.rotr32L(Dh, Dl)
|
|
40
40
|
});
|
|
41
|
-
({ h: Ch, l: Cl } =
|
|
41
|
+
({ h: Ch, l: Cl } = __rspack_external__u64_js_5917676b.add(Ch, Cl, Dh, Dl));
|
|
42
42
|
({ Bh, Bl } = {
|
|
43
43
|
Bh: Bh ^ Ch,
|
|
44
44
|
Bl: Bl ^ Cl
|
|
45
45
|
});
|
|
46
46
|
({ Bh, Bl } = {
|
|
47
|
-
Bh:
|
|
48
|
-
Bl:
|
|
47
|
+
Bh: __rspack_external__u64_js_5917676b.rotrSH(Bh, Bl, 24),
|
|
48
|
+
Bl: __rspack_external__u64_js_5917676b.rotrSL(Bh, Bl, 24)
|
|
49
49
|
});
|
|
50
50
|
BBUF[2 * a] = Al, BBUF[2 * a + 1] = Ah;
|
|
51
51
|
BBUF[2 * b] = Bl, BBUF[2 * b + 1] = Bh;
|
|
@@ -58,25 +58,25 @@ function G2b(a, b, c, d, msg, x) {
|
|
|
58
58
|
let Bl = BBUF[2 * b], Bh = BBUF[2 * b + 1];
|
|
59
59
|
let Cl = BBUF[2 * c], Ch = BBUF[2 * c + 1];
|
|
60
60
|
let Dl = BBUF[2 * d], Dh = BBUF[2 * d + 1];
|
|
61
|
-
let ll =
|
|
62
|
-
Ah =
|
|
61
|
+
let ll = __rspack_external__u64_js_5917676b.add3L(Al, Bl, Xl);
|
|
62
|
+
Ah = __rspack_external__u64_js_5917676b.add3H(ll, Ah, Bh, Xh);
|
|
63
63
|
Al = 0 | ll;
|
|
64
64
|
({ Dh, Dl } = {
|
|
65
65
|
Dh: Dh ^ Ah,
|
|
66
66
|
Dl: Dl ^ Al
|
|
67
67
|
});
|
|
68
68
|
({ Dh, Dl } = {
|
|
69
|
-
Dh:
|
|
70
|
-
Dl:
|
|
69
|
+
Dh: __rspack_external__u64_js_5917676b.rotrSH(Dh, Dl, 16),
|
|
70
|
+
Dl: __rspack_external__u64_js_5917676b.rotrSL(Dh, Dl, 16)
|
|
71
71
|
});
|
|
72
|
-
({ h: Ch, l: Cl } =
|
|
72
|
+
({ h: Ch, l: Cl } = __rspack_external__u64_js_5917676b.add(Ch, Cl, Dh, Dl));
|
|
73
73
|
({ Bh, Bl } = {
|
|
74
74
|
Bh: Bh ^ Ch,
|
|
75
75
|
Bl: Bl ^ Cl
|
|
76
76
|
});
|
|
77
77
|
({ Bh, Bl } = {
|
|
78
|
-
Bh:
|
|
79
|
-
Bl:
|
|
78
|
+
Bh: __rspack_external__u64_js_5917676b.rotrBH(Bh, Bl, 63),
|
|
79
|
+
Bl: __rspack_external__u64_js_5917676b.rotrBL(Bh, Bl, 63)
|
|
80
80
|
});
|
|
81
81
|
BBUF[2 * a] = Al, BBUF[2 * a + 1] = Ah;
|
|
82
82
|
BBUF[2 * b] = Bl, BBUF[2 * b + 1] = Bh;
|
|
@@ -226,7 +226,7 @@ class _BLAKE2b extends _BLAKE2 {
|
|
|
226
226
|
compress(msg, offset, isLast) {
|
|
227
227
|
this.get().forEach((v, i)=>BBUF[i] = v);
|
|
228
228
|
BBUF.set(B2B_IV, 16);
|
|
229
|
-
let { h, l } =
|
|
229
|
+
let { h, l } = __rspack_external__u64_js_5917676b.fromBig(BigInt(this.length));
|
|
230
230
|
BBUF[24] = B2B_IV[8] ^ l;
|
|
231
231
|
BBUF[25] = B2B_IV[9] ^ h;
|
|
232
232
|
if (isLast) {
|
|
@@ -376,7 +376,7 @@ class _BLAKE2s extends _BLAKE2 {
|
|
|
376
376
|
this.v7 = 0 | v7;
|
|
377
377
|
}
|
|
378
378
|
compress(msg, offset, isLast) {
|
|
379
|
-
const { h, l } =
|
|
379
|
+
const { h, l } = __rspack_external__u64_js_5917676b.fromBig(BigInt(this.length));
|
|
380
380
|
const { v0, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15 } = compress(BSIGMA, offset, msg, 10, this.v0, this.v1, this.v2, this.v3, this.v4, this.v5, this.v6, this.v7, B2S_IV[0], B2S_IV[1], B2S_IV[2], B2S_IV[3], l ^ B2S_IV[4], h ^ B2S_IV[5], isLast ? ~B2S_IV[6] : B2S_IV[6], B2S_IV[7]);
|
|
381
381
|
this.v0 ^= v0 ^ v8;
|
|
382
382
|
this.v1 ^= v1 ^ v9;
|
|
@@ -10,21 +10,7 @@ export interface SourceLocaleBadgeProps {
|
|
|
10
10
|
locale: string;
|
|
11
11
|
className?: string;
|
|
12
12
|
}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
* list views. Mirrors the localized-field {@link LocaleBadge} in spirit but uses
|
|
18
|
-
* the shared {@link Badge} with the `noeffect` (neutral) intent.
|
|
19
|
-
*
|
|
20
|
-
* NOTE: currently rendered for *every* document so the anchor is visible during
|
|
21
|
-
* development. The intended end state is to show it only when `locale` differs
|
|
22
|
-
* from the system's current default content locale (a normal single-default
|
|
23
|
-
* install then shows nothing). See docs/07-internationalization/index.md.
|
|
24
|
-
*
|
|
25
|
-
* Stable override handle: `.byline-source-locale-badge`.
|
|
26
|
-
*/
|
|
27
|
-
export declare const SourceLocaleBadge: {
|
|
28
|
-
({ locale, className }: SourceLocaleBadgeProps): import("react").JSX.Element;
|
|
29
|
-
displayName: string;
|
|
30
|
-
};
|
|
13
|
+
export declare function SourceLocaleBadge({ locale, className }: SourceLocaleBadgeProps): import("react").JSX.Element;
|
|
14
|
+
export declare namespace SourceLocaleBadge {
|
|
15
|
+
var displayName: string;
|
|
16
|
+
}
|
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": "3.
|
|
5
|
+
"version": "3.18.0",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": ">=20.9.0"
|
|
8
8
|
},
|
|
@@ -153,31 +153,31 @@
|
|
|
153
153
|
"classnames": "^2.5.1",
|
|
154
154
|
"jose": "^6.2.3",
|
|
155
155
|
"react-diff-viewer-continued": "^4.2.2",
|
|
156
|
-
"uuid": "^14.0.
|
|
156
|
+
"uuid": "^14.0.1",
|
|
157
157
|
"zod": "^4.4.3",
|
|
158
|
-
"@byline/auth": "3.
|
|
159
|
-
"@byline/i18n": "3.
|
|
160
|
-
"@byline/
|
|
161
|
-
"@byline/
|
|
158
|
+
"@byline/auth": "3.18.0",
|
|
159
|
+
"@byline/i18n": "3.18.0",
|
|
160
|
+
"@byline/ui": "3.18.0",
|
|
161
|
+
"@byline/core": "3.18.0"
|
|
162
162
|
},
|
|
163
163
|
"peerDependencies": {
|
|
164
164
|
"react": "^19.0.0",
|
|
165
165
|
"react-dom": "^19.0.0"
|
|
166
166
|
},
|
|
167
167
|
"devDependencies": {
|
|
168
|
-
"@biomejs/biome": "2.
|
|
169
|
-
"@rsbuild/plugin-react": "^2.
|
|
170
|
-
"@rslib/core": "^0.
|
|
171
|
-
"@types/node": "^
|
|
172
|
-
"@types/react": "19.2.
|
|
168
|
+
"@biomejs/biome": "2.5.2",
|
|
169
|
+
"@rsbuild/plugin-react": "^2.1.0",
|
|
170
|
+
"@rslib/core": "^0.23.2",
|
|
171
|
+
"@types/node": "^26.1.0",
|
|
172
|
+
"@types/react": "19.2.17",
|
|
173
173
|
"@types/react-dom": "19.2.3",
|
|
174
|
-
"react": "19.2.
|
|
175
|
-
"react-dom": "19.2.
|
|
174
|
+
"react": "^19.2.7",
|
|
175
|
+
"react-dom": "^19.2.7",
|
|
176
176
|
"rimraf": "^6.1.3",
|
|
177
|
-
"tsx": "^4.
|
|
178
|
-
"typescript": "
|
|
177
|
+
"tsx": "^4.23.0",
|
|
178
|
+
"typescript": "^7.0.2",
|
|
179
179
|
"typescript-plugin-css-modules": "^5.2.0",
|
|
180
|
-
"vitest": "^4.1.
|
|
180
|
+
"vitest": "^4.1.10"
|
|
181
181
|
},
|
|
182
182
|
"publishConfig": {
|
|
183
183
|
"access": "public",
|
|
@@ -18,6 +18,7 @@ import type {
|
|
|
18
18
|
TabSetDefinition,
|
|
19
19
|
WorkflowStatus,
|
|
20
20
|
} from '@byline/core'
|
|
21
|
+
import { getClientConfig } from '@byline/core'
|
|
21
22
|
import type { DocumentPatch } from '@byline/core/patches'
|
|
22
23
|
import { useTranslation } from '@byline/i18n/react'
|
|
23
24
|
import { Alert, Button, ComboButton } from '@byline/ui/react'
|
|
@@ -241,6 +242,19 @@ const FormContent = ({
|
|
|
241
242
|
const [contentLocale, setContentLocale] = useState(initialLocale ?? defaultLocale)
|
|
242
243
|
const { uploadField } = useBylineFieldServices()
|
|
243
244
|
|
|
245
|
+
// Path-widget wiring. The live preview must use the installation's
|
|
246
|
+
// client-side slugifier (same function as `ServerConfig.slugifier`) so it
|
|
247
|
+
// agrees with the persisted path. And when the `useAsPath` source is a
|
|
248
|
+
// server-assigned `counter` or a read-only field, its value can't be
|
|
249
|
+
// reproduced or changed through the form, so the widget suppresses its
|
|
250
|
+
// source-derived preview and "Regenerate" affordance.
|
|
251
|
+
const pathSlugifier = getClientConfig().slugifier
|
|
252
|
+
const pathSourceLocked = useMemo(() => {
|
|
253
|
+
if (!useAsPath) return false
|
|
254
|
+
const source = fields.find((f) => f.name === useAsPath)
|
|
255
|
+
return source != null && (source.type === 'counter' || source.readOnly === true)
|
|
256
|
+
}, [useAsPath, fields])
|
|
257
|
+
|
|
244
258
|
// Sync contentLocale when the route re-fetches with a different locale.
|
|
245
259
|
useEffect(() => {
|
|
246
260
|
if (initialLocale) setContentLocale(initialLocale)
|
|
@@ -672,6 +686,8 @@ const FormContent = ({
|
|
|
672
686
|
defaultLocale={defaultLocale}
|
|
673
687
|
activeLocale={contentLocale}
|
|
674
688
|
mode={mode}
|
|
689
|
+
slugifier={pathSlugifier}
|
|
690
|
+
sourceLocked={pathSourceLocked}
|
|
675
691
|
/>
|
|
676
692
|
)}
|
|
677
693
|
{tree && mode === 'edit' && typeof initialData?.id === 'string' && (
|
|
@@ -105,6 +105,9 @@ describe('PathWidget', () => {
|
|
|
105
105
|
props: Partial<{
|
|
106
106
|
useAsPath: string | undefined
|
|
107
107
|
mode: 'create' | 'edit'
|
|
108
|
+
activeLocale: string
|
|
109
|
+
slugifier: (value: string, ctx: { locale: string; collectionPath: string }) => string
|
|
110
|
+
sourceLocked: boolean
|
|
108
111
|
}> = {}
|
|
109
112
|
) => {
|
|
110
113
|
act(() => {
|
|
@@ -113,7 +116,10 @@ describe('PathWidget', () => {
|
|
|
113
116
|
useAsPath={props.useAsPath ?? 'title'}
|
|
114
117
|
collectionPath="pages"
|
|
115
118
|
defaultLocale="en"
|
|
119
|
+
activeLocale={props.activeLocale ?? 'en'}
|
|
116
120
|
mode={props.mode ?? 'create'}
|
|
121
|
+
slugifier={props.slugifier}
|
|
122
|
+
sourceLocked={props.sourceLocked}
|
|
117
123
|
/>
|
|
118
124
|
)
|
|
119
125
|
})
|
|
@@ -214,4 +220,31 @@ describe('PathWidget', () => {
|
|
|
214
220
|
render({ mode: 'edit', useAsPath: undefined })
|
|
215
221
|
expect(container.querySelector('button')).toBeNull()
|
|
216
222
|
})
|
|
223
|
+
|
|
224
|
+
it('derives the preview with the provided slugifier instead of the default', () => {
|
|
225
|
+
// A custom slugifier that zero-pads a numeric serial to 7 digits — the
|
|
226
|
+
// shape used when a `counter` field feeds the path.
|
|
227
|
+
const padSerial = (value: string) => value.padStart(7, '0')
|
|
228
|
+
setFixture({ systemPath: null, sourceValue: 1 })
|
|
229
|
+
render({ mode: 'create', slugifier: padSerial })
|
|
230
|
+
|
|
231
|
+
const input = getInput()
|
|
232
|
+
// Default slugify would yield "1"; the custom slugifier yields "0000001".
|
|
233
|
+
expect(input.getAttribute('placeholder')).toBe('Will be saved as "0000001"')
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
it('suppresses the create-mode placeholder preview when the source is locked', () => {
|
|
237
|
+
setFixture({ systemPath: null, sourceValue: 1 })
|
|
238
|
+
render({ mode: 'create', sourceLocked: true })
|
|
239
|
+
expect(getInput().getAttribute('placeholder')).toBeNull()
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
it('suppresses the Regenerate button when the source is locked', () => {
|
|
243
|
+
// Persisted path differs from what a naive re-derivation would produce,
|
|
244
|
+
// which would normally surface Regenerate — but a locked (server-assigned)
|
|
245
|
+
// source can't be regenerated from the form, so it must stay hidden.
|
|
246
|
+
setFixture({ systemPath: '0000001', sourceValue: 1 })
|
|
247
|
+
render({ mode: 'edit', sourceLocked: true })
|
|
248
|
+
expect(container.querySelector('button')).toBeNull()
|
|
249
|
+
})
|
|
217
250
|
})
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import { useCallback, useMemo } from 'react'
|
|
12
12
|
|
|
13
|
+
import type { SlugifierFn } from '@byline/core'
|
|
13
14
|
import { slugify } from '@byline/core'
|
|
14
15
|
import { useTranslation } from '@byline/i18n/react'
|
|
15
16
|
import { Input, Label } from '@byline/ui/react'
|
|
@@ -47,6 +48,23 @@ export interface PathWidgetProps {
|
|
|
47
48
|
activeLocale: string
|
|
48
49
|
/** `'create'` shows the live derived preview as placeholder text. */
|
|
49
50
|
mode: 'create' | 'edit'
|
|
51
|
+
/**
|
|
52
|
+
* Installation slugifier used to derive the live preview. Must match the
|
|
53
|
+
* server-side `ServerConfig.slugifier` so the preview agrees with what is
|
|
54
|
+
* persisted. Defaults to the built-in `slugify` when omitted — callers
|
|
55
|
+
* that keep the default slugifier need not pass this.
|
|
56
|
+
*/
|
|
57
|
+
slugifier?: SlugifierFn
|
|
58
|
+
/**
|
|
59
|
+
* When `true`, the `useAsPath` source field's value is not editable through
|
|
60
|
+
* this form (e.g. it is an allocator-assigned `counter`, or an otherwise
|
|
61
|
+
* read-only field). Its value is either server-assigned — and so not
|
|
62
|
+
* reproducible client-side — or simply cannot change, which means the
|
|
63
|
+
* source-derived live preview and the "Regenerate" affordance are
|
|
64
|
+
* meaningless. When set, the widget suppresses both and just shows the
|
|
65
|
+
* persisted path. Defaults to `false`.
|
|
66
|
+
*/
|
|
67
|
+
sourceLocked?: boolean
|
|
50
68
|
}
|
|
51
69
|
|
|
52
70
|
/**
|
|
@@ -71,12 +89,20 @@ export const PathWidget = ({
|
|
|
71
89
|
defaultLocale,
|
|
72
90
|
activeLocale,
|
|
73
91
|
mode,
|
|
92
|
+
slugifier,
|
|
93
|
+
sourceLocked = false,
|
|
74
94
|
}: PathWidgetProps) => {
|
|
75
95
|
const { setSystemPath } = useFormContext()
|
|
76
96
|
const { t } = useTranslation('byline-admin')
|
|
77
97
|
const systemPath = useSystemPath()
|
|
78
98
|
const sourceValue = useFieldValue<unknown>(useAsPath ?? '')
|
|
79
99
|
|
|
100
|
+
// The installation slugifier, or the built-in default. Server-side path
|
|
101
|
+
// derivation uses `ServerConfig.slugifier`; this must be the same function
|
|
102
|
+
// (registered via `ClientConfig.slugifier`) or the preview will disagree
|
|
103
|
+
// with what the server persists.
|
|
104
|
+
const runSlugify = slugifier ?? slugify
|
|
105
|
+
|
|
80
106
|
// Phase 1: paths are written/edited only under the default content
|
|
81
107
|
// locale. When editing a translation, the widget locks down — the
|
|
82
108
|
// input is read-only, the Regenerate action is suppressed, and a
|
|
@@ -87,11 +113,13 @@ export const PathWidget = ({
|
|
|
87
113
|
// field value if no override were set. Used as placeholder in create
|
|
88
114
|
// mode and as the target of the "Regenerate" action.
|
|
89
115
|
const livePreview = useMemo(() => {
|
|
90
|
-
|
|
116
|
+
// A locked source (server-assigned counter / read-only field) can't be
|
|
117
|
+
// previewed or regenerated from the form — suppress the derivation.
|
|
118
|
+
if (!useAsPath || sourceLocked) return ''
|
|
91
119
|
const asString = coerceToString(sourceValue)
|
|
92
120
|
if (asString.length === 0) return ''
|
|
93
|
-
return
|
|
94
|
-
}, [useAsPath, sourceValue, defaultLocale, collectionPath])
|
|
121
|
+
return runSlugify(asString, { locale: defaultLocale, collectionPath })
|
|
122
|
+
}, [useAsPath, sourceLocked, sourceValue, defaultLocale, collectionPath, runSlugify])
|
|
95
123
|
|
|
96
124
|
const inputValue = systemPath ?? ''
|
|
97
125
|
|
|
@@ -115,8 +143,8 @@ export const PathWidget = ({
|
|
|
115
143
|
// field-hook advisory behaviour).
|
|
116
144
|
const formatted = useMemo(() => {
|
|
117
145
|
if (inputValue.length === 0) return ''
|
|
118
|
-
return
|
|
119
|
-
}, [inputValue, defaultLocale, collectionPath])
|
|
146
|
+
return runSlugify(inputValue, { locale: defaultLocale, collectionPath })
|
|
147
|
+
}, [inputValue, defaultLocale, collectionPath, runSlugify])
|
|
120
148
|
|
|
121
149
|
const validationHint =
|
|
122
150
|
inputValue.length > 0 && formatted !== inputValue
|
|
@@ -97,10 +97,10 @@ export async function executeUploads(
|
|
|
97
97
|
/**
|
|
98
98
|
* Extract the leaf field name from a `fieldPath`. Top-level upload
|
|
99
99
|
* fields (`'image'`, `'avatar'`) pass through unchanged; nested paths
|
|
100
|
-
* (`'
|
|
101
|
-
* server-side resolver
|
|
102
|
-
*
|
|
103
|
-
*
|
|
100
|
+
* (`'files[0].filesGroup.publicationFile'`) reduce to their last
|
|
101
|
+
* segment. The server-side resolver walks the schema recursively and
|
|
102
|
+
* matches upload fields by leaf name at any nesting depth, so leaf
|
|
103
|
+
* names must be unique among a collection's upload-capable fields.
|
|
104
104
|
*/
|
|
105
105
|
function uploadFieldName(fieldPath: string): string {
|
|
106
106
|
const dot = fieldPath.lastIndexOf('.')
|