@bison-lab/payload-core 3.13.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 +57 -29
- package/dist/admin.d.mts +25 -5
- package/dist/admin.d.mts.map +1 -1
- package/dist/admin.mjs +437 -178
- package/dist/admin.mjs.map +1 -1
- package/dist/index.d.mts +93 -50
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +849 -725
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
package/dist/index.mjs
CHANGED
|
@@ -303,75 +303,664 @@ async function readJson(req) {
|
|
|
303
303
|
return null;
|
|
304
304
|
}
|
|
305
305
|
//#endregion
|
|
306
|
-
//#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
|
+
];
|
|
307
332
|
/**
|
|
308
|
-
*
|
|
309
|
-
*
|
|
310
|
-
* different child never goes live. A first save (no stored row) still
|
|
311
|
-
* writes only the named child; `getPublishedTheme` fills the rest from
|
|
312
|
-
* 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.
|
|
313
335
|
*/
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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
|
|
318
452
|
};
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
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
|
|
322
474
|
};
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
475
|
+
return {
|
|
476
|
+
ok: true,
|
|
477
|
+
rows
|
|
326
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 };
|
|
327
639
|
return {
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
640
|
+
id: role,
|
|
641
|
+
role,
|
|
642
|
+
label: roleLabel(role, "label" in item && typeof item.label === "string" ? item.label : void 0),
|
|
643
|
+
grants: grantsFromStoredRole(item)
|
|
332
644
|
};
|
|
333
645
|
}
|
|
334
|
-
function
|
|
335
|
-
if (
|
|
336
|
-
|
|
337
|
-
|
|
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
|
+
};
|
|
338
673
|
return {
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
logoMark: theme.logoMark
|
|
674
|
+
ok: true,
|
|
675
|
+
rows
|
|
342
676
|
};
|
|
343
677
|
}
|
|
344
|
-
function
|
|
345
|
-
if (
|
|
346
|
-
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));
|
|
347
685
|
}
|
|
348
686
|
/**
|
|
349
|
-
*
|
|
350
|
-
*
|
|
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.
|
|
351
689
|
*/
|
|
352
|
-
function
|
|
353
|
-
if (!
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
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;
|
|
358
733
|
}
|
|
359
734
|
/**
|
|
360
|
-
*
|
|
361
|
-
*
|
|
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.
|
|
362
738
|
*/
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
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;
|
|
373
792
|
});
|
|
374
|
-
|
|
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
|
+
};
|
|
375
964
|
}
|
|
376
965
|
//#endregion
|
|
377
966
|
//#region src/theme/global.ts
|
|
@@ -671,40 +1260,16 @@ function identityFields(collection) {
|
|
|
671
1260
|
name: "logoMark",
|
|
672
1261
|
type: "upload",
|
|
673
1262
|
relationTo: collection,
|
|
674
|
-
label: "Mobile menu logo",
|
|
675
|
-
admin: { description: "Optional. Used when the full logo is too wide for the mobile menu — typically the mark." }
|
|
676
|
-
}
|
|
677
|
-
];
|
|
678
|
-
}
|
|
679
|
-
function pageHooks(child) {
|
|
680
|
-
return {
|
|
681
|
-
beforeChange: [async ({ data, req }) => {
|
|
682
|
-
if (req?.payload) await persistThemeChild(req.payload, data, child);
|
|
683
|
-
return data;
|
|
684
|
-
}],
|
|
685
|
-
afterRead: [async ({ doc, req }) => {
|
|
686
|
-
if (hasThemeSlice(doc, child)) return doc;
|
|
687
|
-
const theme = await req?.payload?.findGlobal({
|
|
688
|
-
slug: THEME_SLUG,
|
|
689
|
-
draft: false,
|
|
690
|
-
depth: child === "identity" ? 1 : 0,
|
|
691
|
-
overrideAccess: true
|
|
692
|
-
});
|
|
693
|
-
if (!theme) return doc;
|
|
694
|
-
return {
|
|
695
|
-
...doc,
|
|
696
|
-
...sliceFromTheme(theme, child)
|
|
697
|
-
};
|
|
698
|
-
}]
|
|
699
|
-
};
|
|
1263
|
+
label: "Mobile menu logo",
|
|
1264
|
+
admin: { description: "Optional. Used when the full logo is too wide for the mobile menu — typically the mark." }
|
|
1265
|
+
}
|
|
1266
|
+
];
|
|
700
1267
|
}
|
|
701
1268
|
/**
|
|
702
|
-
*
|
|
703
|
-
*
|
|
704
|
-
*
|
|
705
|
-
*
|
|
706
|
-
* settings forms, not collaborative documents. No draft mode, no preview
|
|
707
|
-
* 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.
|
|
708
1273
|
*/
|
|
709
1274
|
function createTheme(options) {
|
|
710
1275
|
const access = requireAccess$1(options);
|
|
@@ -719,58 +1284,49 @@ function createTheme(options) {
|
|
|
719
1284
|
const typeFields = typographyFields(seed, fonts, fontsBaseUrl);
|
|
720
1285
|
const lookFields = appearanceFields(seed);
|
|
721
1286
|
const markFields = logo ? identityFields(logo.collection) : [];
|
|
722
|
-
|
|
1287
|
+
return [{
|
|
723
1288
|
slug: THEME_SLUG,
|
|
724
1289
|
label: "Theme",
|
|
725
1290
|
lockDocuments: false,
|
|
726
1291
|
admin: {
|
|
727
|
-
|
|
1292
|
+
group: "Theme",
|
|
1293
|
+
hidden: hideUnlessFeature("theme"),
|
|
728
1294
|
hideAPIURL: true,
|
|
729
|
-
custom: adminCustom
|
|
1295
|
+
custom: adminCustom,
|
|
1296
|
+
components: { elements: { beforeDocumentControls: [THEME_DOCUMENT_CONTROLS] } }
|
|
730
1297
|
},
|
|
731
1298
|
access: {
|
|
732
1299
|
read: access.read,
|
|
733
1300
|
update: access.update
|
|
734
1301
|
},
|
|
735
|
-
fields: [
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
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
|
+
}],
|
|
741
1323
|
endpoints: colorUsageEndpoints(access, colorUsages),
|
|
742
1324
|
hooks: { afterChange: [async ({ doc }) => {
|
|
743
1325
|
if (!onPublish) return;
|
|
744
1326
|
const themeDoc = doc;
|
|
745
1327
|
await onPublish(themeConfigFromDoc(themeDoc, seed), themeDoc);
|
|
746
1328
|
}] }
|
|
747
|
-
};
|
|
748
|
-
function page(slug, label, child, fields, preview) {
|
|
749
|
-
return {
|
|
750
|
-
slug,
|
|
751
|
-
label,
|
|
752
|
-
lockDocuments: false,
|
|
753
|
-
admin: {
|
|
754
|
-
group: "Theme",
|
|
755
|
-
hideAPIURL: true,
|
|
756
|
-
custom: adminCustom,
|
|
757
|
-
components: preview ? { elements: { beforeDocumentControls: [THEME_DOCUMENT_CONTROLS] } } : void 0
|
|
758
|
-
},
|
|
759
|
-
access: {
|
|
760
|
-
read: access.read,
|
|
761
|
-
update: access.update
|
|
762
|
-
},
|
|
763
|
-
fields,
|
|
764
|
-
hooks: pageHooks(child)
|
|
765
|
-
};
|
|
766
|
-
}
|
|
767
|
-
return [
|
|
768
|
-
store,
|
|
769
|
-
page(THEME_COLORS_SLUG, "Colors", "colors", colorFields, true),
|
|
770
|
-
page(THEME_TYPOGRAPHY_SLUG, "Typography", "typography", typeFields, true),
|
|
771
|
-
page(THEME_APPEARANCE_SLUG, "Appearance", "appearance", lookFields, true),
|
|
772
|
-
...logo ? [page(THEME_IDENTITY_SLUG, "Identity", "identity", markFields, false)] : []
|
|
773
|
-
];
|
|
1329
|
+
}];
|
|
774
1330
|
}
|
|
775
1331
|
//#endregion
|
|
776
1332
|
//#region src/brand-assets/sanitize.ts
|
|
@@ -838,6 +1394,7 @@ function createBrandAssets(options) {
|
|
|
838
1394
|
admin: {
|
|
839
1395
|
group: "Settings",
|
|
840
1396
|
useAsTitle: "label",
|
|
1397
|
+
hidden: hideUnlessFeature("brand-assets"),
|
|
841
1398
|
description: "Logo, favicon, and mobile-menu mark. SVG preferred; PNG and ICO allowed."
|
|
842
1399
|
},
|
|
843
1400
|
upload: {
|
|
@@ -919,288 +1476,73 @@ const THEME_PREVIEW_BREAKPOINTS = [
|
|
|
919
1476
|
}
|
|
920
1477
|
];
|
|
921
1478
|
//#endregion
|
|
922
|
-
//#region src/
|
|
923
|
-
/**
|
|
924
|
-
* The Roles Global a site's generated types will describe. Optional and
|
|
925
|
-
* nullable, no index signature: a generated Global is assignable to this,
|
|
926
|
-
* never the reverse.
|
|
927
|
-
*/
|
|
928
|
-
const ROLES = [
|
|
929
|
-
"developer",
|
|
930
|
-
"admin",
|
|
931
|
-
"designer",
|
|
932
|
-
"author"
|
|
933
|
-
];
|
|
934
|
-
const ROLE_LABELS = {
|
|
935
|
-
developer: "Developer",
|
|
936
|
-
admin: "Admin",
|
|
937
|
-
designer: "Designer",
|
|
938
|
-
author: "Author"
|
|
939
|
-
};
|
|
940
|
-
const CAPABILITIES = [
|
|
941
|
-
"content",
|
|
942
|
-
"brand",
|
|
943
|
-
"publish",
|
|
944
|
-
"users"
|
|
945
|
-
];
|
|
946
|
-
function isRole(value) {
|
|
947
|
-
return typeof value === "string" && ROLES.includes(value);
|
|
948
|
-
}
|
|
949
|
-
/** Lowercase slug: `editor`, `site-editor`. Empty and punctuation are out. */
|
|
950
|
-
function isRoleSlug(value) {
|
|
951
|
-
return typeof value === "string" && /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/.test(value);
|
|
952
|
-
}
|
|
953
|
-
function roleLabel(role, label) {
|
|
954
|
-
const trimmed = typeof label === "string" ? label.trim() : "";
|
|
955
|
-
if (trimmed) return trimmed;
|
|
956
|
-
return isRole(role) ? ROLE_LABELS[role] : role;
|
|
957
|
-
}
|
|
958
|
-
//#endregion
|
|
959
|
-
//#region src/roles/matrix.ts
|
|
960
|
-
const ROLES_SLUG = "roles";
|
|
961
|
-
const ROLES_GLOBAL_DESCRIPTION = "Who may do what. Drag to change rank.";
|
|
962
|
-
const ROLES_FIELD_DESCRIPTION = "Drag to change rank. Ticks are Content, Brand, Publish, and Users.";
|
|
1479
|
+
//#region src/theme/publish.ts
|
|
963
1480
|
/**
|
|
964
|
-
*
|
|
965
|
-
*
|
|
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.
|
|
966
1486
|
*/
|
|
967
|
-
|
|
968
|
-
{
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
brand: true,
|
|
972
|
-
publish: true,
|
|
973
|
-
users: true
|
|
974
|
-
},
|
|
975
|
-
{
|
|
976
|
-
role: "admin",
|
|
977
|
-
content: true,
|
|
978
|
-
brand: false,
|
|
979
|
-
publish: true,
|
|
980
|
-
users: true
|
|
981
|
-
},
|
|
982
|
-
{
|
|
983
|
-
role: "designer",
|
|
984
|
-
content: false,
|
|
985
|
-
brand: true,
|
|
986
|
-
publish: false,
|
|
987
|
-
users: false
|
|
988
|
-
},
|
|
989
|
-
{
|
|
990
|
-
role: "author",
|
|
991
|
-
content: true,
|
|
992
|
-
brand: false,
|
|
993
|
-
publish: false,
|
|
994
|
-
users: false
|
|
995
|
-
}
|
|
996
|
-
];
|
|
997
|
-
const DEVELOPER_DESCRIPTION = "Everything Admin can do, plus the document API tab.";
|
|
998
|
-
const LAST_USERS_TICK_MESSAGE = "Keep Users ticked on at least one of Admin or Developer.";
|
|
999
|
-
const MISSING_SEED_ROLES_MESSAGE = "Roles must include Developer, Admin, Designer, and Author.";
|
|
1000
|
-
const DUPLICATE_ROLE_SLUG_MESSAGE = "Each role needs a unique slug.";
|
|
1001
|
-
const CAPABILITY_LABELS = {
|
|
1002
|
-
content: "Content",
|
|
1003
|
-
brand: "Brand",
|
|
1004
|
-
publish: "Publish",
|
|
1005
|
-
users: "Users"
|
|
1006
|
-
};
|
|
1007
|
-
function seedRoleRows(extras = []) {
|
|
1008
|
-
return [...DEFAULT_ROLE_MATRIX.map((row) => ({
|
|
1009
|
-
...row,
|
|
1010
|
-
label: isRole(row.role) ? ROLE_LABELS[row.role] : row.role
|
|
1011
|
-
})), ...extras.map((extra) => ({ ...extra }))];
|
|
1012
|
-
}
|
|
1013
|
-
function defaultRolesFieldValue(extras = []) {
|
|
1014
|
-
return seedRoleRows(extras).map((row) => ({
|
|
1015
|
-
id: row.role,
|
|
1016
|
-
...row
|
|
1017
|
-
}));
|
|
1018
|
-
}
|
|
1019
|
-
function readRoleRow(item) {
|
|
1020
|
-
const role = "role" in item ? item.role : void 0;
|
|
1021
|
-
if (typeof role !== "string" || role.trim() === "") return { error: DUPLICATE_ROLE_SLUG_MESSAGE };
|
|
1022
|
-
if (!isRoleSlug(role)) return { error: DUPLICATE_ROLE_SLUG_MESSAGE };
|
|
1023
|
-
return {
|
|
1024
|
-
id: role,
|
|
1025
|
-
role,
|
|
1026
|
-
label: roleLabel(role, "label" in item && typeof item.label === "string" ? item.label : void 0),
|
|
1027
|
-
content: Boolean("content" in item && item.content),
|
|
1028
|
-
brand: Boolean("brand" in item && item.brand),
|
|
1029
|
-
publish: Boolean("publish" in item && item.publish),
|
|
1030
|
-
users: Boolean("users" in item && item.users)
|
|
1487
|
+
function publishThemeChild(stored, incoming, child) {
|
|
1488
|
+
if (child === "colors") return {
|
|
1489
|
+
...stored,
|
|
1490
|
+
colors: incoming.colors
|
|
1031
1491
|
};
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
ok: false,
|
|
1036
|
-
message: MISSING_SEED_ROLES_MESSAGE
|
|
1492
|
+
if (child === "typography") return {
|
|
1493
|
+
...stored,
|
|
1494
|
+
typography: incoming.typography
|
|
1037
1495
|
};
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
if (!item || typeof item !== "object") continue;
|
|
1042
|
-
const parsed = readRoleRow(item);
|
|
1043
|
-
if ("error" in parsed) return {
|
|
1044
|
-
ok: false,
|
|
1045
|
-
message: parsed.error
|
|
1046
|
-
};
|
|
1047
|
-
if (seen.has(parsed.role)) return {
|
|
1048
|
-
ok: false,
|
|
1049
|
-
message: DUPLICATE_ROLE_SLUG_MESSAGE
|
|
1050
|
-
};
|
|
1051
|
-
seen.add(parsed.role);
|
|
1052
|
-
rows.push(parsed);
|
|
1053
|
-
}
|
|
1054
|
-
if (ROLES.some((role) => !seen.has(role))) return {
|
|
1055
|
-
ok: false,
|
|
1056
|
-
message: MISSING_SEED_ROLES_MESSAGE
|
|
1496
|
+
if (child === "appearance") return {
|
|
1497
|
+
...stored,
|
|
1498
|
+
appearance: incoming.appearance
|
|
1057
1499
|
};
|
|
1058
1500
|
return {
|
|
1059
|
-
|
|
1060
|
-
|
|
1501
|
+
...stored,
|
|
1502
|
+
logo: incoming.logo,
|
|
1503
|
+
favicon: incoming.favicon,
|
|
1504
|
+
logoMark: incoming.logoMark
|
|
1061
1505
|
};
|
|
1062
1506
|
}
|
|
1063
|
-
function hasPrivilegedUsersTick(rows) {
|
|
1064
|
-
return rows.some((row) => (row.role === "admin" || row.role === "developer") && row.users);
|
|
1065
|
-
}
|
|
1066
|
-
function validateRolesMatrix(value) {
|
|
1067
|
-
const parsed = parseRolesMatrix(value);
|
|
1068
|
-
if (!parsed.ok) return parsed.message;
|
|
1069
|
-
if (!hasPrivilegedUsersTick(parsed.rows)) return LAST_USERS_TICK_MESSAGE;
|
|
1070
|
-
return true;
|
|
1071
|
-
}
|
|
1072
|
-
/**
|
|
1073
|
-
* Developer is exclusive. Admin does not swallow Designer: both store.
|
|
1074
|
-
* Unknown slugs (dropped catalogue keys such as `approver`) fall away unless
|
|
1075
|
-
* they appear on the matrix.
|
|
1076
|
-
*/
|
|
1077
|
-
function normalizeStoredRoles(value, matrix = DEFAULT_ROLE_MATRIX) {
|
|
1078
|
-
if (!Array.isArray(value)) return [];
|
|
1079
|
-
const allowed = new Set(matrix.map((row) => row.role));
|
|
1080
|
-
const roles = [...new Set(value.filter((entry) => typeof entry === "string" && allowed.has(entry)))];
|
|
1081
|
-
if (roles.includes("developer")) return ["developer"];
|
|
1082
|
-
return roles;
|
|
1083
|
-
}
|
|
1084
|
-
function roleSelectOptions(matrix = DEFAULT_ROLE_MATRIX) {
|
|
1085
|
-
return matrix.map((row) => ({
|
|
1086
|
-
label: roleLabel(row.role, row.label),
|
|
1087
|
-
value: row.role
|
|
1088
|
-
}));
|
|
1089
|
-
}
|
|
1090
|
-
/** Capability copy only. Developer names the API tab; nothing about MCP or seed. */
|
|
1091
|
-
function roleDescription(role, matrix = DEFAULT_ROLE_MATRIX) {
|
|
1092
|
-
if (role === "developer") return DEVELOPER_DESCRIPTION;
|
|
1093
|
-
const row = matrix.find((entry) => entry.role === role);
|
|
1094
|
-
if (!row) return "No capabilities";
|
|
1095
|
-
return CAPABILITIES.filter((capability) => row[capability]).map((capability) => CAPABILITY_LABELS[capability]).join(", ") || "No capabilities";
|
|
1096
|
-
}
|
|
1097
|
-
/**
|
|
1098
|
-
* Seed ticks and package labels come back; extra rows stay as they are.
|
|
1099
|
-
*/
|
|
1100
|
-
function resetRolesMatrix(current) {
|
|
1101
|
-
const extras = [];
|
|
1102
|
-
if (Array.isArray(current)) for (const item of current) {
|
|
1103
|
-
if (!item || typeof item !== "object") continue;
|
|
1104
|
-
const parsed = readRoleRow(item);
|
|
1105
|
-
if ("error" in parsed || isRole(parsed.role)) continue;
|
|
1106
|
-
extras.push(parsed);
|
|
1107
|
-
}
|
|
1108
|
-
return [...defaultRolesFieldValue(), ...extras];
|
|
1109
|
-
}
|
|
1110
|
-
//#endregion
|
|
1111
|
-
//#region src/roles/access.ts
|
|
1112
|
-
function isAccessArgs$1(value) {
|
|
1113
|
-
return typeof value === "object" && value !== null && "req" in value;
|
|
1114
|
-
}
|
|
1115
|
-
function storedRoles(user) {
|
|
1116
|
-
return Array.isArray(user?.roles) ? user.roles : [];
|
|
1117
|
-
}
|
|
1118
|
-
function hasRole(user, role) {
|
|
1119
|
-
return storedRoles(user).includes(role);
|
|
1120
|
-
}
|
|
1121
|
-
/** Not a tick. True only when `developer` is stored on the row. */
|
|
1122
|
-
function isDeveloper(user) {
|
|
1123
|
-
return hasRole(user, "developer");
|
|
1124
|
-
}
|
|
1125
|
-
function hasCapability(user, capability, matrix = DEFAULT_ROLE_MATRIX) {
|
|
1126
|
-
return storedRoles(user).some((role) => {
|
|
1127
|
-
return matrix.find((entry) => entry.role === role)?.[capability] === true;
|
|
1128
|
-
});
|
|
1129
|
-
}
|
|
1130
1507
|
/**
|
|
1131
|
-
*
|
|
1132
|
-
*
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
return parsed.ok ? parsed.rows : fallback;
|
|
1147
|
-
} catch {
|
|
1148
|
-
return fallback;
|
|
1149
|
-
}
|
|
1150
|
-
}
|
|
1151
|
-
function capabilityPredicate(capability) {
|
|
1152
|
-
function predicate(userOrArgs, matrix) {
|
|
1153
|
-
if (isAccessArgs$1(userOrArgs)) {
|
|
1154
|
-
const user = userOrArgs.req.user;
|
|
1155
|
-
if (matrix) return hasCapability(user, capability, matrix);
|
|
1156
|
-
return getRolesMatrix(userOrArgs.req).then((rows) => hasCapability(user, capability, rows));
|
|
1157
|
-
}
|
|
1158
|
-
return hasCapability(userOrArgs, capability, matrix ?? DEFAULT_ROLE_MATRIX);
|
|
1159
|
-
}
|
|
1160
|
-
return predicate;
|
|
1161
|
-
}
|
|
1162
|
-
const canManageContent = capabilityPredicate("content");
|
|
1163
|
-
const canManageBrand = capabilityPredicate("brand");
|
|
1164
|
-
const canPublish = capabilityPredicate("publish");
|
|
1165
|
-
/** Users tick on any held role. */
|
|
1166
|
-
const isAdmin = capabilityPredicate("users");
|
|
1167
|
-
/** This role id currently has the Users tick. */
|
|
1168
|
-
function isPrivilegedRole(role, matrix = DEFAULT_ROLE_MATRIX) {
|
|
1169
|
-
return typeof role === "string" && matrix.some((row) => row.role === role && row.users);
|
|
1170
|
-
}
|
|
1171
|
-
function isAuthenticated(userOrArgs) {
|
|
1172
|
-
if (isAccessArgs$1(userOrArgs)) return Boolean(userOrArgs.req.user);
|
|
1173
|
-
return Boolean(userOrArgs);
|
|
1174
|
-
}
|
|
1175
|
-
const isAdminOrSelf = async ({ req }) => {
|
|
1176
|
-
if (!req.user) return false;
|
|
1177
|
-
if (await isAdmin({ req })) return true;
|
|
1178
|
-
return { id: { equals: req.user.id } };
|
|
1179
|
-
};
|
|
1180
|
-
const authenticatedOrPublished = ({ req: { user } }) => {
|
|
1181
|
-
if (isAuthenticated(user)) return true;
|
|
1182
|
-
return { _status: { equals: "published" } };
|
|
1183
|
-
};
|
|
1184
|
-
/** API tab condition: Developer only, not the Users tick. */
|
|
1185
|
-
function isDeveloperTab({ req }) {
|
|
1186
|
-
return isDeveloper(req.user);
|
|
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;
|
|
1187
1523
|
}
|
|
1188
1524
|
//#endregion
|
|
1189
1525
|
//#region src/roles/fields.ts
|
|
1190
1526
|
const ROLES_MATRIX_FIELD = "@bison-lab/payload-core/admin#RolesMatrixField";
|
|
1191
1527
|
const ROLES_ROW_LABEL = "@bison-lab/payload-core/admin#RolesRowLabel";
|
|
1192
1528
|
const ROLE_SLUG_FIELD = "@bison-lab/payload-core/admin#RoleSlugField";
|
|
1529
|
+
const ROLE_NAME_FIELD = "@bison-lab/payload-core/admin#RoleNameField";
|
|
1193
1530
|
const ROLES_FIELD = "@bison-lab/payload-core/admin#RolesField";
|
|
1531
|
+
const ROLES_GRANTS_FIELD = "@bison-lab/payload-core/admin#RolesGrantsField";
|
|
1194
1532
|
//#endregion
|
|
1195
1533
|
//#region src/roles/global.ts
|
|
1196
|
-
const roleSlugValidate = (value
|
|
1197
|
-
if (
|
|
1198
|
-
if (typeof value !== "string" || value === "") return DUPLICATE_ROLE_SLUG_MESSAGE;
|
|
1534
|
+
const roleSlugValidate = (value) => {
|
|
1535
|
+
if (typeof value !== "string" || value === "") return true;
|
|
1199
1536
|
return isRoleSlug(value) || "Each role needs a unique slug.";
|
|
1200
1537
|
};
|
|
1538
|
+
const roleNameValidate = (value) => {
|
|
1539
|
+
if (typeof value !== "string" || value.trim() === "") return true;
|
|
1540
|
+
return isRoleName(value) || "Use letters and spaces only.";
|
|
1541
|
+
};
|
|
1201
1542
|
/**
|
|
1202
|
-
* Settings → Roles. Rank is the array order (Payload's drag handle).
|
|
1203
|
-
* are
|
|
1543
|
+
* Settings → Roles. Rank is the array order (Payload's drag handle).
|
|
1544
|
+
* Ticks are released catalogue rows. Developer is implicit and shown
|
|
1545
|
+
* only to a Developer, with every catalogue tick locked on.
|
|
1204
1546
|
*/
|
|
1205
1547
|
function createRoles({ extras = [] } = {}) {
|
|
1206
1548
|
const rolesField = {
|
|
@@ -1215,6 +1557,7 @@ function createRoles({ extras = [] } = {}) {
|
|
|
1215
1557
|
required: true,
|
|
1216
1558
|
defaultValue: defaultRolesFieldValue(extras),
|
|
1217
1559
|
validate: validateRolesMatrix,
|
|
1560
|
+
hooks: { beforeChange: [({ value }) => applyRoleSlugsFromNames(value)] },
|
|
1218
1561
|
admin: {
|
|
1219
1562
|
components: {
|
|
1220
1563
|
Field: ROLES_MATRIX_FIELD,
|
|
@@ -1228,36 +1571,23 @@ function createRoles({ extras = [] } = {}) {
|
|
|
1228
1571
|
name: "role",
|
|
1229
1572
|
type: "text",
|
|
1230
1573
|
label: "Slug",
|
|
1231
|
-
required: true,
|
|
1232
1574
|
validate: roleSlugValidate,
|
|
1233
|
-
|
|
1234
|
-
admin: { components: { Field: ROLE_SLUG_FIELD } }
|
|
1575
|
+
admin: { hidden: true }
|
|
1235
1576
|
},
|
|
1236
1577
|
{
|
|
1237
1578
|
name: "label",
|
|
1238
1579
|
type: "text",
|
|
1239
1580
|
label: "Name",
|
|
1240
|
-
required: true
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
name: "content",
|
|
1244
|
-
type: "checkbox",
|
|
1245
|
-
label: "Content"
|
|
1246
|
-
},
|
|
1247
|
-
{
|
|
1248
|
-
name: "brand",
|
|
1249
|
-
type: "checkbox",
|
|
1250
|
-
label: "Brand"
|
|
1251
|
-
},
|
|
1252
|
-
{
|
|
1253
|
-
name: "publish",
|
|
1254
|
-
type: "checkbox",
|
|
1255
|
-
label: "Publish"
|
|
1581
|
+
required: true,
|
|
1582
|
+
validate: roleNameValidate,
|
|
1583
|
+
admin: { components: { Field: ROLE_NAME_FIELD } }
|
|
1256
1584
|
},
|
|
1257
1585
|
{
|
|
1258
|
-
name: "
|
|
1259
|
-
type: "
|
|
1260
|
-
label: "
|
|
1586
|
+
name: "grants",
|
|
1587
|
+
type: "json",
|
|
1588
|
+
label: "Grants",
|
|
1589
|
+
defaultValue: [],
|
|
1590
|
+
admin: { components: { Field: ROLES_GRANTS_FIELD } }
|
|
1261
1591
|
}
|
|
1262
1592
|
]
|
|
1263
1593
|
};
|
|
@@ -1266,11 +1596,11 @@ function createRoles({ extras = [] } = {}) {
|
|
|
1266
1596
|
label: "Roles",
|
|
1267
1597
|
admin: {
|
|
1268
1598
|
group: "Settings",
|
|
1269
|
-
hidden: (
|
|
1599
|
+
hidden: hideUnlessFeature("roles"),
|
|
1270
1600
|
description: ROLES_GLOBAL_DESCRIPTION
|
|
1271
1601
|
},
|
|
1272
1602
|
access: {
|
|
1273
|
-
read: (args) =>
|
|
1603
|
+
read: (args) => canUseFeature("roles")(args),
|
|
1274
1604
|
update: (args) => isAdmin(args)
|
|
1275
1605
|
},
|
|
1276
1606
|
fields: [rolesField]
|
|
@@ -1293,237 +1623,75 @@ async function seedRoles(payload, extras = []) {
|
|
|
1293
1623
|
//#region src/features/fields.ts
|
|
1294
1624
|
const FEATURES_MATRIX_FIELD = "@bison-lab/payload-core/admin#FeaturesMatrixField";
|
|
1295
1625
|
//#endregion
|
|
1296
|
-
//#region src/features/types.ts
|
|
1297
|
-
const FEATURES_SLUG = "features";
|
|
1298
|
-
const PACKAGE_FEATURE_SLUGS = [
|
|
1299
|
-
"pages",
|
|
1300
|
-
"media",
|
|
1301
|
-
"theme",
|
|
1302
|
-
"brand-assets",
|
|
1303
|
-
"users",
|
|
1304
|
-
"roles",
|
|
1305
|
-
"features"
|
|
1306
|
-
];
|
|
1307
|
-
/**
|
|
1308
|
-
* Package catalogue. Locked rows cannot be turned off. Empty Global falls
|
|
1309
|
-
* back to the named capability (Content → Pages/Media, Brand → Theme /
|
|
1310
|
-
* Brand assets, Users → Users/Roles/Features).
|
|
1311
|
-
*/
|
|
1312
|
-
const PACKAGE_FEATURES = [
|
|
1313
|
-
{
|
|
1314
|
-
slug: "pages",
|
|
1315
|
-
label: "Pages",
|
|
1316
|
-
fallback: "content",
|
|
1317
|
-
locked: false
|
|
1318
|
-
},
|
|
1319
|
-
{
|
|
1320
|
-
slug: "media",
|
|
1321
|
-
label: "Media",
|
|
1322
|
-
fallback: "content",
|
|
1323
|
-
locked: false
|
|
1324
|
-
},
|
|
1325
|
-
{
|
|
1326
|
-
slug: "theme",
|
|
1327
|
-
label: "Theme",
|
|
1328
|
-
fallback: "brand",
|
|
1329
|
-
locked: false
|
|
1330
|
-
},
|
|
1331
|
-
{
|
|
1332
|
-
slug: "brand-assets",
|
|
1333
|
-
label: "Brand assets",
|
|
1334
|
-
fallback: "brand",
|
|
1335
|
-
locked: false
|
|
1336
|
-
},
|
|
1337
|
-
{
|
|
1338
|
-
slug: "users",
|
|
1339
|
-
label: "Users",
|
|
1340
|
-
fallback: "users",
|
|
1341
|
-
locked: true
|
|
1342
|
-
},
|
|
1343
|
-
{
|
|
1344
|
-
slug: "roles",
|
|
1345
|
-
label: "Roles",
|
|
1346
|
-
fallback: "users",
|
|
1347
|
-
locked: true
|
|
1348
|
-
},
|
|
1349
|
-
{
|
|
1350
|
-
slug: "features",
|
|
1351
|
-
label: "Features",
|
|
1352
|
-
fallback: "users",
|
|
1353
|
-
locked: true
|
|
1354
|
-
}
|
|
1355
|
-
];
|
|
1356
|
-
function isPackageFeatureSlug(value) {
|
|
1357
|
-
return typeof value === "string" && PACKAGE_FEATURE_SLUGS.includes(value);
|
|
1358
|
-
}
|
|
1359
|
-
function isFeatureSlug(value) {
|
|
1360
|
-
return typeof value === "string" && /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/.test(value);
|
|
1361
|
-
}
|
|
1362
|
-
function isLockedFeature(slug, locked) {
|
|
1363
|
-
if (locked === true) return true;
|
|
1364
|
-
return PACKAGE_FEATURES.find((feature) => feature.slug === slug)?.locked === true;
|
|
1365
|
-
}
|
|
1366
|
-
//#endregion
|
|
1367
|
-
//#region src/features/matrix.ts
|
|
1368
|
-
const MISSING_PACKAGE_FEATURES_MESSAGE = "Features must include Pages, Media, Theme, Brand assets, Users, Roles, and Features.";
|
|
1369
|
-
const LOCKED_FEATURE_MESSAGE = "Users, Roles, and Features cannot be turned off. Developer stays allowed on every row.";
|
|
1370
|
-
function featureCatalogue(extras = []) {
|
|
1371
|
-
return [...PACKAGE_FEATURES.map((feature) => ({ ...feature })), ...extras.map((extra) => ({
|
|
1372
|
-
slug: extra.slug,
|
|
1373
|
-
label: extra.label,
|
|
1374
|
-
fallback: null,
|
|
1375
|
-
locked: false
|
|
1376
|
-
}))];
|
|
1377
|
-
}
|
|
1378
|
-
function rolesForFeature(feature, matrix) {
|
|
1379
|
-
const allowed = new Set(["developer"]);
|
|
1380
|
-
if (feature.locked) {
|
|
1381
|
-
for (const row of matrix) allowed.add(row.role);
|
|
1382
|
-
return [...allowed];
|
|
1383
|
-
}
|
|
1384
|
-
if (feature.fallback) {
|
|
1385
|
-
for (const row of matrix) if (row[feature.fallback]) allowed.add(row.role);
|
|
1386
|
-
}
|
|
1387
|
-
return [...allowed];
|
|
1388
|
-
}
|
|
1389
|
-
function defaultFeaturesFieldValue(extras = [], matrix = DEFAULT_ROLE_MATRIX) {
|
|
1390
|
-
return featureCatalogue(extras).map((feature) => ({
|
|
1391
|
-
id: feature.slug,
|
|
1392
|
-
slug: feature.slug,
|
|
1393
|
-
label: feature.label,
|
|
1394
|
-
locked: feature.locked,
|
|
1395
|
-
roles: rolesForFeature(feature, matrix)
|
|
1396
|
-
}));
|
|
1397
|
-
}
|
|
1398
|
-
function parseFeaturesMatrix(value) {
|
|
1399
|
-
if (!Array.isArray(value) || value.length === 0) return {
|
|
1400
|
-
ok: false,
|
|
1401
|
-
message: MISSING_PACKAGE_FEATURES_MESSAGE
|
|
1402
|
-
};
|
|
1403
|
-
const rows = [];
|
|
1404
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1405
|
-
for (const item of value) {
|
|
1406
|
-
if (!item || typeof item !== "object") continue;
|
|
1407
|
-
const slug = "slug" in item ? item.slug : void 0;
|
|
1408
|
-
if (!isFeatureSlug(slug) || seen.has(slug)) continue;
|
|
1409
|
-
seen.add(slug);
|
|
1410
|
-
const rawRoles = "roles" in item && Array.isArray(item.roles) ? item.roles : [];
|
|
1411
|
-
const roles = [];
|
|
1412
|
-
for (const role of rawRoles) if (typeof role === "string" && !roles.includes(role)) roles.push(role);
|
|
1413
|
-
const label = "label" in item && typeof item.label === "string" ? item.label : slug;
|
|
1414
|
-
rows.push({
|
|
1415
|
-
id: slug,
|
|
1416
|
-
slug,
|
|
1417
|
-
label,
|
|
1418
|
-
locked: isLockedFeature(slug, "locked" in item ? Boolean(item.locked) : void 0),
|
|
1419
|
-
roles
|
|
1420
|
-
});
|
|
1421
|
-
}
|
|
1422
|
-
if (PACKAGE_FEATURES.some((feature) => !seen.has(feature.slug))) return {
|
|
1423
|
-
ok: false,
|
|
1424
|
-
message: MISSING_PACKAGE_FEATURES_MESSAGE
|
|
1425
|
-
};
|
|
1426
|
-
return {
|
|
1427
|
-
ok: true,
|
|
1428
|
-
rows
|
|
1429
|
-
};
|
|
1430
|
-
}
|
|
1431
|
-
function enforceFeatureLocks(rows, matrix = DEFAULT_ROLE_MATRIX) {
|
|
1432
|
-
const roleSlugs = matrix.map((row) => row.role);
|
|
1433
|
-
return rows.map((row) => {
|
|
1434
|
-
const roles = new Set(row.roles ?? []);
|
|
1435
|
-
roles.add("developer");
|
|
1436
|
-
if (isLockedFeature(row.slug, row.locked)) for (const role of roleSlugs) roles.add(role);
|
|
1437
|
-
return {
|
|
1438
|
-
...row,
|
|
1439
|
-
roles: [...roles]
|
|
1440
|
-
};
|
|
1441
|
-
});
|
|
1442
|
-
}
|
|
1443
|
-
function validateFeaturesMatrix(value, extras = [], matrix = DEFAULT_ROLE_MATRIX) {
|
|
1444
|
-
const parsed = parseFeaturesMatrix(value);
|
|
1445
|
-
if (!parsed.ok) return parsed.message;
|
|
1446
|
-
const allowed = new Set(featureCatalogue(extras).map((feature) => feature.slug));
|
|
1447
|
-
if (parsed.rows.some((row) => !allowed.has(row.slug))) return MISSING_PACKAGE_FEATURES_MESSAGE;
|
|
1448
|
-
for (const row of parsed.rows) {
|
|
1449
|
-
if (!row.roles?.includes("developer")) return LOCKED_FEATURE_MESSAGE;
|
|
1450
|
-
if (isLockedFeature(row.slug, row.locked) && matrix.some((role) => !row.roles?.includes(role.role))) return LOCKED_FEATURE_MESSAGE;
|
|
1451
|
-
}
|
|
1452
|
-
return true;
|
|
1453
|
-
}
|
|
1454
|
-
//#endregion
|
|
1455
1626
|
//#region src/features/global.ts
|
|
1456
1627
|
/**
|
|
1457
|
-
* Settings → Features.
|
|
1458
|
-
*
|
|
1628
|
+
* Settings → Features. One release switch per catalogue row. Off hides
|
|
1629
|
+
* the tick on Roles; Developer still has the feature. This screen is
|
|
1630
|
+
* not itself a row.
|
|
1459
1631
|
*/
|
|
1460
1632
|
function createFeatures({ extras = [] } = {}) {
|
|
1461
1633
|
const catalogue = featureCatalogue(extras);
|
|
1462
|
-
|
|
1463
|
-
|
|
1634
|
+
const featuresField = {
|
|
1635
|
+
name: "features",
|
|
1636
|
+
type: "array",
|
|
1464
1637
|
label: "Features",
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
description: "Who may use which collections and globals."
|
|
1638
|
+
labels: {
|
|
1639
|
+
singular: "Feature",
|
|
1640
|
+
plural: "Features"
|
|
1469
1641
|
},
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
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
|
|
1473
1655
|
},
|
|
1474
|
-
fields: [
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1656
|
+
fields: [
|
|
1657
|
+
{
|
|
1658
|
+
name: "slug",
|
|
1659
|
+
type: "text",
|
|
1660
|
+
label: "Slug",
|
|
1661
|
+
required: true,
|
|
1662
|
+
admin: { readOnly: true }
|
|
1481
1663
|
},
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1664
|
+
{
|
|
1665
|
+
name: "label",
|
|
1666
|
+
type: "text",
|
|
1667
|
+
label: "Name",
|
|
1668
|
+
required: true,
|
|
1669
|
+
admin: { readOnly: true }
|
|
1488
1670
|
},
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
components: { Field: FEATURES_MATRIX_FIELD },
|
|
1495
|
-
description: "Turn a feature on or off per role. Users, Roles, and Features stay on. Developer is always allowed.",
|
|
1496
|
-
initCollapsed: false
|
|
1671
|
+
{
|
|
1672
|
+
name: "group",
|
|
1673
|
+
type: "text",
|
|
1674
|
+
label: "Group",
|
|
1675
|
+
admin: { hidden: true }
|
|
1497
1676
|
},
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
label: "Locked",
|
|
1517
|
-
admin: { hidden: true }
|
|
1518
|
-
},
|
|
1519
|
-
{
|
|
1520
|
-
name: "roles",
|
|
1521
|
-
type: "json",
|
|
1522
|
-
label: "Roles",
|
|
1523
|
-
required: true
|
|
1524
|
-
}
|
|
1525
|
-
]
|
|
1526
|
-
}]
|
|
1677
|
+
{
|
|
1678
|
+
name: "released",
|
|
1679
|
+
type: "checkbox",
|
|
1680
|
+
label: "Released",
|
|
1681
|
+
admin: { hidden: true }
|
|
1682
|
+
}
|
|
1683
|
+
]
|
|
1684
|
+
};
|
|
1685
|
+
return {
|
|
1686
|
+
slug: FEATURES_SLUG,
|
|
1687
|
+
label: "Features",
|
|
1688
|
+
admin: {
|
|
1689
|
+
group: "Settings",
|
|
1690
|
+
hidden: hideUnlessDeveloper,
|
|
1691
|
+
description: "Which features other people may be granted on Roles."
|
|
1692
|
+
},
|
|
1693
|
+
access: developerOnlyAccess(),
|
|
1694
|
+
fields: [featuresField]
|
|
1527
1695
|
};
|
|
1528
1696
|
}
|
|
1529
1697
|
//#endregion
|
|
@@ -1539,80 +1707,6 @@ async function seedFeatures(payload, extras = []) {
|
|
|
1539
1707
|
});
|
|
1540
1708
|
}
|
|
1541
1709
|
//#endregion
|
|
1542
|
-
//#region src/features/access.ts
|
|
1543
|
-
function isAccessArgs(value) {
|
|
1544
|
-
return typeof value === "object" && value !== null && "req" in value;
|
|
1545
|
-
}
|
|
1546
|
-
function fallbackFeature(user, slug, matrix) {
|
|
1547
|
-
const pack = PACKAGE_FEATURES.find((feature) => feature.slug === slug);
|
|
1548
|
-
if (!pack) return false;
|
|
1549
|
-
return hasCapability(user, pack.fallback, matrix);
|
|
1550
|
-
}
|
|
1551
|
-
/**
|
|
1552
|
-
* True when any stored role is on the feature row. An empty grid falls
|
|
1553
|
-
* back to the capability map. Developer is always allowed.
|
|
1554
|
-
*/
|
|
1555
|
-
function hasFeature(user, slug, features = null, matrix = DEFAULT_ROLE_MATRIX) {
|
|
1556
|
-
if (isDeveloper(user)) return true;
|
|
1557
|
-
if (!user) return false;
|
|
1558
|
-
if (!features || features.length === 0) return fallbackFeature(user, slug, matrix);
|
|
1559
|
-
const row = features.find((entry) => entry.slug === slug);
|
|
1560
|
-
if (!row) return false;
|
|
1561
|
-
const allowed = row.roles ?? [];
|
|
1562
|
-
return storedRoles(user).some((role) => allowed.includes(role));
|
|
1563
|
-
}
|
|
1564
|
-
/**
|
|
1565
|
-
* Reads the saved Features Global. `null` means empty or unreadable — callers
|
|
1566
|
-
* fall back to the capability map. Always override-access.
|
|
1567
|
-
*/
|
|
1568
|
-
async function getFeaturesMatrix(req = {}) {
|
|
1569
|
-
const findGlobal = req.payload?.findGlobal;
|
|
1570
|
-
if (typeof findGlobal !== "function") return null;
|
|
1571
|
-
try {
|
|
1572
|
-
const doc = await findGlobal({
|
|
1573
|
-
slug: FEATURES_SLUG,
|
|
1574
|
-
overrideAccess: true,
|
|
1575
|
-
req
|
|
1576
|
-
});
|
|
1577
|
-
const parsed = parseFeaturesMatrix(doc && typeof doc === "object" && "features" in doc ? doc.features : void 0);
|
|
1578
|
-
if (!parsed.ok) return null;
|
|
1579
|
-
const matrix = await getRolesMatrix(req);
|
|
1580
|
-
return enforceFeatureLocks(parsed.rows, matrix);
|
|
1581
|
-
} catch {
|
|
1582
|
-
return null;
|
|
1583
|
-
}
|
|
1584
|
-
}
|
|
1585
|
-
/**
|
|
1586
|
-
* Access / nav helper for one catalogue slug. Sync against a passed grid;
|
|
1587
|
-
* async when given `req` so it can read both Globals.
|
|
1588
|
-
*/
|
|
1589
|
-
function canUseFeature(slug) {
|
|
1590
|
-
function predicate(userOrArgs, features, roles) {
|
|
1591
|
-
if (isAccessArgs(userOrArgs)) {
|
|
1592
|
-
const user = userOrArgs.req.user;
|
|
1593
|
-
if (features !== void 0) return hasFeature(user, slug, features, roles ?? DEFAULT_ROLE_MATRIX);
|
|
1594
|
-
return Promise.all([getFeaturesMatrix(userOrArgs.req), getRolesMatrix(userOrArgs.req)]).then(([grid, matrix]) => hasFeature(user, slug, grid, matrix));
|
|
1595
|
-
}
|
|
1596
|
-
return hasFeature(userOrArgs, slug, features ?? null, roles ?? DEFAULT_ROLE_MATRIX);
|
|
1597
|
-
}
|
|
1598
|
-
return predicate;
|
|
1599
|
-
}
|
|
1600
|
-
/** `admin.hidden`: hide when the login cannot use the feature. */
|
|
1601
|
-
function hideUnlessFeature(slug) {
|
|
1602
|
-
return (args) => {
|
|
1603
|
-
const user = args.user;
|
|
1604
|
-
if (args.req) {
|
|
1605
|
-
const result = canUseFeature(slug)({ req: {
|
|
1606
|
-
...args.req,
|
|
1607
|
-
user
|
|
1608
|
-
} });
|
|
1609
|
-
if (result instanceof Promise) return result.then((ok) => !ok);
|
|
1610
|
-
return !result;
|
|
1611
|
-
}
|
|
1612
|
-
return !canUseFeature(slug)(user ?? null);
|
|
1613
|
-
};
|
|
1614
|
-
}
|
|
1615
|
-
//#endregion
|
|
1616
1710
|
//#region src/fields/slug.ts
|
|
1617
1711
|
/**
|
|
1618
1712
|
* A stored slug from whatever was typed: lowercased, with any run of
|
|
@@ -1694,6 +1788,15 @@ function slugField({ collection, isReserved }) {
|
|
|
1694
1788
|
//#endregion
|
|
1695
1789
|
//#region src/collections/pages.ts
|
|
1696
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
|
+
/**
|
|
1697
1800
|
* CMS-managed pages: one required hero, then a reorderable body of sections.
|
|
1698
1801
|
* The CMS owns copy and the order of sections; what a section looks like is
|
|
1699
1802
|
* code-owned, which is why the blocks are an argument.
|
|
@@ -1712,15 +1815,15 @@ function createPages({ heroBlocks, isReservedSlug, layoutBlocks, previewPath, pr
|
|
|
1712
1815
|
"_status",
|
|
1713
1816
|
"updatedAt"
|
|
1714
1817
|
],
|
|
1715
|
-
hidden: hideUnlessFeature("
|
|
1818
|
+
hidden: hideUnlessFeature("content"),
|
|
1716
1819
|
preview: (doc) => previewPath(typeof doc.slug === "string" ? doc.slug : ""),
|
|
1717
1820
|
...previewButton ? { components: { edit: { PreviewButton: previewButton } } } : {}
|
|
1718
1821
|
},
|
|
1719
1822
|
access: {
|
|
1720
1823
|
read: authenticatedOrPublished,
|
|
1721
1824
|
readVersions: isAuthenticated,
|
|
1722
|
-
create: canUseFeature("
|
|
1723
|
-
update:
|
|
1825
|
+
create: canUseFeature("content"),
|
|
1826
|
+
update: pagesUpdate,
|
|
1724
1827
|
delete: isAdmin
|
|
1725
1828
|
},
|
|
1726
1829
|
versions: {
|
|
@@ -1791,7 +1894,7 @@ async function lockLastAdminDecision(req) {
|
|
|
1791
1894
|
});
|
|
1792
1895
|
}
|
|
1793
1896
|
function privilegedRoles(matrix) {
|
|
1794
|
-
return matrix.filter((row) => row.
|
|
1897
|
+
return matrix.filter((row) => isPrivilegedRole(row.role, matrix)).map((row) => row.role);
|
|
1795
1898
|
}
|
|
1796
1899
|
function holdsPrivilegedRole(roles, matrix) {
|
|
1797
1900
|
return Array.isArray(roles) && roles.some((role) => isPrivilegedRole(role, matrix));
|
|
@@ -1847,13 +1950,16 @@ function createUsers({ secureCookies, rolesField, extras = [] }) {
|
|
|
1847
1950
|
"lastName",
|
|
1848
1951
|
"roles"
|
|
1849
1952
|
],
|
|
1850
|
-
hidden: (
|
|
1953
|
+
hidden: hideUnlessFeature("users")
|
|
1851
1954
|
},
|
|
1852
1955
|
access: {
|
|
1853
1956
|
create: isAdmin,
|
|
1854
1957
|
delete: isAdmin,
|
|
1855
1958
|
unlock: isAdmin,
|
|
1856
|
-
read:
|
|
1959
|
+
read: async (args) => {
|
|
1960
|
+
if (await Promise.resolve(canUseFeature("users")(args))) return true;
|
|
1961
|
+
return isAdminOrSelf(args);
|
|
1962
|
+
},
|
|
1857
1963
|
update: isAdminOrSelf
|
|
1858
1964
|
},
|
|
1859
1965
|
hooks: {
|
|
@@ -1876,27 +1982,45 @@ function createUsers({ secureCookies, rolesField, extras = [] }) {
|
|
|
1876
1982
|
return arg.result;
|
|
1877
1983
|
}]
|
|
1878
1984
|
},
|
|
1879
|
-
fields: [
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
name: "roles",
|
|
1884
|
-
type: "select",
|
|
1885
|
-
hasMany: true,
|
|
1886
|
-
required: true,
|
|
1887
|
-
defaultValue: ["author"],
|
|
1888
|
-
options: roleSelectOptions(fallback),
|
|
1889
|
-
admin: {
|
|
1890
|
-
components: { Field: rolesField ?? "@bison-lab/payload-core/admin#RolesField" },
|
|
1891
|
-
description: "One person can hold several, e.g. Author + Designer."
|
|
1985
|
+
fields: [
|
|
1986
|
+
{
|
|
1987
|
+
type: "row",
|
|
1988
|
+
fields: [nameField("firstName"), nameField("lastName")]
|
|
1892
1989
|
},
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
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
|
+
]
|
|
1900
2024
|
};
|
|
1901
2025
|
}
|
|
1902
2026
|
//#endregion
|
|
@@ -1972,6 +2096,6 @@ const adminOnlyApiTab = definePlugin({
|
|
|
1972
2096
|
})
|
|
1973
2097
|
});
|
|
1974
2098
|
//#endregion
|
|
1975
|
-
export { BRAND_ASSETS_MIME_TYPES, BRAND_ASSETS_SLUG, CAPABILITIES, DEFAULT_ROLE_MATRIX, DESCRIPTION_LENGTH, DEVELOPER_DESCRIPTION, DUPLICATE_ROLE_SLUG_MESSAGE, FEATURES_MATRIX_FIELD, FEATURES_SLUG, LAST_ADMIN_DELETE_MESSAGE, LAST_ADMIN_DEMOTE_MESSAGE, LAST_USERS_TICK_MESSAGE,
|
|
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 };
|
|
1976
2100
|
|
|
1977
2101
|
//# sourceMappingURL=index.mjs.map
|