@bison-lab/payload-core 3.24.0 → 3.26.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/README.md +23 -8
- package/dist/admin.d.mts +18 -2
- package/dist/admin.d.mts.map +1 -1
- package/dist/admin.mjs +322 -3
- package/dist/admin.mjs.map +1 -1
- package/dist/index.d.mts +86 -14
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +345 -16
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -8,10 +8,10 @@ are extracted.
|
|
|
8
8
|
Full guide: <https://payload.bisonlab.ai/site/seo/>
|
|
9
9
|
|
|
10
10
|
```bash
|
|
11
|
-
pnpm add @bison-lab/payload-core @payloadcms/plugin-seo @bison-lab/tokens @bison-lab/fonts
|
|
11
|
+
pnpm add @bison-lab/payload-core @payloadcms/plugin-seo @payloadcms/plugin-redirects @bison-lab/tokens @bison-lab/fonts
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
-
Peers: `payload
|
|
14
|
+
Peers: `payload`, `@payloadcms/plugin-seo`, and `@payloadcms/plugin-redirects` are required. `@bison-lab/tokens`
|
|
15
15
|
and `@bison-lab/fonts` are needed to adopt the Theme Global; `@payloadcms/ui`
|
|
16
16
|
and `react` are needed for its admin fields; `@bison-lab/ui` and
|
|
17
17
|
`@payloadcms/live-preview-react` are needed for the live theme preview.
|
|
@@ -22,7 +22,7 @@ lockstep.
|
|
|
22
22
|
|
|
23
23
|
| Import | Contents | Runs where |
|
|
24
24
|
| ---------------------------------- | ------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------- |
|
|
25
|
-
| `@bison-lab/payload-core` | `seoPlugin`, `createTheme`, `createBrandAssets`, `createRoles`, `createFeatures`, `createPages`, `createUsers`, `createMedia`, `createAdminNav`, `adminNav`, `createNavigation`, `adminOnlyApiTab`, `documentTitleActions`, `seedTheme`, `seedRoles`, `seedFeatures`, predicates, `lookField`, `colorTokenField`, import-map strings, title and text helpers, types | Node. What `payload.config.ts` imports; it loads the plugin. |
|
|
25
|
+
| `@bison-lab/payload-core` | `seoPlugin`, `createTheme`, `createBrandAssets`, `createRoles`, `createFeatures`, `createPages`, `createUsers`, `createMedia`, `createRedirects`, `createAdminNav`, `adminNav`, `createNavigation`, `adminOnlyApiTab`, `documentTitleActions`, `seedTheme`, `seedRoles`, `seedFeatures`, predicates, `lookField`, `colorTokenField`, import-map strings, title and text helpers, types | Node. What `payload.config.ts` imports; it loads the plugin. |
|
|
26
26
|
| `@bison-lab/payload-core/metadata` | `pageMetadata`, the title helpers, the same types | Server. What a page route imports; it does not load the plugin. |
|
|
27
27
|
| `@bison-lab/payload-core/theme` | `getPublishedTheme`, `getPublishedIdentity`, `themeConfigFromDoc`, `themeHead`, `themeHeadFromDoc` | Server. What a root layout imports; it does not load the plugin or React. |
|
|
28
28
|
| `@bison-lab/payload-core/navigation` | `getNavigation`, the Header and Footer slugs | Server. What a site header imports; it does not load the plugin or React. |
|
|
@@ -45,11 +45,12 @@ the site spells its name and its URL scheme once.
|
|
|
45
45
|
|
|
46
46
|
```ts
|
|
47
47
|
// payload.config.ts
|
|
48
|
-
import { firstImageIn, seoPlugin } from "@bison-lab/payload-core";
|
|
48
|
+
import { createRedirects, firstImageIn, seoPlugin } from "@bison-lab/payload-core";
|
|
49
49
|
import type { Page } from "@/payload-types";
|
|
50
50
|
|
|
51
51
|
export default buildConfig({
|
|
52
52
|
plugins: [
|
|
53
|
+
createRedirects(),
|
|
53
54
|
seoPlugin<Page>({
|
|
54
55
|
siteName: "Acme Clinics",
|
|
55
56
|
urlFor: (doc) => (doc.slug ? absoluteUrl(pagePath(doc.slug)) : undefined),
|
|
@@ -318,10 +319,23 @@ read from the live Features and Roles Globals) so Payload's
|
|
|
318
319
|
## Pages, users, and media
|
|
319
320
|
|
|
320
321
|
`createPages`, `createUsers`, `createMedia`, `slugField`,
|
|
321
|
-
`adminOnlyApiTab`, and `documentTitleActions` ship from the same
|
|
322
|
+
`createRedirects`, `adminOnlyApiTab`, and `documentTitleActions` ship from the same
|
|
322
323
|
React-free entry. Pages take the
|
|
323
|
-
site's hero and layout blocks, reserved-slug predicate,
|
|
324
|
-
path
|
|
324
|
+
site's hero and layout blocks, reserved-slug predicate, preview
|
|
325
|
+
path, and optional `folderRoots` (`patients`, `providers`, …). Parent
|
|
326
|
+
on the page editor is where the page lives: site root, those roots,
|
|
327
|
+
published CMS pages, folders created by earlier saves, or New folder….
|
|
328
|
+
The address is that folder plus a title leaf until first publish, then
|
|
329
|
+
locks. Layout carries the same standing copy as the hero — a page cannot
|
|
330
|
+
be published without a section — and Publish names that fix instead of
|
|
331
|
+
only `Content → Layout`. Title-follow does not wipe the folder prefix. A taken address is
|
|
332
|
+
refused — never rewritten to `slug-2`. Unlock + a parent change writes a
|
|
333
|
+
308 the same way a typed slug change does. `createRedirects()`
|
|
334
|
+
wraps `@payloadcms/plugin-redirects` for `pages` only (permanent 308) and
|
|
335
|
+
collapses chains when a published slug changes. The plugin stores the
|
|
336
|
+
rows; the site still has to serve them. Pin the plugin to the same
|
|
337
|
+
version as `payload`, then `payload generate:importmap`,
|
|
338
|
+
`payload generate:types`, and `payload migrate:create`. Create follows the Content tick. Update without Publish is
|
|
325
339
|
constrained to `_status: draft` — an Author can save a draft and cannot
|
|
326
340
|
publish or edit a live page. Users sit under Settings next to Roles,
|
|
327
341
|
take `secureCookies`, and offer whatever rows the Global (or the seed
|
|
@@ -418,6 +432,7 @@ to a type that has one.
|
|
|
418
432
|
|
|
419
433
|
`noIndexField`, the plugin's own fields, the Theme Global fields, the
|
|
420
434
|
Roles Global fields, the Admin nav fields, the Header and Footer fields,
|
|
421
|
-
|
|
435
|
+
the Pages slug lock fields, the Redirects collection, and the
|
|
436
|
+
brand-assets collection fields are columns
|
|
422
437
|
in every consuming site. A change to them is a schema change: say "run
|
|
423
438
|
`payload migrate:create`" in the changeset.
|
package/dist/admin.d.mts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { ThemeConfig } from "@bison-lab/tokens";
|
|
3
3
|
import { ReactNode } from "react";
|
|
4
4
|
import * as react_jsx_runtime0 from "react/jsx-runtime";
|
|
5
|
-
import { ArrayFieldClientComponent, GroupFieldClientComponent, JSONFieldClientComponent, SelectFieldClientComponent, TextFieldClientComponent, UIFieldClientComponent } from "payload";
|
|
5
|
+
import { ArrayFieldClientComponent, DefaultCellComponentProps, GroupFieldClientComponent, JSONFieldClientComponent, SelectFieldClientComponent, TextFieldClientComponent, UIFieldClientComponent } from "payload";
|
|
6
6
|
|
|
7
7
|
//#region src/admin/color-field.d.ts
|
|
8
8
|
/**
|
|
@@ -126,6 +126,22 @@ declare function RolesRowLabel(): react_jsx_runtime0.JSX.Element;
|
|
|
126
126
|
*/
|
|
127
127
|
declare const RoleSlugField: TextFieldClientComponent;
|
|
128
128
|
//#endregion
|
|
129
|
+
//#region src/admin/slug-field.d.ts
|
|
130
|
+
/**
|
|
131
|
+
* Parent, new-folder name, composed address, and Unlock. Slug follows the
|
|
132
|
+
* title leaf until first publish; the folder prefix is not wiped. After
|
|
133
|
+
* publish the cluster locks until Unlock, which warns that the old address
|
|
134
|
+
* will redirect.
|
|
135
|
+
*/
|
|
136
|
+
declare const SlugField: TextFieldClientComponent;
|
|
137
|
+
//#endregion
|
|
138
|
+
//#region src/admin/page-title-cell.d.ts
|
|
139
|
+
/**
|
|
140
|
+
* Indent a Pages list title by how deep its slug sits under a parent folder.
|
|
141
|
+
* DefaultCell still draws the document link — a custom Cell replaces that wrap.
|
|
142
|
+
*/
|
|
143
|
+
declare function PageTitleCell(props: DefaultCellComponentProps): react_jsx_runtime0.JSX.Element;
|
|
144
|
+
//#endregion
|
|
129
145
|
//#region src/admin/role-name-field.d.ts
|
|
130
146
|
/**
|
|
131
147
|
* Role name. Letters and spaces only, so the hidden slug stays
|
|
@@ -214,5 +230,5 @@ declare function AdminNavItemRowLabel(): react_jsx_runtime0.JSX.Element;
|
|
|
214
230
|
*/
|
|
215
231
|
declare const AdminNavEntityField: TextFieldClientComponent;
|
|
216
232
|
//#endregion
|
|
217
|
-
export { AdminNav, AdminNavEntityField, AdminNavItemRowLabel, AdminNavRowLabel, AppearanceField, ColorField, ColorScaleField, ContrastReport, DocumentCreateNew, DocumentTitleActions, FeaturesMatrixField, FontField, GreyScaleField, HiddenSaveButton, IdentityFallback, LibraryField, LookField, PairingField, PublishChild, RoleNameField, RoleSlugField, RolesField, RolesGrantsField, RolesMatrixField, RolesRowLabel, SectionHeading, ThemeDocumentControls, ThemeSaveButton, setThemePreviewMode };
|
|
233
|
+
export { AdminNav, AdminNavEntityField, AdminNavItemRowLabel, AdminNavRowLabel, AppearanceField, ColorField, ColorScaleField, ContrastReport, DocumentCreateNew, DocumentTitleActions, FeaturesMatrixField, FontField, GreyScaleField, HiddenSaveButton, IdentityFallback, LibraryField, LookField, PageTitleCell, PairingField, PublishChild, RoleNameField, RoleSlugField, RolesField, RolesGrantsField, RolesMatrixField, RolesRowLabel, SectionHeading, SlugField, ThemeDocumentControls, ThemeSaveButton, setThemePreviewMode };
|
|
218
234
|
//# sourceMappingURL=admin.d.mts.map
|
package/dist/admin.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"admin.d.mts","names":[],"sources":["../src/admin/color-field.tsx","../src/admin/color-scale-field.tsx","../src/admin/library-field.tsx","../src/admin/font-field.tsx","../src/admin/pairing-field.tsx","../src/admin/appearance-field.tsx","../src/admin/grey-scale-field.tsx","../src/admin/contrast-report.tsx","../src/admin/section-heading.tsx","../src/admin/publish-child.tsx","../src/admin/save-button.tsx","../src/admin/preview-mode.tsx","../src/admin/identity-fallback.tsx","../src/admin/look-field.tsx","../src/admin/roles-matrix-field.tsx","../src/admin/roles-row-label.tsx","../src/admin/role-slug-field.tsx","../src/admin/role-name-field.tsx","../src/admin/roles-field.tsx","../src/admin/roles-grants-field.tsx","../src/admin/features-matrix-field.tsx","../src/admin/document-title-actions.tsx","../src/admin/document-create-new.tsx","../src/admin-nav/types.ts","../src/admin/admin-nav.tsx","../src/admin/admin-nav-row-label.tsx","../src/admin/admin-nav-item-row-label.tsx","../src/admin/admin-nav-entity-field.tsx"],"mappings":";;;;;;;;;;;cAWa,UAAA,EAAY,wBAAA;;;;;;;cCsBZ,eAAA,EAAiB,yBAAA;;;;;;;;;cCOjB,YAAA,EAAc,yBAAA;;;;;;;;;cCtBd,SAAA,EAAW,0BAAA;;;;;;;cCNX,YAAA,EAAc,sBAAA;;;;;;;cCCd,eAAA,EAAiB,yBAAA;;;ALF9B;;;;AAAA,cM4Da,cAAA,EAAgB,0BAAA;;;;;;;cCnDhB,cAAA,EAAgB,sBAAA;;;;;;cCbhB,cAAA,EAAgB,sBAAA;;;cC0IhB,YAAA,EAAc,sBAAA;;;;;;;;iBCtIX,qBAAA,CAAA;;AVAhB;;;iBUQgB,eAAA,CAAA,GAAe,kBAAA,CAAA,GAAA,CAAA,OAAA;;iBA0Bf,gBAAA,CAAA,GAAgB,kBAAA,CAAA,GAAA,CAAA,OAAA;;;KCrCpB,gBAAA;AAAA,iBAkCI,mBAAA,CAAoB,IAAA,EAAM,gBAAA;;;;;;;cClC7B,gBAAA,EAAkB,sBAAA;;;;;;;cCIlB,SAAA,EAAW,wBAAA;;;;;;;;cCqBX,gBAAA,EAAkB,yBAAA;;;;;;iBC1Bf,aAAA,CAAA,GAAa,kBAAA,CAAA,GAAA,CAAA,OAAA;;;;;;cCChB,aAAA,EAAe,wBAAA;;;;;;;
|
|
1
|
+
{"version":3,"file":"admin.d.mts","names":[],"sources":["../src/admin/color-field.tsx","../src/admin/color-scale-field.tsx","../src/admin/library-field.tsx","../src/admin/font-field.tsx","../src/admin/pairing-field.tsx","../src/admin/appearance-field.tsx","../src/admin/grey-scale-field.tsx","../src/admin/contrast-report.tsx","../src/admin/section-heading.tsx","../src/admin/publish-child.tsx","../src/admin/save-button.tsx","../src/admin/preview-mode.tsx","../src/admin/identity-fallback.tsx","../src/admin/look-field.tsx","../src/admin/roles-matrix-field.tsx","../src/admin/roles-row-label.tsx","../src/admin/role-slug-field.tsx","../src/admin/slug-field.tsx","../src/admin/page-title-cell.tsx","../src/admin/role-name-field.tsx","../src/admin/roles-field.tsx","../src/admin/roles-grants-field.tsx","../src/admin/features-matrix-field.tsx","../src/admin/document-title-actions.tsx","../src/admin/document-create-new.tsx","../src/admin-nav/types.ts","../src/admin/admin-nav.tsx","../src/admin/admin-nav-row-label.tsx","../src/admin/admin-nav-item-row-label.tsx","../src/admin/admin-nav-entity-field.tsx"],"mappings":";;;;;;;;;;;cAWa,UAAA,EAAY,wBAAA;;;;;;;cCsBZ,eAAA,EAAiB,yBAAA;;;;;;;;;cCOjB,YAAA,EAAc,yBAAA;;;;;;;;;cCtBd,SAAA,EAAW,0BAAA;;;;;;;cCNX,YAAA,EAAc,sBAAA;;;;;;;cCCd,eAAA,EAAiB,yBAAA;;;ALF9B;;;;AAAA,cM4Da,cAAA,EAAgB,0BAAA;;;;;;;cCnDhB,cAAA,EAAgB,sBAAA;;;;;;cCbhB,cAAA,EAAgB,sBAAA;;;cC0IhB,YAAA,EAAc,sBAAA;;;;;;;;iBCtIX,qBAAA,CAAA;;AVAhB;;;iBUQgB,eAAA,CAAA,GAAe,kBAAA,CAAA,GAAA,CAAA,OAAA;;iBA0Bf,gBAAA,CAAA,GAAgB,kBAAA,CAAA,GAAA,CAAA,OAAA;;;KCrCpB,gBAAA;AAAA,iBAkCI,mBAAA,CAAoB,IAAA,EAAM,gBAAA;;;;;;;cClC7B,gBAAA,EAAkB,sBAAA;;;;;;;cCIlB,SAAA,EAAW,wBAAA;;;;;;;;cCqBX,gBAAA,EAAkB,yBAAA;;;;;;iBC1Bf,aAAA,CAAA,GAAa,kBAAA,CAAA,GAAA,CAAA,OAAA;;;;;;cCChB,aAAA,EAAe,wBAAA;;;;;;;;;cC2Bf,SAAA,EAAW,wBAAA;;;;;;;iBC1BR,aAAA,CAAc,KAAA,EAAO,yBAAA,GAAyB,kBAAA,CAAA,GAAA,CAAA,OAAA;;;;;;;cCAjD,aAAA,EAAe,wBAAA;;;;;;;;cC8Cf,UAAA,EAAY,0BAAA;;;;;;;;cChCZ,gBAAA,EAAkB,wBAAA;;;;;;;;;ArBZ/B;;csB0Ca,mBAAA,EAAqB,yBAAA;;;iBCflB,oBAAA,CAAA;EAAuB;AAAA;EAAc,QAAA,GAAW,SAAA;AAAA,IAAW,kBAAA,CAAA,GAAA,CAAA,OAAA;;;iBCxB3D,iBAAA,CAAA;EAAoB;AAAA;EAAU,IAAA;AAAA,IAAc,kBAAA,CAAA,GAAA,CAAA,OAAA;;;UCmB3C,uBAAA;EACf,WAAA;EACA,OAAA;AAAA;;;UC5Be,aAAA;EACf,eAAA,GAAkB,uBAAA;AAAA;;;A1BGpB;;;iB0B6CgB,QAAA,CAAA;EAAW;AAAA,GAAmB,aAAA,GAAa,kBAAA,CAAA,GAAA,CAAA,OAAA;;;;;;iBCnD3C,gBAAA,CAAA,GAAgB,kBAAA,CAAA,GAAA,CAAA,OAAA;;;;;;;iBCGhB,oBAAA,CAAA,GAAoB,kBAAA,CAAA,GAAA,CAAA,OAAA;;;;;;;;cCEvB,mBAAA,EAAqB,wBAAA"}
|
package/dist/admin.mjs
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { a as colorDocFromState, i as SYSTEM_COLOR_KEYS, n as pageEditorLooks, r as pageEditorTokens, s as stateFromColorDoc } from "./looks-BbL309kO.mjs";
|
|
3
|
-
import { ArrayField, Button, CheckboxInput, FieldDescription, FieldError, FieldLabel, Link, Logout, NavGroup, NavToggler, SelectField, fieldBaseClass, useAuth, useConfig, useField, useForm, useFormFields, useNav, useRowLabel } from "@payloadcms/ui";
|
|
3
|
+
import { ArrayField, Button, CheckboxInput, DefaultCell, FieldDescription, FieldError, FieldLabel, Link, Logout, NavGroup, NavToggler, SelectField, fieldBaseClass, useAuth, useConfig, useField, useForm, useFormFields, useNav, useRowLabel } from "@payloadcms/ui";
|
|
4
4
|
import { GREY_SCALES, SHADE_STEPS, contrastForeground, contrastRatio, createColorScale, deriveDarkPalette, deriveLightPalette, hexToHSL, hslToString, includedShadeSteps, isThemeHex, presetHints, setColorScaleInclude } from "@bison-lab/tokens";
|
|
5
5
|
import { useCallback, useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
6
6
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
7
|
-
import { Badge, Dialog, Field, Input, KitProvider, Label, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@bison-lab/admin-ui";
|
|
7
|
+
import { Badge, Button as Button$1, Dialog, Field, Input, KitProvider, Label, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@bison-lab/admin-ui";
|
|
8
8
|
import { HexColorPicker } from "react-colorful";
|
|
9
9
|
import { createPortal } from "react-dom";
|
|
10
10
|
import { findFont, fontFaceCss, isFontId } from "@bison-lab/fonts";
|
|
11
|
+
import "payload";
|
|
11
12
|
//#region src/admin/color-field.tsx
|
|
12
13
|
const HEX_ERROR = "Enter a six-digit hex colour like #1e3a5f";
|
|
13
14
|
/**
|
|
@@ -3004,6 +3005,324 @@ const RoleSlugField = ({ field, path, readOnly }) => {
|
|
|
3004
3005
|
});
|
|
3005
3006
|
};
|
|
3006
3007
|
//#endregion
|
|
3008
|
+
//#region src/fields/slug.ts
|
|
3009
|
+
/** Sentinel the Parent control writes while the editor is naming a new folder. */
|
|
3010
|
+
const NEW_FOLDER_PARENT = "__new__";
|
|
3011
|
+
const SLUG_UNLOCK_WARNING = "This page is live. Changing the address will send visitors from the old URL to the new one.";
|
|
3012
|
+
function slugifySegment(segment) {
|
|
3013
|
+
return segment.normalize("NFKD").replace(/\p{M}/gu, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
3014
|
+
}
|
|
3015
|
+
/**
|
|
3016
|
+
* A stored slug from whatever was typed: NFKD, hyphenated, slash segments
|
|
3017
|
+
* kept, so `Café / About Us` becomes `cafe/about-us`.
|
|
3018
|
+
*/
|
|
3019
|
+
function normalizeSlug(value) {
|
|
3020
|
+
return value.split("/").map(slugifySegment).filter(Boolean).join("/");
|
|
3021
|
+
}
|
|
3022
|
+
/** The folder prefix Parent stores. `__new__` resolves through `newFolder`. */
|
|
3023
|
+
function parentPrefix(parent, newFolder) {
|
|
3024
|
+
if (parent === "__new__") return normalizeSlug(newFolder ?? "");
|
|
3025
|
+
return typeof parent === "string" ? normalizeSlug(parent) : "";
|
|
3026
|
+
}
|
|
3027
|
+
/**
|
|
3028
|
+
* Address from a parent prefix and a title (or an existing leaf). Title-follow
|
|
3029
|
+
* uses this so a folder is not wiped when the title changes.
|
|
3030
|
+
*/
|
|
3031
|
+
function composeSlug(parent, title, newFolder) {
|
|
3032
|
+
const prefix = parentPrefix(parent, newFolder);
|
|
3033
|
+
const leaf = slugifySegment(title);
|
|
3034
|
+
return prefix ? `${prefix}/${leaf}` : leaf;
|
|
3035
|
+
}
|
|
3036
|
+
/** Prefixes that exist only because a child slug lives under them. */
|
|
3037
|
+
function inferredFolders(slugs) {
|
|
3038
|
+
const folders = /* @__PURE__ */ new Set();
|
|
3039
|
+
for (const slug of slugs) {
|
|
3040
|
+
const parts = normalizeSlug(slug).split("/");
|
|
3041
|
+
for (let i = 1; i < parts.length; i += 1) folders.add(parts.slice(0, i).join("/"));
|
|
3042
|
+
}
|
|
3043
|
+
return [...folders].sort();
|
|
3044
|
+
}
|
|
3045
|
+
/** Slash count after the first segment — how far to indent a list row. */
|
|
3046
|
+
function slugDepth(slug) {
|
|
3047
|
+
const normalized = normalizeSlug(slug);
|
|
3048
|
+
if (!normalized) return 0;
|
|
3049
|
+
return normalized.split("/").length - 1;
|
|
3050
|
+
}
|
|
3051
|
+
//#endregion
|
|
3052
|
+
//#region src/admin/slug-field.tsx
|
|
3053
|
+
const SEARCH_DELAY_MS = 200;
|
|
3054
|
+
const SEARCH_LIMIT = 8;
|
|
3055
|
+
/**
|
|
3056
|
+
* Parent, new-folder name, composed address, and Unlock. Slug follows the
|
|
3057
|
+
* title leaf until first publish; the folder prefix is not wiped. After
|
|
3058
|
+
* publish the cluster locks until Unlock, which warns that the old address
|
|
3059
|
+
* will redirect.
|
|
3060
|
+
*/
|
|
3061
|
+
const SlugField = ({ field, path, readOnly }) => {
|
|
3062
|
+
const slug = useField({ path });
|
|
3063
|
+
const parent = useField({ path: "parent" });
|
|
3064
|
+
const newFolder = useField({ path: "newFolder" });
|
|
3065
|
+
const unlock = useField({ path: "slugUnlock" });
|
|
3066
|
+
const title = useFormFields(([fields]) => fields.title?.value);
|
|
3067
|
+
const slugLocked = Boolean(useFormFields(([fields]) => fields.slugLocked?.value));
|
|
3068
|
+
const locked = Boolean(readOnly) || slugLocked && !unlock.value;
|
|
3069
|
+
const folderRoots = useMemo(() => {
|
|
3070
|
+
return ((field.admin?.custom ?? {}).folderRoots ?? []).map(normalizeSlug).filter(Boolean);
|
|
3071
|
+
}, [field.admin?.custom]);
|
|
3072
|
+
const { config } = useConfig();
|
|
3073
|
+
const collection = "pages";
|
|
3074
|
+
const titleKey = config.collections.find((entry) => entry.slug === collection)?.admin?.useAsTitle ?? "title";
|
|
3075
|
+
const endpoint = `${config.serverURL}${config.routes.api}/${collection}`;
|
|
3076
|
+
const id = useId();
|
|
3077
|
+
const parentInputId = `${id}-parent`;
|
|
3078
|
+
const folderInputId = `${id}-folder`;
|
|
3079
|
+
const addressId = `${id}-address`;
|
|
3080
|
+
const listId = `${id}-list`;
|
|
3081
|
+
const [query, setQuery] = useState("");
|
|
3082
|
+
const [open, setOpen] = useState(false);
|
|
3083
|
+
const [results, setResults] = useState([]);
|
|
3084
|
+
const [allSlugs, setAllSlugs] = useState([]);
|
|
3085
|
+
const [active, setActive] = useState(-1);
|
|
3086
|
+
const creatingFolder = parent.value === NEW_FOLDER_PARENT;
|
|
3087
|
+
const setSlug = slug.setValue;
|
|
3088
|
+
const slugValue = slug.value ?? "";
|
|
3089
|
+
useEffect(() => {
|
|
3090
|
+
if (slugLocked || typeof title !== "string") return;
|
|
3091
|
+
const next = composeSlug(parent.value, title, newFolder.value);
|
|
3092
|
+
if (next !== slugValue) setSlug(next);
|
|
3093
|
+
}, [
|
|
3094
|
+
newFolder.value,
|
|
3095
|
+
parent.value,
|
|
3096
|
+
setSlug,
|
|
3097
|
+
slugLocked,
|
|
3098
|
+
slugValue,
|
|
3099
|
+
title
|
|
3100
|
+
]);
|
|
3101
|
+
useEffect(() => {
|
|
3102
|
+
if (!open) return;
|
|
3103
|
+
const seq = { cancelled: false };
|
|
3104
|
+
const timer = setTimeout(() => {
|
|
3105
|
+
const params = new URLSearchParams({
|
|
3106
|
+
depth: "0",
|
|
3107
|
+
limit: String(SEARCH_LIMIT),
|
|
3108
|
+
sort: titleKey,
|
|
3109
|
+
"where[_status][equals]": "published"
|
|
3110
|
+
});
|
|
3111
|
+
const trimmed = query.trim();
|
|
3112
|
+
if (trimmed) params.set(`where[${titleKey}][like]`, trimmed);
|
|
3113
|
+
fetch(`${endpoint}?${params.toString()}`, { credentials: "include" }).then((response) => response.ok ? response.json() : { docs: [] }).then((body) => {
|
|
3114
|
+
if (seq.cancelled) return;
|
|
3115
|
+
setResults(body.docs ?? []);
|
|
3116
|
+
}).catch(() => {
|
|
3117
|
+
if (!seq.cancelled) setResults([]);
|
|
3118
|
+
});
|
|
3119
|
+
}, SEARCH_DELAY_MS);
|
|
3120
|
+
return () => {
|
|
3121
|
+
seq.cancelled = true;
|
|
3122
|
+
clearTimeout(timer);
|
|
3123
|
+
};
|
|
3124
|
+
}, [
|
|
3125
|
+
endpoint,
|
|
3126
|
+
open,
|
|
3127
|
+
query,
|
|
3128
|
+
titleKey
|
|
3129
|
+
]);
|
|
3130
|
+
useEffect(() => {
|
|
3131
|
+
let cancelled = false;
|
|
3132
|
+
fetch(`${endpoint}?depth=0&limit=100&sort=slug`, { credentials: "include" }).then((response) => response.ok ? response.json() : { docs: [] }).then((body) => {
|
|
3133
|
+
if (cancelled) return;
|
|
3134
|
+
setAllSlugs((body.docs ?? []).flatMap((doc) => doc.slug ? [doc.slug] : []));
|
|
3135
|
+
}).catch(() => {
|
|
3136
|
+
if (!cancelled) setAllSlugs([]);
|
|
3137
|
+
});
|
|
3138
|
+
return () => {
|
|
3139
|
+
cancelled = true;
|
|
3140
|
+
};
|
|
3141
|
+
}, [endpoint]);
|
|
3142
|
+
const folders = useMemo(() => {
|
|
3143
|
+
const inferred = inferredFolders(allSlugs);
|
|
3144
|
+
return [...new Set([...folderRoots, ...inferred])].sort();
|
|
3145
|
+
}, [allSlugs, folderRoots]);
|
|
3146
|
+
const options = useMemo(() => {
|
|
3147
|
+
const trimmed = query.trim().toLowerCase();
|
|
3148
|
+
const matches = (label, path) => !trimmed || label.toLowerCase().includes(trimmed) || (path ?? "").toLowerCase().includes(trimmed);
|
|
3149
|
+
const list = [{
|
|
3150
|
+
kind: "root",
|
|
3151
|
+
id: "root",
|
|
3152
|
+
label: "Site root"
|
|
3153
|
+
}];
|
|
3154
|
+
for (const folder of folders) if (matches(folder, folder)) list.push({
|
|
3155
|
+
kind: "folder",
|
|
3156
|
+
id: folder,
|
|
3157
|
+
label: folder,
|
|
3158
|
+
path: `/${folder}`
|
|
3159
|
+
});
|
|
3160
|
+
for (const doc of results) {
|
|
3161
|
+
const label = String(doc[titleKey] ?? doc.id);
|
|
3162
|
+
const path = doc.slug ? `/${doc.slug}` : "";
|
|
3163
|
+
if (matches(label, path)) list.push({
|
|
3164
|
+
kind: "page",
|
|
3165
|
+
id: `page:${doc.id}`,
|
|
3166
|
+
label,
|
|
3167
|
+
path,
|
|
3168
|
+
page: doc
|
|
3169
|
+
});
|
|
3170
|
+
}
|
|
3171
|
+
list.push({
|
|
3172
|
+
kind: "new",
|
|
3173
|
+
id: NEW_FOLDER_PARENT,
|
|
3174
|
+
label: "New folder…"
|
|
3175
|
+
});
|
|
3176
|
+
return list;
|
|
3177
|
+
}, [
|
|
3178
|
+
folders,
|
|
3179
|
+
query,
|
|
3180
|
+
results,
|
|
3181
|
+
titleKey
|
|
3182
|
+
]);
|
|
3183
|
+
function choose(option) {
|
|
3184
|
+
if (option.kind === "root") {
|
|
3185
|
+
parent.setValue("");
|
|
3186
|
+
newFolder.setValue("");
|
|
3187
|
+
} else if (option.kind === "folder") {
|
|
3188
|
+
parent.setValue(option.id);
|
|
3189
|
+
newFolder.setValue("");
|
|
3190
|
+
} else if (option.kind === "page") {
|
|
3191
|
+
parent.setValue(option.page.slug ?? "");
|
|
3192
|
+
newFolder.setValue("");
|
|
3193
|
+
} else parent.setValue(NEW_FOLDER_PARENT);
|
|
3194
|
+
setQuery("");
|
|
3195
|
+
setOpen(false);
|
|
3196
|
+
setActive(-1);
|
|
3197
|
+
}
|
|
3198
|
+
function onKeyDown(event) {
|
|
3199
|
+
if (event.key === "ArrowDown") {
|
|
3200
|
+
event.preventDefault();
|
|
3201
|
+
if (!open) {
|
|
3202
|
+
setOpen(true);
|
|
3203
|
+
return;
|
|
3204
|
+
}
|
|
3205
|
+
setActive((index) => options.length ? (index + 1) % options.length : -1);
|
|
3206
|
+
} else if (event.key === "ArrowUp") {
|
|
3207
|
+
event.preventDefault();
|
|
3208
|
+
setActive((index) => options.length ? index <= 0 ? options.length - 1 : index - 1 : -1);
|
|
3209
|
+
} else if (event.key === "Enter") {
|
|
3210
|
+
if (!open) return;
|
|
3211
|
+
event.preventDefault();
|
|
3212
|
+
const pick = options[active] ?? options[0];
|
|
3213
|
+
if (pick) choose(pick);
|
|
3214
|
+
} else if (event.key === "Escape") {
|
|
3215
|
+
if (!open) return;
|
|
3216
|
+
event.preventDefault();
|
|
3217
|
+
setOpen(false);
|
|
3218
|
+
setActive(-1);
|
|
3219
|
+
}
|
|
3220
|
+
}
|
|
3221
|
+
const selectedPage = options.find((option) => option.kind === "page" && option.page.slug === parent.value);
|
|
3222
|
+
const selectedFolder = parent.value && parent.value !== "__new__" ? parent.value : "";
|
|
3223
|
+
const parentChip = creatingFolder ? "New folder" : selectedPage ? selectedPage.label : selectedFolder ? selectedFolder : "Site root";
|
|
3224
|
+
const parentKind = creatingFolder ? "Folder" : selectedPage ? "Page" : selectedFolder ? "Folder" : "Root";
|
|
3225
|
+
const address = slug.value ? `/${slug.value}` : "/";
|
|
3226
|
+
const activeId = active >= 0 && open ? `${id}-option-${active}` : void 0;
|
|
3227
|
+
return /* @__PURE__ */ jsx(KitProvider, { children: /* @__PURE__ */ jsxs("div", {
|
|
3228
|
+
className: "bl-page-address",
|
|
3229
|
+
children: [
|
|
3230
|
+
/* @__PURE__ */ jsxs(Field, { children: [/* @__PURE__ */ jsx(Label, { children: "Parent" }), /* @__PURE__ */ jsx("div", {
|
|
3231
|
+
className: "bl-nav-kit__picker",
|
|
3232
|
+
children: locked ? /* @__PURE__ */ jsxs("div", {
|
|
3233
|
+
className: "bl-nav-kit__picker-control",
|
|
3234
|
+
children: [/* @__PURE__ */ jsx(Badge, {
|
|
3235
|
+
variant: "neutral-subtle",
|
|
3236
|
+
children: parentKind
|
|
3237
|
+
}), /* @__PURE__ */ jsx("span", { children: parentChip })]
|
|
3238
|
+
}) : /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Input, {
|
|
3239
|
+
id: parentInputId,
|
|
3240
|
+
role: "combobox",
|
|
3241
|
+
"aria-label": "Parent",
|
|
3242
|
+
placeholder: "Site root, a folder, or a published page",
|
|
3243
|
+
autoComplete: "off",
|
|
3244
|
+
value: open ? query : parentChip,
|
|
3245
|
+
"aria-expanded": open,
|
|
3246
|
+
"aria-controls": listId,
|
|
3247
|
+
"aria-autocomplete": "list",
|
|
3248
|
+
"aria-activedescendant": activeId,
|
|
3249
|
+
onChange: (event) => {
|
|
3250
|
+
setQuery(event.target.value);
|
|
3251
|
+
setActive(-1);
|
|
3252
|
+
setOpen(true);
|
|
3253
|
+
},
|
|
3254
|
+
onFocus: () => setOpen(true),
|
|
3255
|
+
onBlur: () => setOpen(false),
|
|
3256
|
+
onKeyDown
|
|
3257
|
+
}), open ? /* @__PURE__ */ jsx("ul", {
|
|
3258
|
+
id: listId,
|
|
3259
|
+
role: "listbox",
|
|
3260
|
+
className: "bl-nav-kit__picker-menu",
|
|
3261
|
+
children: options.map((option, index) => /* @__PURE__ */ jsxs("li", {
|
|
3262
|
+
id: `${id}-option-${index}`,
|
|
3263
|
+
role: "option",
|
|
3264
|
+
"aria-selected": index === active,
|
|
3265
|
+
className: "bl-nav-kit__picker-option",
|
|
3266
|
+
onMouseDown: (event) => {
|
|
3267
|
+
event.preventDefault();
|
|
3268
|
+
choose(option);
|
|
3269
|
+
},
|
|
3270
|
+
onMouseEnter: () => setActive(index),
|
|
3271
|
+
children: [/* @__PURE__ */ jsx("span", { children: option.label }), "path" in option && option.path ? /* @__PURE__ */ jsx("span", {
|
|
3272
|
+
className: "bl-nav-kit__picker-path",
|
|
3273
|
+
children: option.path
|
|
3274
|
+
}) : null]
|
|
3275
|
+
}, option.id))
|
|
3276
|
+
}) : null] })
|
|
3277
|
+
})] }),
|
|
3278
|
+
creatingFolder && !locked ? /* @__PURE__ */ jsxs(Field, { children: [/* @__PURE__ */ jsx(Label, { children: "New folder name" }), /* @__PURE__ */ jsx(Input, {
|
|
3279
|
+
id: folderInputId,
|
|
3280
|
+
"aria-label": "New folder name",
|
|
3281
|
+
value: newFolder.value ?? "",
|
|
3282
|
+
autoComplete: "off",
|
|
3283
|
+
onChange: (event) => newFolder.setValue(event.target.value)
|
|
3284
|
+
})] }) : null,
|
|
3285
|
+
/* @__PURE__ */ jsxs(Field, { children: [
|
|
3286
|
+
/* @__PURE__ */ jsx(Label, { children: "Address" }),
|
|
3287
|
+
/* @__PURE__ */ jsxs("div", {
|
|
3288
|
+
className: "bl-nav-kit__picker-address",
|
|
3289
|
+
children: [/* @__PURE__ */ jsx(Input, {
|
|
3290
|
+
id: addressId,
|
|
3291
|
+
readOnly: true,
|
|
3292
|
+
disabled: locked,
|
|
3293
|
+
value: address,
|
|
3294
|
+
autoComplete: "off",
|
|
3295
|
+
spellCheck: false
|
|
3296
|
+
}), slugLocked && !unlock.value ? /* @__PURE__ */ jsx(Button$1, {
|
|
3297
|
+
variant: "outline",
|
|
3298
|
+
onClick: () => unlock.setValue(true),
|
|
3299
|
+
children: "Unlock"
|
|
3300
|
+
}) : null]
|
|
3301
|
+
}),
|
|
3302
|
+
unlock.value ? /* @__PURE__ */ jsx("p", { children: SLUG_UNLOCK_WARNING }) : null,
|
|
3303
|
+
/* @__PURE__ */ jsx(FieldError, {
|
|
3304
|
+
message: slug.errorMessage,
|
|
3305
|
+
path,
|
|
3306
|
+
showError: slug.showError
|
|
3307
|
+
})
|
|
3308
|
+
] })
|
|
3309
|
+
]
|
|
3310
|
+
}) });
|
|
3311
|
+
};
|
|
3312
|
+
//#endregion
|
|
3313
|
+
//#region src/admin/page-title-cell.tsx
|
|
3314
|
+
/**
|
|
3315
|
+
* Indent a Pages list title by how deep its slug sits under a parent folder.
|
|
3316
|
+
* DefaultCell still draws the document link — a custom Cell replaces that wrap.
|
|
3317
|
+
*/
|
|
3318
|
+
function PageTitleCell(props) {
|
|
3319
|
+
return /* @__PURE__ */ jsx("span", {
|
|
3320
|
+
className: "bl-page-title-cell",
|
|
3321
|
+
style: { paddingInlineStart: `${slugDepth(typeof props.rowData?.slug === "string" ? props.rowData.slug : "") * 1.25}rem` },
|
|
3322
|
+
children: /* @__PURE__ */ jsx(DefaultCell, { ...props })
|
|
3323
|
+
});
|
|
3324
|
+
}
|
|
3325
|
+
//#endregion
|
|
3007
3326
|
//#region src/admin/role-name-field.tsx
|
|
3008
3327
|
/**
|
|
3009
3328
|
* Role name. Letters and spaces only, so the hidden slug stays
|
|
@@ -3743,6 +4062,6 @@ const AdminNavEntityField = ({ field, path, permissions, readOnly, schemaPath })
|
|
|
3743
4062
|
});
|
|
3744
4063
|
};
|
|
3745
4064
|
//#endregion
|
|
3746
|
-
export { AdminNav, AdminNavEntityField, AdminNavItemRowLabel, AdminNavRowLabel, AppearanceField, ColorField, ColorScaleField, ContrastReport, DocumentCreateNew, DocumentTitleActions, FeaturesMatrixField, FontField, GreyScaleField, HiddenSaveButton, IdentityFallback, LibraryField, LookField, PairingField, PublishChild, RoleNameField, RoleSlugField, RolesField, RolesGrantsField, RolesMatrixField, RolesRowLabel, SectionHeading, ThemeDocumentControls, ThemeSaveButton, setThemePreviewMode };
|
|
4065
|
+
export { AdminNav, AdminNavEntityField, AdminNavItemRowLabel, AdminNavRowLabel, AppearanceField, ColorField, ColorScaleField, ContrastReport, DocumentCreateNew, DocumentTitleActions, FeaturesMatrixField, FontField, GreyScaleField, HiddenSaveButton, IdentityFallback, LibraryField, LookField, PageTitleCell, PairingField, PublishChild, RoleNameField, RoleSlugField, RolesField, RolesGrantsField, RolesMatrixField, RolesRowLabel, SectionHeading, SlugField, ThemeDocumentControls, ThemeSaveButton, setThemePreviewMode };
|
|
3747
4066
|
|
|
3748
4067
|
//# sourceMappingURL=admin.mjs.map
|