@bison-lab/payload-core 3.14.0 → 3.15.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 +46 -25
- package/dist/admin.mjs +2 -8
- package/dist/admin.mjs.map +1 -1
- package/dist/{identity-Dwyq8519.mjs → identity-LAzSeorC.mjs} +1 -1
- package/dist/{identity-Dwyq8519.mjs.map → identity-LAzSeorC.mjs.map} +1 -1
- package/dist/index.d.mts +32 -16
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +823 -197
- package/dist/index.mjs.map +1 -1
- package/dist/{looks-Xs6v5vYU.d.mts → looks-C6kTjvk3.d.mts} +1 -1
- package/dist/{looks-Xs6v5vYU.d.mts.map → looks-C6kTjvk3.d.mts.map} +1 -1
- package/dist/metadata.d.mts +1 -1
- package/dist/metadata.mjs +1 -1
- package/dist/{share-image-BlFAJLnd.mjs → share-image-C4ILz4p2.mjs} +1 -1
- package/dist/{share-image-BlFAJLnd.mjs.map → share-image-C4ILz4p2.mjs.map} +1 -1
- package/dist/theme.d.mts +1 -1
- package/dist/theme.mjs +1 -1
- package/dist/{title-sWwcCqHT.d.mts → title-B2-gWQZd.d.mts} +1 -1
- package/dist/{title-sWwcCqHT.d.mts.map → title-B2-gWQZd.d.mts.map} +1 -1
- package/package.json +3 -3
- package/dist/access-B3LglN2J.mjs +0 -662
- package/dist/access-B3LglN2J.mjs.map +0 -1
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { n as
|
|
3
|
-
import { A as THEME_FONT_FIELD, C as validateThemeHex, D as THEME_COLOR_SCALE_FIELD, E as THEME_COLOR_FIELD, F as THEME_PUBLISH_FIELD, I as THEME_SAVE_BUTTON, L as THEME_SECTION_HEADING, M as THEME_IDENTITY_FALLBACK, N as THEME_LIBRARY_FIELD, O as THEME_CONTRAST_REPORT, P as THEME_PAIRING_FIELD, R as headingSelectValue, S as validateSourceIncluded, T as THEME_APPEARANCE_FIELD, _ as THEME_SLUG, a as pageEditorLooks, c as findColorTokens, d as rewriteColorToken, f as themeColorKeys, g as THEME_IDENTITY_SLUG, h as THEME_COLORS_SLUG, i as PAGE_EDITOR_SYSTEM_KEYS, j as THEME_GREY_SCALE_FIELD, k as THEME_DOCUMENT_CONTROLS, l as rewriteColorTokens, m as THEME_APPEARANCE_SLUG, n as colorTokenField, o as pageEditorTokens, p as SYSTEM_COLOR_KEYS, r as lookField, s as themeLibraryFromDoc, t as resolveThemeIdentity, u as deleteLibraryColor, v as THEME_TYPOGRAPHY_SLUG, w as LOOK_FIELD, x as themeConfigFromDoc, y as docFromConfig } from "./identity-Dwyq8519.mjs";
|
|
1
|
+
import { n as documentTitle, r as titleTemplate, t as SHARE_IMAGE_SIZE } from "./share-image-C4ILz4p2.mjs";
|
|
2
|
+
import { A as THEME_FONT_FIELD, C as validateThemeHex, D as THEME_COLOR_SCALE_FIELD, E as THEME_COLOR_FIELD, F as THEME_PUBLISH_FIELD, I as THEME_SAVE_BUTTON, L as THEME_SECTION_HEADING, M as THEME_IDENTITY_FALLBACK, N as THEME_LIBRARY_FIELD, O as THEME_CONTRAST_REPORT, P as THEME_PAIRING_FIELD, R as headingSelectValue, S as validateSourceIncluded, T as THEME_APPEARANCE_FIELD, _ as THEME_SLUG, a as pageEditorLooks, c as findColorTokens, d as rewriteColorToken, f as themeColorKeys, g as THEME_IDENTITY_SLUG, h as THEME_COLORS_SLUG, i as PAGE_EDITOR_SYSTEM_KEYS, j as THEME_GREY_SCALE_FIELD, k as THEME_DOCUMENT_CONTROLS, l as rewriteColorTokens, m as THEME_APPEARANCE_SLUG, n as colorTokenField, o as pageEditorTokens, p as SYSTEM_COLOR_KEYS, r as lookField, s as themeLibraryFromDoc, t as resolveThemeIdentity, u as deleteLibraryColor, v as THEME_TYPOGRAPHY_SLUG, w as LOOK_FIELD, x as themeConfigFromDoc, y as docFromConfig } from "./identity-LAzSeorC.mjs";
|
|
4
3
|
import { seoPlugin as seoPlugin$1 } from "@payloadcms/plugin-seo";
|
|
5
4
|
import { catalog } from "@bison-lab/fonts";
|
|
6
5
|
import { DESTRUCTIVE_SCALE_HEX, SHADE_STEPS, presetHints } from "@bison-lab/tokens";
|
|
@@ -304,75 +303,664 @@ async function readJson(req) {
|
|
|
304
303
|
return null;
|
|
305
304
|
}
|
|
306
305
|
//#endregion
|
|
307
|
-
//#region src/
|
|
306
|
+
//#region src/features/types.ts
|
|
307
|
+
const FEATURES_SLUG = "features";
|
|
308
|
+
const FEATURE_GROUPS = [
|
|
309
|
+
"pages",
|
|
310
|
+
"media",
|
|
311
|
+
"theme",
|
|
312
|
+
"users"
|
|
313
|
+
];
|
|
314
|
+
const FEATURE_GROUP_LABELS = {
|
|
315
|
+
pages: "Pages",
|
|
316
|
+
media: "Media",
|
|
317
|
+
theme: "Theme",
|
|
318
|
+
users: "Users"
|
|
319
|
+
};
|
|
320
|
+
const PACKAGE_FEATURE_SLUGS = [
|
|
321
|
+
"content",
|
|
322
|
+
"publish",
|
|
323
|
+
"media",
|
|
324
|
+
"theme-colors",
|
|
325
|
+
"theme-typography",
|
|
326
|
+
"theme-appearance",
|
|
327
|
+
"theme-identity",
|
|
328
|
+
"brand-assets",
|
|
329
|
+
"users",
|
|
330
|
+
"roles"
|
|
331
|
+
];
|
|
308
332
|
/**
|
|
309
|
-
*
|
|
310
|
-
*
|
|
311
|
-
* different child never goes live. A first save (no stored row) still
|
|
312
|
-
* writes only the named child; `getPublishedTheme` fills the rest from
|
|
313
|
-
* the seed.
|
|
333
|
+
* Package catalogue. Features itself is not a row — that screen is
|
|
334
|
+
* Developer-only in code. Empty Global falls back to these defaults.
|
|
314
335
|
*/
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
336
|
+
const PACKAGE_FEATURES = [
|
|
337
|
+
{
|
|
338
|
+
slug: "content",
|
|
339
|
+
label: "Content",
|
|
340
|
+
group: "pages",
|
|
341
|
+
defaultReleased: true
|
|
342
|
+
},
|
|
343
|
+
{
|
|
344
|
+
slug: "publish",
|
|
345
|
+
label: "Publish",
|
|
346
|
+
group: "pages",
|
|
347
|
+
defaultReleased: true
|
|
348
|
+
},
|
|
349
|
+
{
|
|
350
|
+
slug: "media",
|
|
351
|
+
label: "Media",
|
|
352
|
+
group: "media",
|
|
353
|
+
defaultReleased: true
|
|
354
|
+
},
|
|
355
|
+
{
|
|
356
|
+
slug: "theme-colors",
|
|
357
|
+
label: "Colors",
|
|
358
|
+
group: "theme",
|
|
359
|
+
defaultReleased: true
|
|
360
|
+
},
|
|
361
|
+
{
|
|
362
|
+
slug: "theme-typography",
|
|
363
|
+
label: "Typography",
|
|
364
|
+
group: "theme",
|
|
365
|
+
defaultReleased: true
|
|
366
|
+
},
|
|
367
|
+
{
|
|
368
|
+
slug: "theme-appearance",
|
|
369
|
+
label: "Appearance",
|
|
370
|
+
group: "theme",
|
|
371
|
+
defaultReleased: true
|
|
372
|
+
},
|
|
373
|
+
{
|
|
374
|
+
slug: "theme-identity",
|
|
375
|
+
label: "Identity",
|
|
376
|
+
group: "theme",
|
|
377
|
+
defaultReleased: true
|
|
378
|
+
},
|
|
379
|
+
{
|
|
380
|
+
slug: "brand-assets",
|
|
381
|
+
label: "Brand assets",
|
|
382
|
+
group: "theme",
|
|
383
|
+
defaultReleased: true
|
|
384
|
+
},
|
|
385
|
+
{
|
|
386
|
+
slug: "users",
|
|
387
|
+
label: "Users",
|
|
388
|
+
group: "users",
|
|
389
|
+
defaultReleased: true
|
|
390
|
+
},
|
|
391
|
+
{
|
|
392
|
+
slug: "roles",
|
|
393
|
+
label: "Roles",
|
|
394
|
+
group: "users",
|
|
395
|
+
defaultReleased: true
|
|
396
|
+
}
|
|
397
|
+
];
|
|
398
|
+
function isFeatureGroupId(value) {
|
|
399
|
+
return typeof value === "string" && FEATURE_GROUPS.includes(value);
|
|
400
|
+
}
|
|
401
|
+
function isPackageFeatureSlug(value) {
|
|
402
|
+
return typeof value === "string" && PACKAGE_FEATURE_SLUGS.includes(value);
|
|
403
|
+
}
|
|
404
|
+
function isFeatureSlug(value) {
|
|
405
|
+
return typeof value === "string" && /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/.test(value);
|
|
406
|
+
}
|
|
407
|
+
/** @deprecated Locks are gone; the release switch is the valve. */
|
|
408
|
+
function isLockedFeature(_slug, _locked) {
|
|
409
|
+
return false;
|
|
410
|
+
}
|
|
411
|
+
//#endregion
|
|
412
|
+
//#region src/features/matrix.ts
|
|
413
|
+
const MISSING_PACKAGE_FEATURES_MESSAGE = "Features must include Content, Publish, Media, Theme screens, Brand assets, Users, and Roles.";
|
|
414
|
+
function featureCatalogue(extras = []) {
|
|
415
|
+
return [...PACKAGE_FEATURES.map((feature) => ({ ...feature })), ...extras.map((extra) => ({
|
|
416
|
+
slug: extra.slug,
|
|
417
|
+
label: extra.label,
|
|
418
|
+
group: extra.group ?? "users",
|
|
419
|
+
defaultReleased: extra.defaultReleased ?? false
|
|
420
|
+
}))];
|
|
421
|
+
}
|
|
422
|
+
function defaultFeaturesFieldValue(extras = []) {
|
|
423
|
+
return featureCatalogue(extras).map((feature) => ({
|
|
424
|
+
id: feature.slug,
|
|
425
|
+
slug: feature.slug,
|
|
426
|
+
label: feature.label,
|
|
427
|
+
group: feature.group,
|
|
428
|
+
released: feature.defaultReleased
|
|
429
|
+
}));
|
|
430
|
+
}
|
|
431
|
+
function stampFeatureCatalogue(value, extras = []) {
|
|
432
|
+
const bySlug = new Map(featureCatalogue(extras).map((feature) => [feature.slug, feature]));
|
|
433
|
+
if (!Array.isArray(value)) return defaultFeaturesFieldValue(extras);
|
|
434
|
+
return value.flatMap((item) => {
|
|
435
|
+
if (!item || typeof item !== "object") return [];
|
|
436
|
+
const slug = "slug" in item && typeof item.slug === "string" ? item.slug : "";
|
|
437
|
+
const feature = bySlug.get(slug);
|
|
438
|
+
if (!feature) return [];
|
|
439
|
+
return [{
|
|
440
|
+
id: feature.slug,
|
|
441
|
+
slug: feature.slug,
|
|
442
|
+
label: feature.label,
|
|
443
|
+
group: feature.group,
|
|
444
|
+
released: Boolean("released" in item && item.released)
|
|
445
|
+
}];
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
function parseFeaturesMatrix(value) {
|
|
449
|
+
if (!Array.isArray(value) || value.length === 0) return {
|
|
450
|
+
ok: false,
|
|
451
|
+
message: MISSING_PACKAGE_FEATURES_MESSAGE
|
|
319
452
|
};
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
453
|
+
const rows = [];
|
|
454
|
+
const seen = /* @__PURE__ */ new Set();
|
|
455
|
+
for (const item of value) {
|
|
456
|
+
if (!item || typeof item !== "object") continue;
|
|
457
|
+
const slug = "slug" in item ? item.slug : void 0;
|
|
458
|
+
if (!isFeatureSlug(slug) || seen.has(slug)) continue;
|
|
459
|
+
seen.add(slug);
|
|
460
|
+
const pack = PACKAGE_FEATURES.find((feature) => feature.slug === slug);
|
|
461
|
+
const label = "label" in item && typeof item.label === "string" ? item.label : pack?.label ?? slug;
|
|
462
|
+
const group = "group" in item && isFeatureGroupId(item.group) ? item.group : pack?.group ?? "users";
|
|
463
|
+
rows.push({
|
|
464
|
+
id: slug,
|
|
465
|
+
slug,
|
|
466
|
+
label,
|
|
467
|
+
group,
|
|
468
|
+
released: Boolean("released" in item && item.released)
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
if (PACKAGE_FEATURES.some((feature) => !seen.has(feature.slug))) return {
|
|
472
|
+
ok: false,
|
|
473
|
+
message: MISSING_PACKAGE_FEATURES_MESSAGE
|
|
323
474
|
};
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
475
|
+
return {
|
|
476
|
+
ok: true,
|
|
477
|
+
rows
|
|
327
478
|
};
|
|
479
|
+
}
|
|
480
|
+
function validateFeaturesMatrix(value, extras = []) {
|
|
481
|
+
const parsed = parseFeaturesMatrix(value);
|
|
482
|
+
if (!parsed.ok) return parsed.message;
|
|
483
|
+
const allowed = new Set(featureCatalogue(extras).map((feature) => feature.slug));
|
|
484
|
+
if (parsed.rows.some((row) => !allowed.has(row.slug))) return MISSING_PACKAGE_FEATURES_MESSAGE;
|
|
485
|
+
return true;
|
|
486
|
+
}
|
|
487
|
+
function featureGroupLabel(group) {
|
|
488
|
+
return isFeatureGroupId(group) ? FEATURE_GROUP_LABELS[group] : group;
|
|
489
|
+
}
|
|
490
|
+
function packageFeatureLabel(slug, extras = []) {
|
|
491
|
+
const pack = PACKAGE_FEATURES.find((feature) => feature.slug === slug);
|
|
492
|
+
if (pack) return pack.label;
|
|
493
|
+
return extras.find((feature) => feature.slug === slug)?.label ?? slug;
|
|
494
|
+
}
|
|
495
|
+
function isGroupOnlySlug(slug) {
|
|
496
|
+
return isFeatureGroupId(slug) && !PACKAGE_FEATURES.some((feature) => feature.slug === slug);
|
|
497
|
+
}
|
|
498
|
+
//#endregion
|
|
499
|
+
//#region src/roles/types.ts
|
|
500
|
+
/**
|
|
501
|
+
* The Roles Global a site's generated types will describe. Optional and
|
|
502
|
+
* nullable, no index signature: a generated Global is assignable to this,
|
|
503
|
+
* never the reverse.
|
|
504
|
+
*/
|
|
505
|
+
const ROLES = [
|
|
506
|
+
"developer",
|
|
507
|
+
"admin",
|
|
508
|
+
"designer",
|
|
509
|
+
"author"
|
|
510
|
+
];
|
|
511
|
+
const ROLE_LABELS = {
|
|
512
|
+
developer: "Developer",
|
|
513
|
+
admin: "Admin",
|
|
514
|
+
designer: "Designer",
|
|
515
|
+
author: "Author"
|
|
516
|
+
};
|
|
517
|
+
/** Capability aliases over feature grants. Brand is any Theme-group leaf. */
|
|
518
|
+
const CAPABILITIES = [
|
|
519
|
+
"content",
|
|
520
|
+
"brand",
|
|
521
|
+
"publish",
|
|
522
|
+
"users"
|
|
523
|
+
];
|
|
524
|
+
const THEME_FEATURE_SLUGS = [
|
|
525
|
+
"theme-colors",
|
|
526
|
+
"theme-typography",
|
|
527
|
+
"theme-appearance",
|
|
528
|
+
"theme-identity",
|
|
529
|
+
"brand-assets"
|
|
530
|
+
];
|
|
531
|
+
const CAPABILITY_FEATURES = {
|
|
532
|
+
content: ["content"],
|
|
533
|
+
publish: ["publish"],
|
|
534
|
+
users: ["users"],
|
|
535
|
+
brand: THEME_FEATURE_SLUGS
|
|
536
|
+
};
|
|
537
|
+
const DEFAULT_ROLE_GRANTS = {
|
|
538
|
+
developer: [],
|
|
539
|
+
admin: [
|
|
540
|
+
"content",
|
|
541
|
+
"publish",
|
|
542
|
+
"media",
|
|
543
|
+
"users",
|
|
544
|
+
"roles"
|
|
545
|
+
],
|
|
546
|
+
designer: [
|
|
547
|
+
"content",
|
|
548
|
+
"media",
|
|
549
|
+
...THEME_FEATURE_SLUGS,
|
|
550
|
+
"users",
|
|
551
|
+
"roles"
|
|
552
|
+
],
|
|
553
|
+
author: ["content", "media"]
|
|
554
|
+
};
|
|
555
|
+
function isRole(value) {
|
|
556
|
+
return typeof value === "string" && ROLES.includes(value);
|
|
557
|
+
}
|
|
558
|
+
/** Lowercase slug from a letters-and-spaces name: `designer`, `the-designer`. */
|
|
559
|
+
function isRoleSlug(value) {
|
|
560
|
+
return typeof value === "string" && /^[a-z]+(-[a-z]+)*$/.test(value);
|
|
561
|
+
}
|
|
562
|
+
/** Display name: letters and single spaces only. `The Greatest Designer`. */
|
|
563
|
+
function isRoleName(value) {
|
|
564
|
+
return typeof value === "string" && /^[A-Za-z]+(?: [A-Za-z]+)*$/.test(value.trim());
|
|
565
|
+
}
|
|
566
|
+
/** Strip digits and punctuation as the name is typed. Trailing space stays. */
|
|
567
|
+
function sanitizeRoleNameInput(value) {
|
|
568
|
+
return value.replace(/[^A-Za-z ]/g, "").replace(/ {2,}/g, " ");
|
|
569
|
+
}
|
|
570
|
+
/** Name → stored key. `The Greatest Designer` → `the-greatest-designer`. */
|
|
571
|
+
function slugifyRoleName(value) {
|
|
572
|
+
if (typeof value !== "string") return "";
|
|
573
|
+
return value.trim().toLowerCase().replace(/[^a-z]+/g, "-").replace(/^-|-$/g, "");
|
|
574
|
+
}
|
|
575
|
+
function roleLabel(role, label) {
|
|
576
|
+
const trimmed = typeof label === "string" ? label.trim() : "";
|
|
577
|
+
if (trimmed) return trimmed;
|
|
578
|
+
return isRole(role) ? ROLE_LABELS[role] : role;
|
|
579
|
+
}
|
|
580
|
+
function defaultGrantsForRole(role, extraGrants) {
|
|
581
|
+
if (isRole(role)) return [...DEFAULT_ROLE_GRANTS[role]];
|
|
582
|
+
return extraGrants ? [...extraGrants] : [];
|
|
583
|
+
}
|
|
584
|
+
//#endregion
|
|
585
|
+
//#region src/roles/matrix.ts
|
|
586
|
+
const ROLES_SLUG = "roles";
|
|
587
|
+
const ROLES_GLOBAL_DESCRIPTION = "Who may do what. Drag to change rank.";
|
|
588
|
+
const ROLES_FIELD_DESCRIPTION = "Drag to change rank. Ticks are features released on Features. Developer has the whole catalogue.";
|
|
589
|
+
/**
|
|
590
|
+
* Default rank and grants. Developer is implicit (empty grants). Brand
|
|
591
|
+
* screens default to Designer.
|
|
592
|
+
*/
|
|
593
|
+
const DEFAULT_ROLE_MATRIX = ROLES.map((role) => ({
|
|
594
|
+
role,
|
|
595
|
+
grants: defaultGrantsForRole(role)
|
|
596
|
+
}));
|
|
597
|
+
const DEVELOPER_DESCRIPTION = "Everything, including features not released for assignment.";
|
|
598
|
+
const LAST_USERS_TICK_MESSAGE = "Keep Users granted on at least one of Admin or Developer.";
|
|
599
|
+
const MISSING_SEED_ROLES_MESSAGE = "Roles must include Developer, Admin, Designer, and Author.";
|
|
600
|
+
const DUPLICATE_ROLE_SLUG_MESSAGE = "Each role needs a unique slug.";
|
|
601
|
+
const DUPLICATE_ROLE_NAME_MESSAGE = "Each role needs a unique name.";
|
|
602
|
+
const INVALID_ROLE_NAME_MESSAGE = "Use letters and spaces only.";
|
|
603
|
+
function uniqueSlugs(values) {
|
|
604
|
+
const slugs = [];
|
|
605
|
+
for (const value of values) if (typeof value === "string" && isFeatureSlug(value) && !slugs.includes(value)) slugs.push(value);
|
|
606
|
+
return slugs;
|
|
607
|
+
}
|
|
608
|
+
/** Read stored grants, or rebuild them from the old capability ticks. */
|
|
609
|
+
function grantsFromStoredRole(item) {
|
|
610
|
+
if ("grants" in item && Array.isArray(item.grants)) return uniqueSlugs(item.grants);
|
|
611
|
+
const grants = [];
|
|
612
|
+
if ("content" in item && item.content) grants.push("content", "media");
|
|
613
|
+
if ("publish" in item && item.publish) grants.push("publish");
|
|
614
|
+
if ("users" in item && item.users) grants.push("users", "roles");
|
|
615
|
+
if ("brand" in item && item.brand) grants.push(...THEME_FEATURE_SLUGS);
|
|
616
|
+
return uniqueSlugs(grants);
|
|
617
|
+
}
|
|
618
|
+
function seedRoleRows(extras = []) {
|
|
619
|
+
return [...DEFAULT_ROLE_MATRIX.map((row) => ({
|
|
620
|
+
...row,
|
|
621
|
+
grants: [...row.grants],
|
|
622
|
+
label: isRole(row.role) ? ROLE_LABELS[row.role] : row.role
|
|
623
|
+
})), ...extras.map((extra) => ({
|
|
624
|
+
role: extra.role,
|
|
625
|
+
label: extra.label,
|
|
626
|
+
grants: extra.grants ? [...extra.grants] : []
|
|
627
|
+
}))];
|
|
628
|
+
}
|
|
629
|
+
function defaultRolesFieldValue(extras = []) {
|
|
630
|
+
return seedRoleRows(extras).map((row) => ({
|
|
631
|
+
id: row.role,
|
|
632
|
+
...row
|
|
633
|
+
}));
|
|
634
|
+
}
|
|
635
|
+
function readRoleRow(item) {
|
|
636
|
+
const role = "role" in item ? item.role : void 0;
|
|
637
|
+
if (typeof role !== "string" || role.trim() === "") return { error: DUPLICATE_ROLE_SLUG_MESSAGE };
|
|
638
|
+
if (!isRoleSlug(role)) return { error: DUPLICATE_ROLE_SLUG_MESSAGE };
|
|
328
639
|
return {
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
640
|
+
id: role,
|
|
641
|
+
role,
|
|
642
|
+
label: roleLabel(role, "label" in item && typeof item.label === "string" ? item.label : void 0),
|
|
643
|
+
grants: grantsFromStoredRole(item)
|
|
333
644
|
};
|
|
334
645
|
}
|
|
335
|
-
function
|
|
336
|
-
if (
|
|
337
|
-
|
|
338
|
-
|
|
646
|
+
function parseRolesMatrix(value) {
|
|
647
|
+
if (!Array.isArray(value)) return {
|
|
648
|
+
ok: false,
|
|
649
|
+
message: MISSING_SEED_ROLES_MESSAGE
|
|
650
|
+
};
|
|
651
|
+
const rows = [];
|
|
652
|
+
const seen = /* @__PURE__ */ new Set();
|
|
653
|
+
for (const item of value) {
|
|
654
|
+
if (!item || typeof item !== "object") continue;
|
|
655
|
+
const role = "role" in item ? item.role : void 0;
|
|
656
|
+
if (typeof role !== "string" || role.trim() === "") continue;
|
|
657
|
+
const parsed = readRoleRow(item);
|
|
658
|
+
if ("error" in parsed) return {
|
|
659
|
+
ok: false,
|
|
660
|
+
message: parsed.error
|
|
661
|
+
};
|
|
662
|
+
if (seen.has(parsed.role)) return {
|
|
663
|
+
ok: false,
|
|
664
|
+
message: DUPLICATE_ROLE_SLUG_MESSAGE
|
|
665
|
+
};
|
|
666
|
+
seen.add(parsed.role);
|
|
667
|
+
rows.push(parsed);
|
|
668
|
+
}
|
|
669
|
+
if (ROLES.some((role) => !seen.has(role))) return {
|
|
670
|
+
ok: false,
|
|
671
|
+
message: MISSING_SEED_ROLES_MESSAGE
|
|
672
|
+
};
|
|
339
673
|
return {
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
logoMark: theme.logoMark
|
|
674
|
+
ok: true,
|
|
675
|
+
rows
|
|
343
676
|
};
|
|
344
677
|
}
|
|
345
|
-
function
|
|
346
|
-
if (
|
|
347
|
-
return
|
|
678
|
+
function roleHasGrant(row, slug) {
|
|
679
|
+
if (row.role === "developer") return true;
|
|
680
|
+
return row.grants.includes(slug);
|
|
681
|
+
}
|
|
682
|
+
function roleHasCapability(row, capability) {
|
|
683
|
+
if (row.role === "developer") return true;
|
|
684
|
+
return CAPABILITY_FEATURES[capability].some((slug) => row.grants.includes(slug));
|
|
348
685
|
}
|
|
349
686
|
/**
|
|
350
|
-
*
|
|
351
|
-
*
|
|
687
|
+
* Mint a slug from the name only when the row has none yet. An existing
|
|
688
|
+
* key — seed or custom — stays put so a rename cannot orphan users.
|
|
352
689
|
*/
|
|
353
|
-
function
|
|
354
|
-
if (!
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
690
|
+
function applyRoleSlugsFromNames(value) {
|
|
691
|
+
if (!Array.isArray(value)) return value;
|
|
692
|
+
return value.map((item) => {
|
|
693
|
+
if (!item || typeof item !== "object") return item;
|
|
694
|
+
const row = item;
|
|
695
|
+
if (isRoleSlug(row.role)) return item;
|
|
696
|
+
const slug = slugifyRoleName(row.label);
|
|
697
|
+
return isRoleSlug(slug) ? {
|
|
698
|
+
...row,
|
|
699
|
+
role: slug
|
|
700
|
+
} : item;
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
function hasDuplicateRoleNames(value) {
|
|
704
|
+
if (!Array.isArray(value)) return false;
|
|
705
|
+
const seen = /* @__PURE__ */ new Set();
|
|
706
|
+
for (const item of value) {
|
|
707
|
+
if (!item || typeof item !== "object") continue;
|
|
708
|
+
const role = "role" in item && typeof item.role === "string" ? item.role : "";
|
|
709
|
+
const name = ("label" in item && typeof item.label === "string" ? item.label : "").trim() || (isRole(role) ? ROLE_LABELS[role] : "");
|
|
710
|
+
if (!name) continue;
|
|
711
|
+
const key = name.toLowerCase();
|
|
712
|
+
if (seen.has(key)) return true;
|
|
713
|
+
seen.add(key);
|
|
714
|
+
}
|
|
715
|
+
return false;
|
|
716
|
+
}
|
|
717
|
+
function hasInvalidRoleName(value) {
|
|
718
|
+
if (!Array.isArray(value)) return false;
|
|
719
|
+
for (const item of value) {
|
|
720
|
+
if (!item || typeof item !== "object") continue;
|
|
721
|
+
const label = "label" in item && typeof item.label === "string" ? item.label.trim() : "";
|
|
722
|
+
if (!label) continue;
|
|
723
|
+
if (!isRoleName(label)) return true;
|
|
724
|
+
}
|
|
725
|
+
return false;
|
|
726
|
+
}
|
|
727
|
+
function validateRolesMatrix(value) {
|
|
728
|
+
if (hasDuplicateRoleNames(value)) return DUPLICATE_ROLE_NAME_MESSAGE;
|
|
729
|
+
if (hasInvalidRoleName(value)) return INVALID_ROLE_NAME_MESSAGE;
|
|
730
|
+
const parsed = parseRolesMatrix(applyRoleSlugsFromNames(value));
|
|
731
|
+
if (!parsed.ok) return parsed.message;
|
|
732
|
+
return true;
|
|
359
733
|
}
|
|
360
734
|
/**
|
|
361
|
-
*
|
|
362
|
-
*
|
|
735
|
+
* Developer is exclusive. Admin does not swallow Designer: both store.
|
|
736
|
+
* Unknown slugs (dropped catalogue keys such as `approver`) fall away unless
|
|
737
|
+
* they appear on the matrix.
|
|
363
738
|
*/
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
739
|
+
function normalizeStoredRoles(value, matrix = DEFAULT_ROLE_MATRIX) {
|
|
740
|
+
if (!Array.isArray(value)) return [];
|
|
741
|
+
const allowed = new Set(matrix.map((row) => row.role));
|
|
742
|
+
const roles = [...new Set(value.filter((entry) => typeof entry === "string" && allowed.has(entry)))];
|
|
743
|
+
if (roles.includes("developer")) return ["developer"];
|
|
744
|
+
return roles;
|
|
745
|
+
}
|
|
746
|
+
function roleSelectOptions(matrix = DEFAULT_ROLE_MATRIX) {
|
|
747
|
+
return matrix.map((row) => ({
|
|
748
|
+
label: roleLabel(row.role, row.label),
|
|
749
|
+
value: row.role
|
|
750
|
+
}));
|
|
751
|
+
}
|
|
752
|
+
/** Grant copy only. Developer names the unreleased catalogue; nothing about MCP or seed. */
|
|
753
|
+
function roleDescription(role, matrix = DEFAULT_ROLE_MATRIX) {
|
|
754
|
+
if (role === "developer") return DEVELOPER_DESCRIPTION;
|
|
755
|
+
const row = matrix.find((entry) => entry.role === role);
|
|
756
|
+
if (!row) return "No capabilities";
|
|
757
|
+
return row.grants.map((slug) => packageFeatureLabel(slug)).join(", ") || "No capabilities";
|
|
758
|
+
}
|
|
759
|
+
/**
|
|
760
|
+
* Seed grants and package labels come back; extra rows stay as they are.
|
|
761
|
+
*/
|
|
762
|
+
function resetRolesMatrix(current) {
|
|
763
|
+
const extras = [];
|
|
764
|
+
if (Array.isArray(current)) for (const item of current) {
|
|
765
|
+
if (!item || typeof item !== "object") continue;
|
|
766
|
+
const parsed = readRoleRow(item);
|
|
767
|
+
if ("error" in parsed || isRole(parsed.role)) continue;
|
|
768
|
+
extras.push(parsed);
|
|
769
|
+
}
|
|
770
|
+
return [...defaultRolesFieldValue(), ...extras];
|
|
771
|
+
}
|
|
772
|
+
//#endregion
|
|
773
|
+
//#region src/roles/access.ts
|
|
774
|
+
function isAccessArgs$1(value) {
|
|
775
|
+
return typeof value === "object" && value !== null && "req" in value;
|
|
776
|
+
}
|
|
777
|
+
function storedRoles(user) {
|
|
778
|
+
return Array.isArray(user?.roles) ? user.roles : [];
|
|
779
|
+
}
|
|
780
|
+
function hasRole(user, role) {
|
|
781
|
+
return storedRoles(user).includes(role);
|
|
782
|
+
}
|
|
783
|
+
/** Not a tick. True only when `developer` is stored on the row. */
|
|
784
|
+
function isDeveloper(user) {
|
|
785
|
+
return hasRole(user, "developer");
|
|
786
|
+
}
|
|
787
|
+
function hasCapability(user, capability, matrix = DEFAULT_ROLE_MATRIX) {
|
|
788
|
+
if (isDeveloper(user)) return true;
|
|
789
|
+
return storedRoles(user).some((role) => {
|
|
790
|
+
const row = matrix.find((entry) => entry.role === role);
|
|
791
|
+
return row ? roleHasCapability(row, capability) : false;
|
|
374
792
|
});
|
|
375
|
-
|
|
793
|
+
}
|
|
794
|
+
function hasGrant(user, slug, matrix = DEFAULT_ROLE_MATRIX) {
|
|
795
|
+
if (isDeveloper(user)) return true;
|
|
796
|
+
return storedRoles(user).some((role) => {
|
|
797
|
+
const row = matrix.find((entry) => entry.role === role);
|
|
798
|
+
return row ? roleHasGrant(row, slug) : false;
|
|
799
|
+
});
|
|
800
|
+
}
|
|
801
|
+
/**
|
|
802
|
+
* Reads the saved Roles Global, falling back to the seed when the row is
|
|
803
|
+
* empty, missing, or unreadable. Always override-access so a Designer
|
|
804
|
+
* evaluating Theme does not have to read Settings → Roles.
|
|
805
|
+
*/
|
|
806
|
+
async function getRolesMatrix(req = {}, extras = []) {
|
|
807
|
+
const fallback = seedRoleRows(extras);
|
|
808
|
+
const findGlobal = req.payload?.findGlobal;
|
|
809
|
+
if (typeof findGlobal !== "function") return fallback;
|
|
810
|
+
try {
|
|
811
|
+
const doc = await findGlobal({
|
|
812
|
+
slug: ROLES_SLUG,
|
|
813
|
+
overrideAccess: true,
|
|
814
|
+
req
|
|
815
|
+
});
|
|
816
|
+
const parsed = parseRolesMatrix(doc && typeof doc === "object" && "roles" in doc ? doc.roles : void 0);
|
|
817
|
+
return parsed.ok ? parsed.rows : fallback;
|
|
818
|
+
} catch {
|
|
819
|
+
return fallback;
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
function capabilityPredicate(capability) {
|
|
823
|
+
function predicate(userOrArgs, matrix) {
|
|
824
|
+
if (isAccessArgs$1(userOrArgs)) {
|
|
825
|
+
const user = userOrArgs.req.user;
|
|
826
|
+
if (matrix) return hasCapability(user, capability, matrix);
|
|
827
|
+
return getRolesMatrix(userOrArgs.req).then((rows) => hasCapability(user, capability, rows));
|
|
828
|
+
}
|
|
829
|
+
return hasCapability(userOrArgs, capability, matrix ?? DEFAULT_ROLE_MATRIX);
|
|
830
|
+
}
|
|
831
|
+
return predicate;
|
|
832
|
+
}
|
|
833
|
+
const canManageContent = capabilityPredicate("content");
|
|
834
|
+
const canManageBrand = capabilityPredicate("brand");
|
|
835
|
+
const canPublish = capabilityPredicate("publish");
|
|
836
|
+
/** Users grant on any held role (Developer is implicit). */
|
|
837
|
+
const isAdmin = capabilityPredicate("users");
|
|
838
|
+
/** This role id currently has the Users grant, or is Developer. */
|
|
839
|
+
function isPrivilegedRole(role, matrix = DEFAULT_ROLE_MATRIX) {
|
|
840
|
+
if (typeof role !== "string") return false;
|
|
841
|
+
if (role === "developer") return true;
|
|
842
|
+
const row = matrix.find((entry) => entry.role === role);
|
|
843
|
+
return row ? roleHasGrant(row, "users") : false;
|
|
844
|
+
}
|
|
845
|
+
function isAuthenticated(userOrArgs) {
|
|
846
|
+
if (isAccessArgs$1(userOrArgs)) return Boolean(userOrArgs.req.user);
|
|
847
|
+
return Boolean(userOrArgs);
|
|
848
|
+
}
|
|
849
|
+
const isAdminOrSelf = async ({ req }) => {
|
|
850
|
+
if (!req.user) return false;
|
|
851
|
+
if (await isAdmin({ req })) return true;
|
|
852
|
+
return { id: { equals: req.user.id } };
|
|
853
|
+
};
|
|
854
|
+
const authenticatedOrPublished = ({ req: { user } }) => {
|
|
855
|
+
if (isAuthenticated(user)) return true;
|
|
856
|
+
return { _status: { equals: "published" } };
|
|
857
|
+
};
|
|
858
|
+
/**
|
|
859
|
+
* API tab condition. Code-locked to the `developer` slug — not a Features
|
|
860
|
+
* row and not a Role tick a client can grant.
|
|
861
|
+
*/
|
|
862
|
+
function isDeveloperTab({ req }) {
|
|
863
|
+
return isDeveloper(req.user);
|
|
864
|
+
}
|
|
865
|
+
/** Access for chrome globals (Features, Better Editor settings). */
|
|
866
|
+
function developerOnlyAccess() {
|
|
867
|
+
return {
|
|
868
|
+
read: ({ req }) => isDeveloper(req.user),
|
|
869
|
+
update: ({ req }) => isDeveloper(req.user)
|
|
870
|
+
};
|
|
871
|
+
}
|
|
872
|
+
/** `admin.hidden`: hide unless the login is Developer. */
|
|
873
|
+
function hideUnlessDeveloper({ user }) {
|
|
874
|
+
return !isDeveloper(user);
|
|
875
|
+
}
|
|
876
|
+
//#endregion
|
|
877
|
+
//#region src/features/access.ts
|
|
878
|
+
function isAccessArgs(value) {
|
|
879
|
+
return typeof value === "object" && value !== null && "req" in value;
|
|
880
|
+
}
|
|
881
|
+
function grantedOnReleased(user, slug, row, matrix) {
|
|
882
|
+
if (!row.released) return false;
|
|
883
|
+
return hasGrant(user, slug, matrix);
|
|
884
|
+
}
|
|
885
|
+
/**
|
|
886
|
+
* True when the feature is released and a held role is granted it.
|
|
887
|
+
* Developer is always allowed. An empty Global falls back to catalogue
|
|
888
|
+
* defaults. A group slug (`pages`, `theme`) is true when any child is.
|
|
889
|
+
*/
|
|
890
|
+
function hasFeature(user, slug, features = null, matrix = DEFAULT_ROLE_MATRIX) {
|
|
891
|
+
if (isDeveloper(user)) return true;
|
|
892
|
+
if (!user) return false;
|
|
893
|
+
const rows = features && features.length > 0 ? features : defaultFeaturesFieldValue();
|
|
894
|
+
const leaf = rows.find((entry) => entry.slug === slug);
|
|
895
|
+
if (leaf) return grantedOnReleased(user, slug, leaf, matrix);
|
|
896
|
+
if (!isGroupOnlySlug(slug)) return false;
|
|
897
|
+
return rows.some((entry) => entry.group === slug && grantedOnReleased(user, entry.slug, entry, matrix));
|
|
898
|
+
}
|
|
899
|
+
/**
|
|
900
|
+
* Reads the saved Features Global. `null` means empty or unreadable — callers
|
|
901
|
+
* fall back to catalogue defaults. Always override-access.
|
|
902
|
+
*/
|
|
903
|
+
async function getFeaturesMatrix(req = {}) {
|
|
904
|
+
const findGlobal = req.payload?.findGlobal;
|
|
905
|
+
if (typeof findGlobal !== "function") return null;
|
|
906
|
+
try {
|
|
907
|
+
const doc = await findGlobal({
|
|
908
|
+
slug: FEATURES_SLUG,
|
|
909
|
+
overrideAccess: true,
|
|
910
|
+
req
|
|
911
|
+
});
|
|
912
|
+
const parsed = parseFeaturesMatrix(doc && typeof doc === "object" && "features" in doc ? doc.features : void 0);
|
|
913
|
+
return parsed.ok ? parsed.rows : null;
|
|
914
|
+
} catch {
|
|
915
|
+
return null;
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
/**
|
|
919
|
+
* Access / nav helper for one catalogue slug. Sync against a passed grid;
|
|
920
|
+
* async when given `req` so it can read both Globals.
|
|
921
|
+
*/
|
|
922
|
+
function canUseFeature(slug) {
|
|
923
|
+
function predicate(userOrArgs, features, roles) {
|
|
924
|
+
if (isAccessArgs(userOrArgs)) {
|
|
925
|
+
const user = userOrArgs.req.user;
|
|
926
|
+
if (features !== void 0) return hasFeature(user, slug, features, roles ?? DEFAULT_ROLE_MATRIX);
|
|
927
|
+
return Promise.all([getFeaturesMatrix(userOrArgs.req), getRolesMatrix(userOrArgs.req)]).then(([grid, matrix]) => hasFeature(user, slug, grid, matrix));
|
|
928
|
+
}
|
|
929
|
+
return hasFeature(userOrArgs, slug, features ?? null, roles ?? DEFAULT_ROLE_MATRIX);
|
|
930
|
+
}
|
|
931
|
+
return predicate;
|
|
932
|
+
}
|
|
933
|
+
/**
|
|
934
|
+
* Feature slugs (and group slugs) this login may use. Written onto the
|
|
935
|
+
* user at afterRead so `admin.hidden({ user })` can follow the saved
|
|
936
|
+
* Features and Roles Globals without a `req`.
|
|
937
|
+
*/
|
|
938
|
+
async function allowedFeaturesForUser(user, req = {}) {
|
|
939
|
+
const [features, roles] = await Promise.all([getFeaturesMatrix(req), getRolesMatrix(req)]);
|
|
940
|
+
return [...(features && features.length > 0 ? features : defaultFeaturesFieldValue()).map((row) => row.slug), ...FEATURE_GROUPS].filter((slug) => hasFeature(user, slug, features, roles));
|
|
941
|
+
}
|
|
942
|
+
function userHasAllowedFeature(user, slug) {
|
|
943
|
+
if (isDeveloper(user)) return true;
|
|
944
|
+
const allowed = user?.allowedFeatures;
|
|
945
|
+
if (!Array.isArray(allowed)) return false;
|
|
946
|
+
return allowed.includes(slug);
|
|
947
|
+
}
|
|
948
|
+
/** `admin.hidden`: hide when the login cannot use the feature. */
|
|
949
|
+
function hideUnlessFeature(slug) {
|
|
950
|
+
return (args) => {
|
|
951
|
+
const user = args.user ?? null;
|
|
952
|
+
if (isDeveloper(user)) return false;
|
|
953
|
+
if (user && Array.isArray(user.allowedFeatures)) return !userHasAllowedFeature(user, slug);
|
|
954
|
+
if (args.req) {
|
|
955
|
+
const result = canUseFeature(slug)({ req: {
|
|
956
|
+
...args.req,
|
|
957
|
+
user
|
|
958
|
+
} });
|
|
959
|
+
if (result instanceof Promise) return result.then((ok) => !ok);
|
|
960
|
+
return !result;
|
|
961
|
+
}
|
|
962
|
+
return !canUseFeature(slug)(user);
|
|
963
|
+
};
|
|
376
964
|
}
|
|
377
965
|
//#endregion
|
|
378
966
|
//#region src/theme/global.ts
|
|
@@ -677,35 +1265,11 @@ function identityFields(collection) {
|
|
|
677
1265
|
}
|
|
678
1266
|
];
|
|
679
1267
|
}
|
|
680
|
-
function pageHooks(child) {
|
|
681
|
-
return {
|
|
682
|
-
beforeChange: [async ({ data, req }) => {
|
|
683
|
-
if (req?.payload) await persistThemeChild(req.payload, data, child);
|
|
684
|
-
return data;
|
|
685
|
-
}],
|
|
686
|
-
afterRead: [async ({ doc, req }) => {
|
|
687
|
-
if (hasThemeSlice(doc, child)) return doc;
|
|
688
|
-
const theme = await req?.payload?.findGlobal({
|
|
689
|
-
slug: THEME_SLUG,
|
|
690
|
-
draft: false,
|
|
691
|
-
depth: child === "identity" ? 1 : 0,
|
|
692
|
-
overrideAccess: true
|
|
693
|
-
});
|
|
694
|
-
if (!theme) return doc;
|
|
695
|
-
return {
|
|
696
|
-
...doc,
|
|
697
|
-
...sliceFromTheme(theme, child)
|
|
698
|
-
};
|
|
699
|
-
}]
|
|
700
|
-
};
|
|
701
|
-
}
|
|
702
1268
|
/**
|
|
703
|
-
*
|
|
704
|
-
*
|
|
705
|
-
*
|
|
706
|
-
*
|
|
707
|
-
* settings forms, not collaborative documents. No draft mode, no preview
|
|
708
|
-
* pane.
|
|
1269
|
+
* One Theme Global. Colors, Typography, Appearance, and Identity are
|
|
1270
|
+
* tabs on that document. Save writes the whole form — Payload hydrates
|
|
1271
|
+
* every tab from the live row, so an untouched tab does not revert.
|
|
1272
|
+
* Locking is off. No draft mode, no preview pane.
|
|
709
1273
|
*/
|
|
710
1274
|
function createTheme(options) {
|
|
711
1275
|
const access = requireAccess$1(options);
|
|
@@ -720,59 +1284,49 @@ function createTheme(options) {
|
|
|
720
1284
|
const typeFields = typographyFields(seed, fonts, fontsBaseUrl);
|
|
721
1285
|
const lookFields = appearanceFields(seed);
|
|
722
1286
|
const markFields = logo ? identityFields(logo.collection) : [];
|
|
723
|
-
|
|
1287
|
+
return [{
|
|
724
1288
|
slug: THEME_SLUG,
|
|
725
1289
|
label: "Theme",
|
|
726
1290
|
lockDocuments: false,
|
|
727
1291
|
admin: {
|
|
728
|
-
|
|
1292
|
+
group: "Theme",
|
|
1293
|
+
hidden: hideUnlessFeature("theme"),
|
|
729
1294
|
hideAPIURL: true,
|
|
730
|
-
custom: adminCustom
|
|
1295
|
+
custom: adminCustom,
|
|
1296
|
+
components: { elements: { beforeDocumentControls: [THEME_DOCUMENT_CONTROLS] } }
|
|
731
1297
|
},
|
|
732
1298
|
access: {
|
|
733
1299
|
read: access.read,
|
|
734
1300
|
update: access.update
|
|
735
1301
|
},
|
|
736
|
-
fields: [
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
1302
|
+
fields: [{
|
|
1303
|
+
type: "tabs",
|
|
1304
|
+
tabs: [
|
|
1305
|
+
{
|
|
1306
|
+
label: "Colors",
|
|
1307
|
+
fields: colorFields
|
|
1308
|
+
},
|
|
1309
|
+
{
|
|
1310
|
+
label: "Typography",
|
|
1311
|
+
fields: typeFields
|
|
1312
|
+
},
|
|
1313
|
+
{
|
|
1314
|
+
label: "Appearance",
|
|
1315
|
+
fields: lookFields
|
|
1316
|
+
},
|
|
1317
|
+
...logo ? [{
|
|
1318
|
+
label: "Identity",
|
|
1319
|
+
fields: markFields
|
|
1320
|
+
}] : []
|
|
1321
|
+
]
|
|
1322
|
+
}],
|
|
742
1323
|
endpoints: colorUsageEndpoints(access, colorUsages),
|
|
743
1324
|
hooks: { afterChange: [async ({ doc }) => {
|
|
744
1325
|
if (!onPublish) return;
|
|
745
1326
|
const themeDoc = doc;
|
|
746
1327
|
await onPublish(themeConfigFromDoc(themeDoc, seed), themeDoc);
|
|
747
1328
|
}] }
|
|
748
|
-
};
|
|
749
|
-
function page(slug, label, child, fields, preview) {
|
|
750
|
-
return {
|
|
751
|
-
slug,
|
|
752
|
-
label,
|
|
753
|
-
lockDocuments: false,
|
|
754
|
-
admin: {
|
|
755
|
-
group: "Theme",
|
|
756
|
-
hidden: hideUnlessFeature("theme"),
|
|
757
|
-
hideAPIURL: true,
|
|
758
|
-
custom: adminCustom,
|
|
759
|
-
components: preview ? { elements: { beforeDocumentControls: [THEME_DOCUMENT_CONTROLS] } } : void 0
|
|
760
|
-
},
|
|
761
|
-
access: {
|
|
762
|
-
read: access.read,
|
|
763
|
-
update: access.update
|
|
764
|
-
},
|
|
765
|
-
fields,
|
|
766
|
-
hooks: pageHooks(child)
|
|
767
|
-
};
|
|
768
|
-
}
|
|
769
|
-
return [
|
|
770
|
-
store,
|
|
771
|
-
page(THEME_COLORS_SLUG, "Colors", "colors", colorFields, true),
|
|
772
|
-
page(THEME_TYPOGRAPHY_SLUG, "Typography", "typography", typeFields, true),
|
|
773
|
-
page(THEME_APPEARANCE_SLUG, "Appearance", "appearance", lookFields, true),
|
|
774
|
-
...logo ? [page(THEME_IDENTITY_SLUG, "Identity", "identity", markFields, false)] : []
|
|
775
|
-
];
|
|
1329
|
+
}];
|
|
776
1330
|
}
|
|
777
1331
|
//#endregion
|
|
778
1332
|
//#region src/brand-assets/sanitize.ts
|
|
@@ -840,6 +1394,7 @@ function createBrandAssets(options) {
|
|
|
840
1394
|
admin: {
|
|
841
1395
|
group: "Settings",
|
|
842
1396
|
useAsTitle: "label",
|
|
1397
|
+
hidden: hideUnlessFeature("brand-assets"),
|
|
843
1398
|
description: "Logo, favicon, and mobile-menu mark. SVG preferred; PNG and ICO allowed."
|
|
844
1399
|
},
|
|
845
1400
|
upload: {
|
|
@@ -921,6 +1476,52 @@ const THEME_PREVIEW_BREAKPOINTS = [
|
|
|
921
1476
|
}
|
|
922
1477
|
];
|
|
923
1478
|
//#endregion
|
|
1479
|
+
//#region src/theme/publish.ts
|
|
1480
|
+
/**
|
|
1481
|
+
* Publishing a child writes that slice only. Other responsibilities stay
|
|
1482
|
+
* as they already are on the stored document. Unsaved form state on a
|
|
1483
|
+
* different child never goes live. A first save (no stored row) still
|
|
1484
|
+
* writes only the named child; `getPublishedTheme` fills the rest from
|
|
1485
|
+
* the seed.
|
|
1486
|
+
*/
|
|
1487
|
+
function publishThemeChild(stored, incoming, child) {
|
|
1488
|
+
if (child === "colors") return {
|
|
1489
|
+
...stored,
|
|
1490
|
+
colors: incoming.colors
|
|
1491
|
+
};
|
|
1492
|
+
if (child === "typography") return {
|
|
1493
|
+
...stored,
|
|
1494
|
+
typography: incoming.typography
|
|
1495
|
+
};
|
|
1496
|
+
if (child === "appearance") return {
|
|
1497
|
+
...stored,
|
|
1498
|
+
appearance: incoming.appearance
|
|
1499
|
+
};
|
|
1500
|
+
return {
|
|
1501
|
+
...stored,
|
|
1502
|
+
logo: incoming.logo,
|
|
1503
|
+
favicon: incoming.favicon,
|
|
1504
|
+
logoMark: incoming.logoMark
|
|
1505
|
+
};
|
|
1506
|
+
}
|
|
1507
|
+
/**
|
|
1508
|
+
* Save on a Theme page writes that slice onto the hidden `theme` row.
|
|
1509
|
+
* Other children stay as stored.
|
|
1510
|
+
*/
|
|
1511
|
+
async function persistThemeChild(payload, incoming, child) {
|
|
1512
|
+
const next = publishThemeChild(await payload.findGlobal({
|
|
1513
|
+
slug: "theme",
|
|
1514
|
+
draft: false,
|
|
1515
|
+
overrideAccess: true
|
|
1516
|
+
}) ?? {}, incoming, child);
|
|
1517
|
+
await payload.updateGlobal({
|
|
1518
|
+
slug: THEME_SLUG,
|
|
1519
|
+
data: next,
|
|
1520
|
+
draft: false
|
|
1521
|
+
});
|
|
1522
|
+
return next;
|
|
1523
|
+
}
|
|
1524
|
+
//#endregion
|
|
924
1525
|
//#region src/roles/fields.ts
|
|
925
1526
|
const ROLES_MATRIX_FIELD = "@bison-lab/payload-core/admin#RolesMatrixField";
|
|
926
1527
|
const ROLES_ROW_LABEL = "@bison-lab/payload-core/admin#RolesRowLabel";
|
|
@@ -1030,69 +1631,67 @@ const FEATURES_MATRIX_FIELD = "@bison-lab/payload-core/admin#FeaturesMatrixField
|
|
|
1030
1631
|
*/
|
|
1031
1632
|
function createFeatures({ extras = [] } = {}) {
|
|
1032
1633
|
const catalogue = featureCatalogue(extras);
|
|
1634
|
+
const featuresField = {
|
|
1635
|
+
name: "features",
|
|
1636
|
+
type: "array",
|
|
1637
|
+
label: "Features",
|
|
1638
|
+
labels: {
|
|
1639
|
+
singular: "Feature",
|
|
1640
|
+
plural: "Features"
|
|
1641
|
+
},
|
|
1642
|
+
minRows: catalogue.length,
|
|
1643
|
+
maxRows: catalogue.length,
|
|
1644
|
+
required: true,
|
|
1645
|
+
defaultValue: defaultFeaturesFieldValue(extras),
|
|
1646
|
+
validate: (value) => validateFeaturesMatrix(value, extras),
|
|
1647
|
+
hooks: { beforeChange: [({ value }) => {
|
|
1648
|
+
if (!Array.isArray(value)) return value;
|
|
1649
|
+
return stampFeatureCatalogue(value, extras);
|
|
1650
|
+
}] },
|
|
1651
|
+
admin: {
|
|
1652
|
+
components: { Field: FEATURES_MATRIX_FIELD },
|
|
1653
|
+
description: "Release a feature so it can be assigned on Roles. Off keeps it Developer-only.",
|
|
1654
|
+
initCollapsed: false
|
|
1655
|
+
},
|
|
1656
|
+
fields: [
|
|
1657
|
+
{
|
|
1658
|
+
name: "slug",
|
|
1659
|
+
type: "text",
|
|
1660
|
+
label: "Slug",
|
|
1661
|
+
required: true,
|
|
1662
|
+
admin: { readOnly: true }
|
|
1663
|
+
},
|
|
1664
|
+
{
|
|
1665
|
+
name: "label",
|
|
1666
|
+
type: "text",
|
|
1667
|
+
label: "Name",
|
|
1668
|
+
required: true,
|
|
1669
|
+
admin: { readOnly: true }
|
|
1670
|
+
},
|
|
1671
|
+
{
|
|
1672
|
+
name: "group",
|
|
1673
|
+
type: "text",
|
|
1674
|
+
label: "Group",
|
|
1675
|
+
admin: { hidden: true }
|
|
1676
|
+
},
|
|
1677
|
+
{
|
|
1678
|
+
name: "released",
|
|
1679
|
+
type: "checkbox",
|
|
1680
|
+
label: "Released",
|
|
1681
|
+
admin: { hidden: true }
|
|
1682
|
+
}
|
|
1683
|
+
]
|
|
1684
|
+
};
|
|
1033
1685
|
return {
|
|
1034
1686
|
slug: FEATURES_SLUG,
|
|
1035
1687
|
label: "Features",
|
|
1036
1688
|
admin: {
|
|
1037
1689
|
group: "Settings",
|
|
1038
|
-
hidden:
|
|
1690
|
+
hidden: hideUnlessDeveloper,
|
|
1039
1691
|
description: "Which features other people may be granted on Roles."
|
|
1040
1692
|
},
|
|
1041
|
-
access:
|
|
1042
|
-
|
|
1043
|
-
update: ({ req }) => isDeveloper(req.user)
|
|
1044
|
-
},
|
|
1045
|
-
fields: [{
|
|
1046
|
-
name: "features",
|
|
1047
|
-
type: "array",
|
|
1048
|
-
label: "Features",
|
|
1049
|
-
labels: {
|
|
1050
|
-
singular: "Feature",
|
|
1051
|
-
plural: "Features"
|
|
1052
|
-
},
|
|
1053
|
-
minRows: catalogue.length,
|
|
1054
|
-
maxRows: catalogue.length,
|
|
1055
|
-
required: true,
|
|
1056
|
-
defaultValue: defaultFeaturesFieldValue(extras),
|
|
1057
|
-
validate: (value) => validateFeaturesMatrix(value, extras),
|
|
1058
|
-
hooks: { beforeChange: [({ value }) => {
|
|
1059
|
-
if (!Array.isArray(value)) return value;
|
|
1060
|
-
return stampFeatureCatalogue(value, extras);
|
|
1061
|
-
}] },
|
|
1062
|
-
admin: {
|
|
1063
|
-
components: { Field: FEATURES_MATRIX_FIELD },
|
|
1064
|
-
description: "Release a feature so it can be assigned on Roles. Off keeps it Developer-only.",
|
|
1065
|
-
initCollapsed: false
|
|
1066
|
-
},
|
|
1067
|
-
fields: [
|
|
1068
|
-
{
|
|
1069
|
-
name: "slug",
|
|
1070
|
-
type: "text",
|
|
1071
|
-
label: "Slug",
|
|
1072
|
-
required: true,
|
|
1073
|
-
admin: { readOnly: true }
|
|
1074
|
-
},
|
|
1075
|
-
{
|
|
1076
|
-
name: "label",
|
|
1077
|
-
type: "text",
|
|
1078
|
-
label: "Name",
|
|
1079
|
-
required: true,
|
|
1080
|
-
admin: { readOnly: true }
|
|
1081
|
-
},
|
|
1082
|
-
{
|
|
1083
|
-
name: "group",
|
|
1084
|
-
type: "text",
|
|
1085
|
-
label: "Group",
|
|
1086
|
-
admin: { hidden: true }
|
|
1087
|
-
},
|
|
1088
|
-
{
|
|
1089
|
-
name: "released",
|
|
1090
|
-
type: "checkbox",
|
|
1091
|
-
label: "Released",
|
|
1092
|
-
admin: { hidden: true }
|
|
1093
|
-
}
|
|
1094
|
-
]
|
|
1095
|
-
}]
|
|
1693
|
+
access: developerOnlyAccess(),
|
|
1694
|
+
fields: [featuresField]
|
|
1096
1695
|
};
|
|
1097
1696
|
}
|
|
1098
1697
|
//#endregion
|
|
@@ -1189,6 +1788,15 @@ function slugField({ collection, isReserved }) {
|
|
|
1189
1788
|
//#endregion
|
|
1190
1789
|
//#region src/collections/pages.ts
|
|
1191
1790
|
/**
|
|
1791
|
+
* Content to create or save a draft; Publish to publish or edit a live
|
|
1792
|
+
* page. Without Publish, update is constrained to `_status: draft`.
|
|
1793
|
+
*/
|
|
1794
|
+
const pagesUpdate = async (args) => {
|
|
1795
|
+
if (!await Promise.resolve(canUseFeature("content")(args))) return false;
|
|
1796
|
+
if (await Promise.resolve(canPublish(args))) return true;
|
|
1797
|
+
return { _status: { equals: "draft" } };
|
|
1798
|
+
};
|
|
1799
|
+
/**
|
|
1192
1800
|
* CMS-managed pages: one required hero, then a reorderable body of sections.
|
|
1193
1801
|
* The CMS owns copy and the order of sections; what a section looks like is
|
|
1194
1802
|
* code-owned, which is why the blocks are an argument.
|
|
@@ -1215,7 +1823,7 @@ function createPages({ heroBlocks, isReservedSlug, layoutBlocks, previewPath, pr
|
|
|
1215
1823
|
read: authenticatedOrPublished,
|
|
1216
1824
|
readVersions: isAuthenticated,
|
|
1217
1825
|
create: canUseFeature("content"),
|
|
1218
|
-
update:
|
|
1826
|
+
update: pagesUpdate,
|
|
1219
1827
|
delete: isAdmin
|
|
1220
1828
|
},
|
|
1221
1829
|
versions: {
|
|
@@ -1374,27 +1982,45 @@ function createUsers({ secureCookies, rolesField, extras = [] }) {
|
|
|
1374
1982
|
return arg.result;
|
|
1375
1983
|
}]
|
|
1376
1984
|
},
|
|
1377
|
-
fields: [
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
name: "roles",
|
|
1382
|
-
type: "select",
|
|
1383
|
-
hasMany: true,
|
|
1384
|
-
required: true,
|
|
1385
|
-
defaultValue: ["author"],
|
|
1386
|
-
options: roleSelectOptions(fallback),
|
|
1387
|
-
admin: {
|
|
1388
|
-
components: { Field: rolesField ?? "@bison-lab/payload-core/admin#RolesField" },
|
|
1389
|
-
description: "One person can hold several, e.g. Author + Designer."
|
|
1985
|
+
fields: [
|
|
1986
|
+
{
|
|
1987
|
+
type: "row",
|
|
1988
|
+
fields: [nameField("firstName"), nameField("lastName")]
|
|
1390
1989
|
},
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1990
|
+
{
|
|
1991
|
+
name: "roles",
|
|
1992
|
+
type: "select",
|
|
1993
|
+
hasMany: true,
|
|
1994
|
+
required: true,
|
|
1995
|
+
defaultValue: ["author"],
|
|
1996
|
+
saveToJWT: true,
|
|
1997
|
+
options: roleSelectOptions(fallback),
|
|
1998
|
+
admin: {
|
|
1999
|
+
components: { Field: rolesField ?? "@bison-lab/payload-core/admin#RolesField" },
|
|
2000
|
+
description: "One person can hold several, e.g. Author + Designer."
|
|
2001
|
+
},
|
|
2002
|
+
access: { update: isAdmin },
|
|
2003
|
+
validate: rolesValidate(extras),
|
|
2004
|
+
hooks: { beforeValidate: [async ({ value, req }) => {
|
|
2005
|
+
if (!Array.isArray(value)) return value;
|
|
2006
|
+
return normalizeStoredRoles(value, await getRolesMatrix(req, extras));
|
|
2007
|
+
}] }
|
|
2008
|
+
},
|
|
2009
|
+
{
|
|
2010
|
+
name: "allowedFeatures",
|
|
2011
|
+
type: "json",
|
|
2012
|
+
admin: {
|
|
2013
|
+
hidden: true,
|
|
2014
|
+
readOnly: true
|
|
2015
|
+
},
|
|
2016
|
+
access: { update: () => false },
|
|
2017
|
+
saveToJWT: true,
|
|
2018
|
+
hooks: { afterRead: [async ({ siblingData, data, req }) => allowedFeaturesForUser({
|
|
2019
|
+
id: siblingData?.id ?? data?.id,
|
|
2020
|
+
roles: siblingData?.roles ?? data?.roles
|
|
2021
|
+
}, req ?? {})] }
|
|
2022
|
+
}
|
|
2023
|
+
]
|
|
1398
2024
|
};
|
|
1399
2025
|
}
|
|
1400
2026
|
//#endregion
|
|
@@ -1470,6 +2096,6 @@ const adminOnlyApiTab = definePlugin({
|
|
|
1470
2096
|
})
|
|
1471
2097
|
});
|
|
1472
2098
|
//#endregion
|
|
1473
|
-
export { BRAND_ASSETS_MIME_TYPES, BRAND_ASSETS_SLUG, CAPABILITIES, CAPABILITY_FEATURES, DEFAULT_ROLE_GRANTS, DEFAULT_ROLE_MATRIX, DESCRIPTION_LENGTH, DEVELOPER_DESCRIPTION, 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, 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, adminOnlyApiTab, applyRoleSlugsFromNames, authenticatedOrPublished, canManageBrand, canManageContent, canPublish, canUseFeature, colorTokenField, createBrandAssets, createFeatures, createMedia, createPages, createRoles, createTheme, createUsers, defaultFeaturesFieldValue, defaultRolesFieldValue, deleteLibraryColor, documentTitle, featureCatalogue, featureGroupLabel, findColorTokens, findColorUsages, firstImageIn, getFeaturesMatrix, getRolesMatrix, hasCapability, hasFeature, hasGrant, hasRole, 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, resolveThemeIdentity, rewriteColorToken, rewriteColorTokens, rewriteColorUsages, roleDescription, roleLabel, roleSelectOptions, sanitizeRoleNameInput, sanitizeSvg, seedFeatures, seedRoleRows, seedRoles, seedTheme, seoPlugin, slugField, slugifyRoleName, stampFeatureCatalogue, storedRoles, themeColorKeys, themeLibraryFromDoc, titleTemplate, truncateAtWord, validateFeaturesMatrix, validateRolesMatrix };
|
|
2099
|
+
export { BRAND_ASSETS_MIME_TYPES, BRAND_ASSETS_SLUG, CAPABILITIES, CAPABILITY_FEATURES, DEFAULT_ROLE_GRANTS, DEFAULT_ROLE_MATRIX, DESCRIPTION_LENGTH, DEVELOPER_DESCRIPTION, 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, 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, adminOnlyApiTab, allowedFeaturesForUser, applyRoleSlugsFromNames, authenticatedOrPublished, canManageBrand, canManageContent, canPublish, canUseFeature, colorTokenField, createBrandAssets, createFeatures, createMedia, createPages, createRoles, createTheme, createUsers, defaultFeaturesFieldValue, defaultRolesFieldValue, deleteLibraryColor, developerOnlyAccess, documentTitle, featureCatalogue, featureGroupLabel, findColorTokens, findColorUsages, firstImageIn, getFeaturesMatrix, getRolesMatrix, hasCapability, hasFeature, hasGrant, hasRole, 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, resolveThemeIdentity, rewriteColorToken, rewriteColorTokens, rewriteColorUsages, roleDescription, roleLabel, roleSelectOptions, sanitizeRoleNameInput, sanitizeSvg, seedFeatures, seedRoleRows, seedRoles, seedTheme, seoPlugin, slugField, slugifyRoleName, stampFeatureCatalogue, storedRoles, themeColorKeys, themeLibraryFromDoc, titleTemplate, truncateAtWord, validateFeaturesMatrix, validateRolesMatrix };
|
|
1474
2100
|
|
|
1475
2101
|
//# sourceMappingURL=index.mjs.map
|