@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/dist/index.mjs
CHANGED
|
@@ -6,6 +6,7 @@ import { catalog } from "@bison-lab/fonts";
|
|
|
6
6
|
import { DESTRUCTIVE_SCALE_HEX, SHADE_STEPS, presetHints } from "@bison-lab/tokens";
|
|
7
7
|
import { APIError, Forbidden, ValidationError, definePlugin, validations } from "payload";
|
|
8
8
|
import { select, text } from "payload/shared";
|
|
9
|
+
import { redirectsPlugin } from "@payloadcms/plugin-redirects";
|
|
9
10
|
//#region src/seo/fields.ts
|
|
10
11
|
/**
|
|
11
12
|
* The index switch, last in the SEO tab. Off by default: a page is public
|
|
@@ -1753,13 +1754,98 @@ async function seedFeatures(payload, extras = []) {
|
|
|
1753
1754
|
}
|
|
1754
1755
|
//#endregion
|
|
1755
1756
|
//#region src/fields/slug.ts
|
|
1757
|
+
const SLUG_FIELD = "@bison-lab/payload-core/admin#SlugField";
|
|
1758
|
+
const PAGE_TITLE_CELL = "@bison-lab/payload-core/admin#PageTitleCell";
|
|
1759
|
+
/** Sentinel the Parent control writes while the editor is naming a new folder. */
|
|
1760
|
+
const NEW_FOLDER_PARENT = "__new__";
|
|
1761
|
+
const EXISTING_PARENT_MESSAGE = "That folder is already in the list. Pick it from the list.";
|
|
1762
|
+
const SLUG_UNLOCK_WARNING = "This page is live. Changing the address will send visitors from the old URL to the new one.";
|
|
1763
|
+
async function allSlugs({ collection, id, req }) {
|
|
1764
|
+
if (typeof req?.payload?.find !== "function") return [];
|
|
1765
|
+
const where = {};
|
|
1766
|
+
if (id !== void 0) where.id = { not_equals: id };
|
|
1767
|
+
const query = {
|
|
1768
|
+
collection,
|
|
1769
|
+
depth: 0,
|
|
1770
|
+
limit: 500,
|
|
1771
|
+
overrideAccess: true,
|
|
1772
|
+
pagination: false,
|
|
1773
|
+
req,
|
|
1774
|
+
where
|
|
1775
|
+
};
|
|
1776
|
+
const [live, drafts] = await Promise.all([req.payload.find(query), req.payload.find({
|
|
1777
|
+
...query,
|
|
1778
|
+
draft: true
|
|
1779
|
+
})]);
|
|
1780
|
+
return [...live.docs, ...drafts.docs].flatMap((doc) => typeof doc.slug === "string" && doc.slug ? [doc.slug] : []);
|
|
1781
|
+
}
|
|
1782
|
+
async function existingParents(prefix, args) {
|
|
1783
|
+
if (new Set((args.folderRoots ?? []).map(normalizeSlug).filter(Boolean)).has(prefix)) return true;
|
|
1784
|
+
const slugs = await allSlugs(args);
|
|
1785
|
+
if (slugs.includes(prefix)) return true;
|
|
1786
|
+
return inferredFolders(slugs).includes(prefix);
|
|
1787
|
+
}
|
|
1788
|
+
function slugifySegment(segment) {
|
|
1789
|
+
return segment.normalize("NFKD").replace(/\p{M}/gu, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
1790
|
+
}
|
|
1756
1791
|
/**
|
|
1757
|
-
* A stored slug from whatever was typed:
|
|
1758
|
-
*
|
|
1759
|
-
* does not keep the inner spaces a trim-then-strip would leave.
|
|
1792
|
+
* A stored slug from whatever was typed: NFKD, hyphenated, slash segments
|
|
1793
|
+
* kept, so `Café / About Us` becomes `cafe/about-us`.
|
|
1760
1794
|
*/
|
|
1761
1795
|
function normalizeSlug(value) {
|
|
1762
|
-
return value.
|
|
1796
|
+
return value.split("/").map(slugifySegment).filter(Boolean).join("/");
|
|
1797
|
+
}
|
|
1798
|
+
/** The folder prefix Parent stores. `__new__` resolves through `newFolder`. */
|
|
1799
|
+
function parentPrefix(parent, newFolder) {
|
|
1800
|
+
if (parent === "__new__") return normalizeSlug(newFolder ?? "");
|
|
1801
|
+
return typeof parent === "string" ? normalizeSlug(parent) : "";
|
|
1802
|
+
}
|
|
1803
|
+
/** Last segment of a stored slug — the title leaf once a folder is set. */
|
|
1804
|
+
function slugLeaf(slug) {
|
|
1805
|
+
const parts = normalizeSlug(slug).split("/");
|
|
1806
|
+
return parts[parts.length - 1] ?? "";
|
|
1807
|
+
}
|
|
1808
|
+
/**
|
|
1809
|
+
* Address from a parent prefix and a title (or an existing leaf). Title-follow
|
|
1810
|
+
* uses this so a folder is not wiped when the title changes.
|
|
1811
|
+
*/
|
|
1812
|
+
function composeSlug(parent, title, newFolder) {
|
|
1813
|
+
const prefix = parentPrefix(parent, newFolder);
|
|
1814
|
+
const leaf = slugifySegment(title);
|
|
1815
|
+
return prefix ? `${prefix}/${leaf}` : leaf;
|
|
1816
|
+
}
|
|
1817
|
+
/** Prefixes that exist only because a child slug lives under them. */
|
|
1818
|
+
function inferredFolders(slugs) {
|
|
1819
|
+
const folders = /* @__PURE__ */ new Set();
|
|
1820
|
+
for (const slug of slugs) {
|
|
1821
|
+
const parts = normalizeSlug(slug).split("/");
|
|
1822
|
+
for (let i = 1; i < parts.length; i += 1) folders.add(parts.slice(0, i).join("/"));
|
|
1823
|
+
}
|
|
1824
|
+
return [...folders].sort();
|
|
1825
|
+
}
|
|
1826
|
+
/** Slash count after the first segment — how far to indent a list row. */
|
|
1827
|
+
function slugDepth(slug) {
|
|
1828
|
+
const normalized = normalizeSlug(slug);
|
|
1829
|
+
if (!normalized) return 0;
|
|
1830
|
+
return normalized.split("/").length - 1;
|
|
1831
|
+
}
|
|
1832
|
+
function resolvePrefix(...sources) {
|
|
1833
|
+
let parent;
|
|
1834
|
+
let newFolder;
|
|
1835
|
+
for (const source of sources) {
|
|
1836
|
+
if (parent === void 0 && typeof source?.parent === "string") parent = source.parent;
|
|
1837
|
+
if (newFolder === void 0 && typeof source?.newFolder === "string") newFolder = source.newFolder;
|
|
1838
|
+
}
|
|
1839
|
+
return {
|
|
1840
|
+
parent,
|
|
1841
|
+
newFolder
|
|
1842
|
+
};
|
|
1843
|
+
}
|
|
1844
|
+
function isLockedDoc(doc) {
|
|
1845
|
+
return Boolean(doc?.slugLocked || doc?.publishedSlug);
|
|
1846
|
+
}
|
|
1847
|
+
function isUnlocking(data) {
|
|
1848
|
+
return Boolean(data?.slugUnlock);
|
|
1763
1849
|
}
|
|
1764
1850
|
async function slugProblem(slug, { collection, id, isReserved, req }) {
|
|
1765
1851
|
if (!slug) return void 0;
|
|
@@ -1781,13 +1867,52 @@ async function slugProblem(slug, { collection, id, isReserved, req }) {
|
|
|
1781
1867
|
draft: true
|
|
1782
1868
|
})).docs.length > 0 ? `Another page is already using "/${slug}". Please choose a different address.` : void 0;
|
|
1783
1869
|
}
|
|
1870
|
+
const slugUpdateAccess = (collection) => async ({ doc, id, req, siblingData }) => {
|
|
1871
|
+
if (isUnlocking(siblingData)) return true;
|
|
1872
|
+
if (isLockedDoc(doc)) return false;
|
|
1873
|
+
if (id === void 0 || typeof req?.payload?.findByID !== "function") return true;
|
|
1874
|
+
try {
|
|
1875
|
+
return (await req.payload.findByID({
|
|
1876
|
+
collection,
|
|
1877
|
+
id,
|
|
1878
|
+
depth: 0,
|
|
1879
|
+
draft: false,
|
|
1880
|
+
overrideAccess: true,
|
|
1881
|
+
req
|
|
1882
|
+
}))?._status !== "published";
|
|
1883
|
+
} catch {
|
|
1884
|
+
return true;
|
|
1885
|
+
}
|
|
1886
|
+
};
|
|
1887
|
+
/**
|
|
1888
|
+
* Locks the slug on first publish and remembers the live path so a later
|
|
1889
|
+
* change can write a redirect. Unlock is a one-save flag and is cleared here.
|
|
1890
|
+
*/
|
|
1891
|
+
const lockSlugOnPublish = ({ data }) => {
|
|
1892
|
+
if (!data) return data;
|
|
1893
|
+
data.slugUnlock = false;
|
|
1894
|
+
if (data._status === "published") {
|
|
1895
|
+
data.slugLocked = true;
|
|
1896
|
+
if (typeof data.slug === "string") data.publishedSlug = data.slug;
|
|
1897
|
+
}
|
|
1898
|
+
return data;
|
|
1899
|
+
};
|
|
1900
|
+
function hiddenCheckbox(name, label) {
|
|
1901
|
+
return {
|
|
1902
|
+
name,
|
|
1903
|
+
type: "checkbox",
|
|
1904
|
+
label,
|
|
1905
|
+
defaultValue: false,
|
|
1906
|
+
admin: { hidden: true }
|
|
1907
|
+
};
|
|
1908
|
+
}
|
|
1784
1909
|
/**
|
|
1785
1910
|
* The path a document is published at, normalised on the way in. Unique
|
|
1786
|
-
* across the collection. `
|
|
1787
|
-
* `
|
|
1788
|
-
* field validation.
|
|
1911
|
+
* across the collection. Follows `title` until first publish, then locks.
|
|
1912
|
+
* `validate` is the message under the field; `beforeChange` is the enforcement
|
|
1913
|
+
* on a draft save, where Payload skips field validation.
|
|
1789
1914
|
*/
|
|
1790
|
-
function slugField({ collection, isReserved }) {
|
|
1915
|
+
function slugField({ collection, isReserved, folderRoots = [] }) {
|
|
1791
1916
|
const validate = async (value, options) => {
|
|
1792
1917
|
try {
|
|
1793
1918
|
const builtIn = await validations.text(value, options);
|
|
@@ -1800,14 +1925,25 @@ function slugField({ collection, isReserved }) {
|
|
|
1800
1925
|
req: options.req
|
|
1801
1926
|
}) ?? true;
|
|
1802
1927
|
};
|
|
1803
|
-
|
|
1928
|
+
const slug = {
|
|
1804
1929
|
name: "slug",
|
|
1805
1930
|
type: "text",
|
|
1806
1931
|
required: true,
|
|
1807
1932
|
unique: true,
|
|
1808
|
-
|
|
1933
|
+
access: { update: slugUpdateAccess(collection) },
|
|
1934
|
+
admin: {
|
|
1935
|
+
description: "Path under the site root, no leading slash: \"about-us\" or \"patients/stories\".",
|
|
1936
|
+
components: { Field: SLUG_FIELD },
|
|
1937
|
+
custom: { folderRoots }
|
|
1938
|
+
},
|
|
1809
1939
|
hooks: {
|
|
1810
|
-
beforeValidate: [({
|
|
1940
|
+
beforeValidate: [({ data, originalDoc, siblingData, value }) => {
|
|
1941
|
+
const title = typeof data?.title === "string" ? data.title : "";
|
|
1942
|
+
const { parent, newFolder } = resolvePrefix(siblingData, data, originalDoc);
|
|
1943
|
+
if (!isLockedDoc(originalDoc) && !isUnlocking(data) && !isUnlocking(siblingData) && title) return composeSlug(parent, title, newFolder);
|
|
1944
|
+
if ((isUnlocking(data) || isUnlocking(siblingData)) && (parent !== void 0 || newFolder)) return composeSlug(parent, typeof value === "string" && value ? slugLeaf(value) : title ? slugifySegment(title) : "", newFolder);
|
|
1945
|
+
return typeof value === "string" ? normalizeSlug(value) : value;
|
|
1946
|
+
}],
|
|
1811
1947
|
beforeChange: [async ({ data, originalDoc, req, value }) => {
|
|
1812
1948
|
if (typeof value !== "string") return value;
|
|
1813
1949
|
const problem = await slugProblem(value, {
|
|
@@ -1829,9 +1965,81 @@ function slugField({ collection, isReserved }) {
|
|
|
1829
1965
|
},
|
|
1830
1966
|
validate
|
|
1831
1967
|
};
|
|
1968
|
+
return [
|
|
1969
|
+
{
|
|
1970
|
+
name: "parent",
|
|
1971
|
+
type: "text",
|
|
1972
|
+
defaultValue: "",
|
|
1973
|
+
access: { update: slugUpdateAccess(collection) },
|
|
1974
|
+
admin: { hidden: true },
|
|
1975
|
+
hooks: { beforeValidate: [({ data, originalDoc, siblingData, value }) => {
|
|
1976
|
+
const { newFolder } = resolvePrefix(siblingData, data, originalDoc);
|
|
1977
|
+
if (value === "__new__") return parentPrefix(NEW_FOLDER_PARENT, newFolder);
|
|
1978
|
+
return typeof value === "string" ? normalizeSlug(value) : "";
|
|
1979
|
+
}] },
|
|
1980
|
+
validate: async (value, options) => {
|
|
1981
|
+
const { newFolder } = resolvePrefix(options.siblingData, options.data);
|
|
1982
|
+
if (value !== "__new__" && !newFolder) return true;
|
|
1983
|
+
const prefix = parentPrefix(NEW_FOLDER_PARENT, newFolder);
|
|
1984
|
+
if (!prefix) return true;
|
|
1985
|
+
return await existingParents(prefix, {
|
|
1986
|
+
collection,
|
|
1987
|
+
folderRoots,
|
|
1988
|
+
id: options.id,
|
|
1989
|
+
isReserved,
|
|
1990
|
+
req: options.req
|
|
1991
|
+
}) ? EXISTING_PARENT_MESSAGE : true;
|
|
1992
|
+
}
|
|
1993
|
+
},
|
|
1994
|
+
{
|
|
1995
|
+
name: "newFolder",
|
|
1996
|
+
type: "text",
|
|
1997
|
+
admin: { hidden: true },
|
|
1998
|
+
access: { update: slugUpdateAccess(collection) }
|
|
1999
|
+
},
|
|
2000
|
+
slug,
|
|
2001
|
+
hiddenCheckbox("slugLocked", "Slug locked"),
|
|
2002
|
+
hiddenCheckbox("slugUnlock", "Unlock slug"),
|
|
2003
|
+
{
|
|
2004
|
+
name: "publishedSlug",
|
|
2005
|
+
type: "text",
|
|
2006
|
+
label: "Published slug",
|
|
2007
|
+
admin: { hidden: true }
|
|
2008
|
+
}
|
|
2009
|
+
];
|
|
1832
2010
|
}
|
|
1833
2011
|
//#endregion
|
|
1834
2012
|
//#region src/collections/pages.ts
|
|
2013
|
+
/** Standing copy on the Layout field — same job as the hero description. */
|
|
2014
|
+
const LAYOUT_DESCRIPTION = "Every page needs at least one section below the hero. A page cannot be published without one.";
|
|
2015
|
+
/** Field error when Publish is clicked with an empty body. */
|
|
2016
|
+
const LAYOUT_REQUIRED_MESSAGE = "Add at least one section. A page cannot be published without one.";
|
|
2017
|
+
/**
|
|
2018
|
+
* Toast label for that error. Payload's envelope is always
|
|
2019
|
+
* "The following field is invalid: {label}", so the label has to name the fix.
|
|
2020
|
+
*/
|
|
2021
|
+
const LAYOUT_REQUIRED_LABEL = "Layout — add a section";
|
|
2022
|
+
function layoutRows(data, originalDoc) {
|
|
2023
|
+
if (data && "layout" in data && data.layout !== void 0) return Array.isArray(data.layout) ? data.layout : [];
|
|
2024
|
+
return Array.isArray(originalDoc?.layout) ? originalDoc.layout : [];
|
|
2025
|
+
}
|
|
2026
|
+
/**
|
|
2027
|
+
* Drafts may be empty. Publish with no section is refused here so the toast
|
|
2028
|
+
* names the fix — Payload's built-in minRows toast only says "Content → Layout".
|
|
2029
|
+
*/
|
|
2030
|
+
const requireLayoutOnPublish = ({ collection, data, originalDoc, req }) => {
|
|
2031
|
+
if (!data || data._status !== "published") return data;
|
|
2032
|
+
if (layoutRows(data, originalDoc).length >= 1) return data;
|
|
2033
|
+
throw new ValidationError({
|
|
2034
|
+
collection: typeof collection?.slug === "string" ? collection.slug : "pages",
|
|
2035
|
+
errors: [{
|
|
2036
|
+
label: LAYOUT_REQUIRED_LABEL,
|
|
2037
|
+
message: LAYOUT_REQUIRED_MESSAGE,
|
|
2038
|
+
path: "layout"
|
|
2039
|
+
}],
|
|
2040
|
+
req
|
|
2041
|
+
}, req?.t);
|
|
2042
|
+
};
|
|
1835
2043
|
/**
|
|
1836
2044
|
* Content to create or save a draft; Publish to publish or edit a live
|
|
1837
2045
|
* page. Without Publish, update is constrained to `_status: draft`.
|
|
@@ -1846,11 +2054,12 @@ const pagesUpdate = async (args) => {
|
|
|
1846
2054
|
* The CMS owns copy and the order of sections; what a section looks like is
|
|
1847
2055
|
* code-owned, which is why the blocks are an argument.
|
|
1848
2056
|
*/
|
|
1849
|
-
function createPages({ heroBlocks, isReservedSlug, layoutBlocks, previewPath, previewButton }) {
|
|
2057
|
+
function createPages({ folderRoots, heroBlocks, isReservedSlug, layoutBlocks, previewPath, previewButton }) {
|
|
1850
2058
|
const [defaultHero] = heroBlocks;
|
|
1851
2059
|
if (!defaultHero) throw new Error("createPages needs at least one hero block");
|
|
1852
2060
|
return {
|
|
1853
2061
|
slug: "pages",
|
|
2062
|
+
defaultSort: "slug",
|
|
1854
2063
|
admin: {
|
|
1855
2064
|
useAsTitle: "title",
|
|
1856
2065
|
group: "Content",
|
|
@@ -1875,14 +2084,20 @@ function createPages({ heroBlocks, isReservedSlug, layoutBlocks, previewPath, pr
|
|
|
1875
2084
|
drafts: { autosave: { interval: 375 } },
|
|
1876
2085
|
maxPerDoc: 50
|
|
1877
2086
|
},
|
|
2087
|
+
hooks: {
|
|
2088
|
+
beforeValidate: [requireLayoutOnPublish],
|
|
2089
|
+
beforeChange: [lockSlugOnPublish]
|
|
2090
|
+
},
|
|
1878
2091
|
fields: [
|
|
1879
2092
|
{
|
|
1880
2093
|
name: "title",
|
|
1881
2094
|
type: "text",
|
|
1882
|
-
required: true
|
|
2095
|
+
required: true,
|
|
2096
|
+
admin: { components: { Cell: PAGE_TITLE_CELL } }
|
|
1883
2097
|
},
|
|
1884
|
-
slugField({
|
|
2098
|
+
...slugField({
|
|
1885
2099
|
collection: "pages",
|
|
2100
|
+
folderRoots,
|
|
1886
2101
|
isReserved: isReservedSlug
|
|
1887
2102
|
}),
|
|
1888
2103
|
{
|
|
@@ -1904,7 +2119,8 @@ function createPages({ heroBlocks, isReservedSlug, layoutBlocks, previewPath, pr
|
|
|
1904
2119
|
labels: {
|
|
1905
2120
|
singular: "Section",
|
|
1906
2121
|
plural: "Sections"
|
|
1907
|
-
}
|
|
2122
|
+
},
|
|
2123
|
+
admin: { description: LAYOUT_DESCRIPTION }
|
|
1908
2124
|
}
|
|
1909
2125
|
]
|
|
1910
2126
|
};
|
|
@@ -2097,6 +2313,119 @@ function createMedia({ staticDir = "media", mimeTypes = ["image/*"], imageSizes
|
|
|
2097
2313
|
};
|
|
2098
2314
|
}
|
|
2099
2315
|
//#endregion
|
|
2316
|
+
//#region src/plugins/redirects.ts
|
|
2317
|
+
const REDIRECTS_SLUG = "redirects";
|
|
2318
|
+
function redirectPath(slug) {
|
|
2319
|
+
const normalized = normalizeSlug(slug);
|
|
2320
|
+
return normalized ? `/${normalized}` : "";
|
|
2321
|
+
}
|
|
2322
|
+
async function recordPublishedSlugRedirect({ fromSlug, payload, req, toSlug }) {
|
|
2323
|
+
const from = redirectPath(fromSlug);
|
|
2324
|
+
const to = redirectPath(toSlug);
|
|
2325
|
+
if (!from || !to || from === to) return;
|
|
2326
|
+
const found = async (where) => payload.find({
|
|
2327
|
+
collection: REDIRECTS_SLUG,
|
|
2328
|
+
depth: 0,
|
|
2329
|
+
limit: 100,
|
|
2330
|
+
overrideAccess: true,
|
|
2331
|
+
pagination: false,
|
|
2332
|
+
req,
|
|
2333
|
+
where
|
|
2334
|
+
});
|
|
2335
|
+
for (const doc of (await found({ from: { equals: to } })).docs) await payload.delete({
|
|
2336
|
+
collection: REDIRECTS_SLUG,
|
|
2337
|
+
id: doc.id,
|
|
2338
|
+
overrideAccess: true,
|
|
2339
|
+
req
|
|
2340
|
+
});
|
|
2341
|
+
for (const doc of (await found({ "to.url": { equals: from } })).docs) await payload.update({
|
|
2342
|
+
collection: REDIRECTS_SLUG,
|
|
2343
|
+
data: { to: {
|
|
2344
|
+
type: "custom",
|
|
2345
|
+
url: to
|
|
2346
|
+
} },
|
|
2347
|
+
id: doc.id,
|
|
2348
|
+
overrideAccess: true,
|
|
2349
|
+
req
|
|
2350
|
+
});
|
|
2351
|
+
const data = {
|
|
2352
|
+
from,
|
|
2353
|
+
to: {
|
|
2354
|
+
type: "custom",
|
|
2355
|
+
url: to
|
|
2356
|
+
},
|
|
2357
|
+
type: "308"
|
|
2358
|
+
};
|
|
2359
|
+
const existing = (await found({ from: { equals: from } })).docs[0];
|
|
2360
|
+
if (existing) {
|
|
2361
|
+
await payload.update({
|
|
2362
|
+
collection: REDIRECTS_SLUG,
|
|
2363
|
+
data,
|
|
2364
|
+
id: existing.id,
|
|
2365
|
+
overrideAccess: true,
|
|
2366
|
+
req
|
|
2367
|
+
});
|
|
2368
|
+
return;
|
|
2369
|
+
}
|
|
2370
|
+
await payload.create({
|
|
2371
|
+
collection: REDIRECTS_SLUG,
|
|
2372
|
+
data,
|
|
2373
|
+
overrideAccess: true,
|
|
2374
|
+
req
|
|
2375
|
+
});
|
|
2376
|
+
}
|
|
2377
|
+
const writePublishedSlugRedirect = async ({ doc, previousDoc, req }) => {
|
|
2378
|
+
if (doc._status !== "published") return doc;
|
|
2379
|
+
const fromSlug = typeof previousDoc?.publishedSlug === "string" ? previousDoc.publishedSlug : "";
|
|
2380
|
+
const toSlug = typeof doc.slug === "string" ? doc.slug : "";
|
|
2381
|
+
await recordPublishedSlugRedirect({
|
|
2382
|
+
fromSlug,
|
|
2383
|
+
payload: req.payload,
|
|
2384
|
+
req,
|
|
2385
|
+
toSlug
|
|
2386
|
+
});
|
|
2387
|
+
return doc;
|
|
2388
|
+
};
|
|
2389
|
+
function withPublishedSlugRedirect(collection) {
|
|
2390
|
+
return {
|
|
2391
|
+
...collection,
|
|
2392
|
+
hooks: {
|
|
2393
|
+
...collection.hooks,
|
|
2394
|
+
afterChange: [...collection.hooks?.afterChange ?? [], writePublishedSlugRedirect]
|
|
2395
|
+
}
|
|
2396
|
+
};
|
|
2397
|
+
}
|
|
2398
|
+
/**
|
|
2399
|
+
* `@payloadcms/plugin-redirects` scoped to `pages`, 308 only. A published
|
|
2400
|
+
* slug change writes the redirect and collapses chains. The plugin stores
|
|
2401
|
+
* rows; the site still has to serve them.
|
|
2402
|
+
*/
|
|
2403
|
+
function createRedirects() {
|
|
2404
|
+
const plugin = redirectsPlugin({
|
|
2405
|
+
collections: ["pages"],
|
|
2406
|
+
redirectTypeFieldOverride: {
|
|
2407
|
+
admin: { hidden: true },
|
|
2408
|
+
defaultValue: "308"
|
|
2409
|
+
},
|
|
2410
|
+
redirectTypes: ["308"],
|
|
2411
|
+
overrides: {
|
|
2412
|
+
access: {
|
|
2413
|
+
create: canUseFeature("content"),
|
|
2414
|
+
delete: isAdmin,
|
|
2415
|
+
update: canUseFeature("content")
|
|
2416
|
+
},
|
|
2417
|
+
admin: { group: "Content" }
|
|
2418
|
+
}
|
|
2419
|
+
});
|
|
2420
|
+
return (config) => {
|
|
2421
|
+
const next = plugin(config);
|
|
2422
|
+
return {
|
|
2423
|
+
...next,
|
|
2424
|
+
collections: next.collections?.map((collection) => collection.slug === "pages" ? withPublishedSlugRedirect(collection) : collection)
|
|
2425
|
+
};
|
|
2426
|
+
};
|
|
2427
|
+
}
|
|
2428
|
+
//#endregion
|
|
2100
2429
|
//#region src/admin-nav/fields.ts
|
|
2101
2430
|
const ADMIN_NAV = "@bison-lab/payload-core/admin#AdminNav";
|
|
2102
2431
|
const ADMIN_NAV_ROW_LABEL = "@bison-lab/payload-core/admin#AdminNavRowLabel";
|
|
@@ -2684,6 +3013,6 @@ const documentTitleActions = definePlugin({
|
|
|
2684
3013
|
plugin: ({ config }) => withTitleActions(config)
|
|
2685
3014
|
});
|
|
2686
3015
|
//#endregion
|
|
2687
|
-
export { ADMIN_NAV, ADMIN_NAV_ENTITY_FIELD, ADMIN_NAV_ITEM_ROW_LABEL, ADMIN_NAV_ROW_LABEL, ADMIN_NAV_SLUG, BRAND_ASSETS_MIME_TYPES, BRAND_ASSETS_SLUG, CAPABILITIES, CAPABILITY_FEATURES, DEFAULT_ROLE_GRANTS, DEFAULT_ROLE_MATRIX, DESCRIPTION_LENGTH, DEVELOPER_DESCRIPTION, DOCUMENT_CREATE_NEW, DOCUMENT_TITLE_ACTIONS, DUPLICATE_ROLE_NAME_MESSAGE, DUPLICATE_ROLE_SLUG_MESSAGE, FEATURES_MATRIX_FIELD, FEATURES_SLUG, FEATURE_GROUPS, FEATURE_GROUP_LABELS, INVALID_ROLE_NAME_MESSAGE, LAST_ADMIN_DELETE_MESSAGE, LAST_ADMIN_DEMOTE_MESSAGE, LAST_USERS_TICK_MESSAGE, LOOK_FIELD, MISSING_PACKAGE_FEATURES_MESSAGE, MISSING_SEED_ROLES_MESSAGE, NAVIGATION_DB_NAME, NAVIGATION_FEATURE, NAVIGATION_FOOTER_DB_NAME, NAVIGATION_FOOTER_SLUG, NAVIGATION_SLUG, PACKAGE_FEATURES, PACKAGE_FEATURE_SLUGS, PAGE_EDITOR_SYSTEM_KEYS, ROLES, ROLES_FIELD, ROLES_FIELD_DESCRIPTION, ROLES_GLOBAL_DESCRIPTION, ROLES_GRANTS_FIELD, ROLES_MATRIX_FIELD, ROLES_ROW_LABEL, ROLES_SLUG, ROLE_LABELS, ROLE_NAME_FIELD, ROLE_SLUG_FIELD, SHARE_IMAGE_SIZE, SYSTEM_COLOR_KEYS, THEME_APPEARANCE_FIELD, THEME_APPEARANCE_SLUG, THEME_COLORS_SLUG, THEME_COLOR_FIELD, THEME_COLOR_SCALE_FIELD, THEME_CONTRAST_REPORT, THEME_DOCUMENT_CONTROLS, THEME_FEATURE_SLUGS, THEME_FONT_FIELD, THEME_GREY_SCALE_FIELD, THEME_IDENTITY_FALLBACK, THEME_IDENTITY_SLUG, THEME_LIBRARY_FIELD, THEME_PAIRING_FIELD, THEME_PREVIEW_BREAKPOINTS, THEME_PUBLISH_FIELD, THEME_SAVE_BUTTON, THEME_SECTION_HEADING, THEME_SLUG, THEME_TYPOGRAPHY_SLUG, adminNav, adminOnlyApiTab, allowedFeaturesForUser, applyRoleSlugsFromNames, authenticatedOrPublished, canManageBrand, canManageContent, canPublish, canUseFeature, colorTokenField, createAdminNav, createBrandAssets, createFeatures, createMedia, createNavigation, createPages, createRoles, createTheme, createUsers, defaultFeaturesFieldValue, defaultRolesFieldValue, deleteLibraryColor, developerOnlyAccess, documentCreateNew, documentTitle, documentTitleActions, featureCatalogue, featureGroupLabel, findColorTokens, findColorUsages, firstImageIn, getFeaturesMatrix, getNavigation, getRolesMatrix, hasCapability, hasFeature, hasGrant, hasRole, headerCtaEnabled, hideUnlessDeveloper, hideUnlessFeature, isAdmin, isAdminOrSelf, isAuthenticated, isDeveloper, isDeveloperTab, isFeatureGroupId, isFeatureSlug, isLockedFeature, isPackageFeatureSlug, isPrivilegedRole, isRole, isRoleName, isRoleSlug, lookField, noIndexField, normalizeSlug, normalizeStoredRoles, pageEditorLooks, pageEditorTokens, parseFeaturesMatrix, parseRolesMatrix, persistThemeChild, publishThemeChild, resetRolesMatrix, resolveAdminNav, resolveThemeIdentity, rewriteColorToken, rewriteColorTokens, rewriteColorUsages, roleDescription, roleLabel, roleSelectOptions, sanitizeRoleNameInput, sanitizeSvg, seedFeatures, seedRoleRows, seedRoles, seedTheme, seoPlugin, slugField, slugifyRoleName, stampFeatureCatalogue, storedRoles, themeColorKeys, themeLibraryFromDoc, titleTemplate, truncateAtWord, validateFeaturesMatrix, validateHeaderCta, validateRolesMatrix };
|
|
3016
|
+
export { ADMIN_NAV, ADMIN_NAV_ENTITY_FIELD, ADMIN_NAV_ITEM_ROW_LABEL, ADMIN_NAV_ROW_LABEL, ADMIN_NAV_SLUG, BRAND_ASSETS_MIME_TYPES, BRAND_ASSETS_SLUG, CAPABILITIES, CAPABILITY_FEATURES, DEFAULT_ROLE_GRANTS, DEFAULT_ROLE_MATRIX, DESCRIPTION_LENGTH, DEVELOPER_DESCRIPTION, DOCUMENT_CREATE_NEW, DOCUMENT_TITLE_ACTIONS, DUPLICATE_ROLE_NAME_MESSAGE, DUPLICATE_ROLE_SLUG_MESSAGE, EXISTING_PARENT_MESSAGE, FEATURES_MATRIX_FIELD, FEATURES_SLUG, FEATURE_GROUPS, FEATURE_GROUP_LABELS, INVALID_ROLE_NAME_MESSAGE, LAST_ADMIN_DELETE_MESSAGE, LAST_ADMIN_DEMOTE_MESSAGE, LAST_USERS_TICK_MESSAGE, LAYOUT_DESCRIPTION, LAYOUT_REQUIRED_LABEL, LAYOUT_REQUIRED_MESSAGE, LOOK_FIELD, MISSING_PACKAGE_FEATURES_MESSAGE, MISSING_SEED_ROLES_MESSAGE, NAVIGATION_DB_NAME, NAVIGATION_FEATURE, NAVIGATION_FOOTER_DB_NAME, NAVIGATION_FOOTER_SLUG, NAVIGATION_SLUG, NEW_FOLDER_PARENT, PACKAGE_FEATURES, PACKAGE_FEATURE_SLUGS, PAGE_EDITOR_SYSTEM_KEYS, PAGE_TITLE_CELL, REDIRECTS_SLUG, ROLES, ROLES_FIELD, ROLES_FIELD_DESCRIPTION, ROLES_GLOBAL_DESCRIPTION, ROLES_GRANTS_FIELD, ROLES_MATRIX_FIELD, ROLES_ROW_LABEL, ROLES_SLUG, ROLE_LABELS, ROLE_NAME_FIELD, ROLE_SLUG_FIELD, SHARE_IMAGE_SIZE, SLUG_FIELD, SLUG_UNLOCK_WARNING, SYSTEM_COLOR_KEYS, THEME_APPEARANCE_FIELD, THEME_APPEARANCE_SLUG, THEME_COLORS_SLUG, THEME_COLOR_FIELD, THEME_COLOR_SCALE_FIELD, THEME_CONTRAST_REPORT, THEME_DOCUMENT_CONTROLS, THEME_FEATURE_SLUGS, THEME_FONT_FIELD, THEME_GREY_SCALE_FIELD, THEME_IDENTITY_FALLBACK, THEME_IDENTITY_SLUG, THEME_LIBRARY_FIELD, THEME_PAIRING_FIELD, THEME_PREVIEW_BREAKPOINTS, THEME_PUBLISH_FIELD, THEME_SAVE_BUTTON, THEME_SECTION_HEADING, THEME_SLUG, THEME_TYPOGRAPHY_SLUG, adminNav, adminOnlyApiTab, allowedFeaturesForUser, applyRoleSlugsFromNames, authenticatedOrPublished, canManageBrand, canManageContent, canPublish, canUseFeature, colorTokenField, composeSlug, createAdminNav, createBrandAssets, createFeatures, createMedia, createNavigation, createPages, createRedirects, createRoles, createTheme, createUsers, defaultFeaturesFieldValue, defaultRolesFieldValue, deleteLibraryColor, developerOnlyAccess, documentCreateNew, documentTitle, documentTitleActions, featureCatalogue, featureGroupLabel, findColorTokens, findColorUsages, firstImageIn, getFeaturesMatrix, getNavigation, getRolesMatrix, hasCapability, hasFeature, hasGrant, hasRole, headerCtaEnabled, hideUnlessDeveloper, hideUnlessFeature, inferredFolders, isAdmin, isAdminOrSelf, isAuthenticated, isDeveloper, isDeveloperTab, isFeatureGroupId, isFeatureSlug, isLockedFeature, isPackageFeatureSlug, isPrivilegedRole, isRole, isRoleName, isRoleSlug, lockSlugOnPublish, lookField, noIndexField, normalizeSlug, normalizeStoredRoles, pageEditorLooks, pageEditorTokens, parentPrefix, parseFeaturesMatrix, parseRolesMatrix, persistThemeChild, publishThemeChild, recordPublishedSlugRedirect, redirectPath, requireLayoutOnPublish, resetRolesMatrix, resolveAdminNav, resolveThemeIdentity, rewriteColorToken, rewriteColorTokens, rewriteColorUsages, roleDescription, roleLabel, roleSelectOptions, sanitizeRoleNameInput, sanitizeSvg, seedFeatures, seedRoleRows, seedRoles, seedTheme, seoPlugin, slugDepth, slugField, slugLeaf, slugifyRoleName, stampFeatureCatalogue, storedRoles, themeColorKeys, themeLibraryFromDoc, titleTemplate, truncateAtWord, validateFeaturesMatrix, validateHeaderCta, validateRolesMatrix, writePublishedSlugRedirect };
|
|
2688
3017
|
|
|
2689
3018
|
//# sourceMappingURL=index.mjs.map
|