@bison-lab/payload-core 3.12.0 → 3.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,5 +1,6 @@
1
- import { n as documentTitle, r as titleTemplate, t as SHARE_IMAGE_SIZE } from "./share-image-C4ILz4p2.mjs";
2
- import { A as THEME_IDENTITY_FALLBACK, C as THEME_APPEARANCE_FIELD, D as THEME_DOCUMENT_CONTROLS, E as THEME_CONTRAST_REPORT, F as THEME_SECTION_HEADING, I as headingSelectValue, M as THEME_PAIRING_FIELD, N as THEME_PUBLISH_FIELD, O as THEME_FONT_FIELD, P as THEME_SAVE_BUTTON, S as LOOK_FIELD, T as THEME_COLOR_SCALE_FIELD, _ as docFromConfig, a as pageEditorLooks, b as validateSourceIncluded, c as deleteLibraryColor, d as SYSTEM_COLOR_KEYS, f as THEME_APPEARANCE_SLUG, g as THEME_TYPOGRAPHY_SLUG, h as THEME_SLUG, i as PAGE_EDITOR_SYSTEM_KEYS, j as THEME_LIBRARY_FIELD, k as THEME_GREY_SCALE_FIELD, l as rewriteColorToken, m as THEME_IDENTITY_SLUG, n as colorTokenField, o as pageEditorTokens, p as THEME_COLORS_SLUG, r as lookField, s as themeLibraryFromDoc, t as resolveThemeIdentity, u as themeColorKeys, w as THEME_COLOR_FIELD, x as validateThemeHex, y as themeConfigFromDoc } from "./identity-DZOb3_Gk.mjs";
1
+ import { $ as defaultFeaturesFieldValue, A as ROLES_SLUG, B as CAPABILITIES, C as DUPLICATE_ROLE_NAME_MESSAGE, D as MISSING_SEED_ROLES_MESSAGE, E as LAST_USERS_TICK_MESSAGE, F as resetRolesMatrix, G as THEME_FEATURE_SLUGS, H as DEFAULT_ROLE_GRANTS, I as roleDescription, J as isRoleSlug, K as isRole, L as roleSelectOptions, M as defaultRolesFieldValue, N as normalizeStoredRoles, O as ROLES_FIELD_DESCRIPTION, P as parseRolesMatrix, Q as MISSING_PACKAGE_FEATURES_MESSAGE, R as seedRoleRows, S as DEVELOPER_DESCRIPTION, T as INVALID_ROLE_NAME_MESSAGE, U as ROLES, V as CAPABILITY_FEATURES, W as ROLE_LABELS, X as sanitizeRoleNameInput, Y as roleLabel, Z as slugifyRoleName, _ as isDeveloper, a as hideUnlessFeature, at as FEATURES_SLUG, b as storedRoles, c as canManageContent, ct as PACKAGE_FEATURES, d as hasCapability, dt as isFeatureSlug, et as featureCatalogue, f as hasGrant, ft as isLockedFeature, g as isAuthenticated, h as isAdminOrSelf, i as hasFeature, it as validateFeaturesMatrix, j as applyRoleSlugsFromNames, k as ROLES_GLOBAL_DESCRIPTION, l as canPublish, lt as PACKAGE_FEATURE_SLUGS, m as isAdmin, n as canUseFeature, nt as parseFeaturesMatrix, o as authenticatedOrPublished, ot as FEATURE_GROUPS, p as hasRole, pt as isPackageFeatureSlug, q as isRoleName, r as getFeaturesMatrix, rt as stampFeatureCatalogue, s as canManageBrand, st as FEATURE_GROUP_LABELS, tt as featureGroupLabel, u as getRolesMatrix, ut as isFeatureGroupId, v as isDeveloperTab, w as DUPLICATE_ROLE_SLUG_MESSAGE, x as DEFAULT_ROLE_MATRIX, y as isPrivilegedRole, z as validateRolesMatrix } from "./access-B3LglN2J.mjs";
2
+ import { n as documentTitle, r as titleTemplate, t as SHARE_IMAGE_SIZE } from "./share-image-BlFAJLnd.mjs";
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";
3
4
  import { seoPlugin as seoPlugin$1 } from "@payloadcms/plugin-seo";
4
5
  import { catalog } from "@bison-lab/fonts";
5
6
  import { DESTRUCTIVE_SCALE_HEX, SHADE_STEPS, presetHints } from "@bison-lab/tokens";
@@ -164,6 +165,145 @@ const APPEARANCE_CHOICES = {
164
165
  }
165
166
  };
166
167
  //#endregion
168
+ //#region src/theme/color-usages.ts
169
+ /** Theme-store REST path. LibraryField calls `/api/globals/theme` + this. */
170
+ const COLOR_USAGES_PATH = "/color-usages";
171
+ /**
172
+ * Walk every current document in the named collections and globals
173
+ * (drafts included). A color only on an unpublished page is still in use.
174
+ */
175
+ async function findColorUsages(payload, scopes, key) {
176
+ const usages = [];
177
+ for (const collection of scopes?.collections ?? []) for await (const doc of eachCollectionDoc(payload, collection)) for (const hit of findColorTokens(doc, key)) usages.push({
178
+ collection,
179
+ id: idOf(doc),
180
+ path: hit.path,
181
+ token: hit.token
182
+ });
183
+ for (const slug of scopes?.globals ?? []) {
184
+ const doc = await payload.findGlobal({
185
+ slug,
186
+ draft: true,
187
+ depth: 0,
188
+ overrideAccess: true
189
+ });
190
+ if (!doc) continue;
191
+ for (const hit of findColorTokens(doc, key)) usages.push({
192
+ global: slug,
193
+ path: hit.path,
194
+ token: hit.token
195
+ });
196
+ }
197
+ return usages;
198
+ }
199
+ async function rewriteColorUsages(payload, scopes, fromKey, toKey) {
200
+ for (const collection of scopes?.collections ?? []) for await (const doc of eachCollectionDoc(payload, collection)) {
201
+ if (findColorTokens(doc, fromKey).length === 0) continue;
202
+ const { id: _id, ...data } = rewriteColorTokens(doc, fromKey, toKey);
203
+ const id = idOf(doc);
204
+ if (id == null) continue;
205
+ await payload.update({
206
+ collection,
207
+ id,
208
+ data,
209
+ draft: true,
210
+ overrideAccess: true
211
+ });
212
+ }
213
+ for (const slug of scopes?.globals ?? []) {
214
+ const doc = await payload.findGlobal({
215
+ slug,
216
+ draft: true,
217
+ depth: 0,
218
+ overrideAccess: true
219
+ });
220
+ if (!doc || findColorTokens(doc, fromKey).length === 0) continue;
221
+ await payload.updateGlobal({
222
+ slug,
223
+ data: rewriteColorTokens(doc, fromKey, toKey),
224
+ draft: true,
225
+ overrideAccess: true
226
+ });
227
+ }
228
+ }
229
+ async function replacementKeys(payload, except) {
230
+ return pageEditorLooks(await payload.findGlobal({
231
+ slug: THEME_SLUG,
232
+ depth: 0,
233
+ overrideAccess: true
234
+ })).map((look) => look.value).filter((key) => key !== except);
235
+ }
236
+ async function* eachCollectionDoc(payload, collection) {
237
+ let page = 1;
238
+ for (;;) {
239
+ const result = await payload.find({
240
+ collection,
241
+ draft: true,
242
+ depth: 0,
243
+ limit: 100,
244
+ page,
245
+ overrideAccess: true
246
+ });
247
+ for (const doc of result.docs) yield doc;
248
+ const totalPages = result.totalPages ?? (result.docs.length < 100 ? page : page + 1);
249
+ if (page >= totalPages || result.docs.length === 0) break;
250
+ page += 1;
251
+ }
252
+ }
253
+ function idOf(doc) {
254
+ const id = doc.id;
255
+ return typeof id === "string" || typeof id === "number" ? id : void 0;
256
+ }
257
+ //#endregion
258
+ //#region src/theme/color-usage-endpoints.ts
259
+ function colorUsageEndpoints(access, scopes) {
260
+ return [{
261
+ path: COLOR_USAGES_PATH,
262
+ method: "get",
263
+ handler: async (req) => {
264
+ const denied = await denyUnlessUpdate(req, access.update);
265
+ if (denied) return denied;
266
+ const key = queryKey(req);
267
+ if (!key) return Response.json({ error: "key is required" }, { status: 400 });
268
+ const usages = await findColorUsages(req.payload, scopes, key);
269
+ return Response.json({ usages });
270
+ }
271
+ }, {
272
+ path: COLOR_USAGES_PATH,
273
+ method: "post",
274
+ handler: async (req) => {
275
+ const denied = await denyUnlessUpdate(req, access.update);
276
+ if (denied) return denied;
277
+ const body = await readJson(req);
278
+ const key = typeof body?.key === "string" ? body.key : "";
279
+ const replacement = typeof body?.replacement === "string" ? body.replacement : "";
280
+ if (!key || !replacement) return Response.json({ error: "key and replacement are required" }, { status: 400 });
281
+ if (!(await replacementKeys(req.payload, key)).includes(replacement)) return Response.json({ error: "Replacement must be a different live system or remaining custom key" }, { status: 400 });
282
+ await rewriteColorUsages(req.payload, scopes, key, replacement);
283
+ return Response.json({ ok: true });
284
+ }
285
+ }];
286
+ }
287
+ async function denyUnlessUpdate(req, update) {
288
+ if (await update({ req })) return null;
289
+ return Response.json({ error: "Forbidden" }, { status: 403 });
290
+ }
291
+ function queryKey(req) {
292
+ const fromQuery = req.query?.key;
293
+ if (typeof fromQuery === "string") return fromQuery;
294
+ if (Array.isArray(fromQuery) && typeof fromQuery[0] === "string") return fromQuery[0];
295
+ if (req.url) try {
296
+ return new URL(req.url, "http://local").searchParams.get("key") ?? "";
297
+ } catch {
298
+ return "";
299
+ }
300
+ return "";
301
+ }
302
+ async function readJson(req) {
303
+ if (typeof req.json === "function") return req.json();
304
+ return null;
305
+ }
306
+ //#endregion
167
307
  //#region src/theme/publish.ts
168
308
  /**
169
309
  * Publishing a child writes that slice only. Other responsibilities stay
@@ -570,7 +710,7 @@ function pageHooks(child) {
570
710
  function createTheme(options) {
571
711
  const access = requireAccess$1(options);
572
712
  const destructive = requireDestructive(options);
573
- const { seed, fonts = catalog, fontsBaseUrl = "/fonts", contrastTarget = 7, logo, identity, onPublish } = options;
713
+ const { seed, fonts = catalog, fontsBaseUrl = "/fonts", contrastTarget = 7, logo, identity, onPublish, colorUsages } = options;
574
714
  const adminCustom = {
575
715
  contrastTarget,
576
716
  fontsBaseUrl,
@@ -599,6 +739,7 @@ function createTheme(options) {
599
739
  ...lookFields,
600
740
  ...markFields
601
741
  ],
742
+ endpoints: colorUsageEndpoints(access, colorUsages),
602
743
  hooks: { afterChange: [async ({ doc }) => {
603
744
  if (!onPublish) return;
604
745
  const themeDoc = doc;
@@ -612,6 +753,7 @@ function createTheme(options) {
612
753
  lockDocuments: false,
613
754
  admin: {
614
755
  group: "Theme",
756
+ hidden: hideUnlessFeature("theme"),
615
757
  hideAPIURL: true,
616
758
  custom: adminCustom,
617
759
  components: preview ? { elements: { beforeDocumentControls: [THEME_DOCUMENT_CONTROLS] } } : void 0
@@ -779,309 +921,190 @@ const THEME_PREVIEW_BREAKPOINTS = [
779
921
  }
780
922
  ];
781
923
  //#endregion
782
- //#region src/roles/types.ts
783
- /**
784
- * The Roles Global a site's generated types will describe. Optional and
785
- * nullable, no index signature: a generated Global is assignable to this,
786
- * never the reverse.
787
- */
788
- const ROLES = [
789
- "developer",
790
- "admin",
791
- "designer",
792
- "author"
793
- ];
794
- const ROLE_LABELS = {
795
- developer: "Developer",
796
- admin: "Admin",
797
- designer: "Designer",
798
- author: "Author"
799
- };
800
- const CAPABILITIES = [
801
- "content",
802
- "brand",
803
- "publish",
804
- "users"
805
- ];
806
- function isRole(value) {
807
- return typeof value === "string" && ROLES.includes(value);
808
- }
924
+ //#region src/roles/fields.ts
925
+ const ROLES_MATRIX_FIELD = "@bison-lab/payload-core/admin#RolesMatrixField";
926
+ const ROLES_ROW_LABEL = "@bison-lab/payload-core/admin#RolesRowLabel";
927
+ const ROLE_SLUG_FIELD = "@bison-lab/payload-core/admin#RoleSlugField";
928
+ const ROLE_NAME_FIELD = "@bison-lab/payload-core/admin#RoleNameField";
929
+ const ROLES_FIELD = "@bison-lab/payload-core/admin#RolesField";
930
+ const ROLES_GRANTS_FIELD = "@bison-lab/payload-core/admin#RolesGrantsField";
809
931
  //#endregion
810
- //#region src/roles/matrix.ts
811
- const ROLES_SLUG = "roles";
932
+ //#region src/roles/global.ts
933
+ const roleSlugValidate = (value) => {
934
+ if (typeof value !== "string" || value === "") return true;
935
+ return isRoleSlug(value) || "Each role needs a unique slug.";
936
+ };
937
+ const roleNameValidate = (value) => {
938
+ if (typeof value !== "string" || value.trim() === "") return true;
939
+ return isRoleName(value) || "Use letters and spaces only.";
940
+ };
812
941
  /**
813
- * Default rank and ticks. Brand is Designer only. The API tab is not a
814
- * column it is locked to Developer in `isDeveloper`.
942
+ * Settings Roles. Rank is the array order (Payload's drag handle).
943
+ * Ticks are released catalogue rows. Developer is implicit and shown
944
+ * only to a Developer, with every catalogue tick locked on.
815
945
  */
816
- const DEFAULT_ROLE_MATRIX = [
817
- {
818
- role: "developer",
819
- content: true,
820
- brand: true,
821
- publish: true,
822
- users: true
823
- },
824
- {
825
- role: "admin",
826
- content: true,
827
- brand: false,
828
- publish: true,
829
- users: true
830
- },
831
- {
832
- role: "designer",
833
- content: false,
834
- brand: true,
835
- publish: false,
836
- users: false
837
- },
838
- {
839
- role: "author",
840
- content: true,
841
- brand: false,
842
- publish: false,
843
- users: false
844
- }
845
- ];
846
- const DEVELOPER_DESCRIPTION = "Everything Admin can do, plus the document API tab.";
847
- const LAST_USERS_TICK_MESSAGE = "Keep Users ticked on at least one of Admin or Developer.";
848
- const CAPABILITY_LABELS = {
849
- content: "Content",
850
- brand: "Brand",
851
- publish: "Publish",
852
- users: "Users"
853
- };
854
- function defaultRolesFieldValue() {
855
- return DEFAULT_ROLE_MATRIX.map((row) => ({
856
- id: row.role,
857
- ...row
858
- }));
859
- }
860
- function parseRolesMatrix(value) {
861
- if (!Array.isArray(value)) return {
862
- ok: false,
863
- message: "Roles must be Developer, Admin, Designer, and Author."
864
- };
865
- const rows = [];
866
- const seen = /* @__PURE__ */ new Set();
867
- for (const item of value) {
868
- if (!item || typeof item !== "object") continue;
869
- const role = "role" in item ? item.role : void 0;
870
- if (!isRole(role) || seen.has(role)) continue;
871
- seen.add(role);
872
- rows.push({
873
- id: role,
874
- role,
875
- content: Boolean("content" in item && item.content),
876
- brand: Boolean("brand" in item && item.brand),
877
- publish: Boolean("publish" in item && item.publish),
878
- users: Boolean("users" in item && item.users)
879
- });
880
- }
881
- if (rows.length !== ROLES.length || ROLES.some((role) => !seen.has(role))) return {
882
- ok: false,
883
- message: "Roles must be Developer, Admin, Designer, and Author."
946
+ function createRoles({ extras = [] } = {}) {
947
+ const rolesField = {
948
+ name: "roles",
949
+ type: "array",
950
+ label: "Roles",
951
+ labels: {
952
+ singular: "Role",
953
+ plural: "Roles"
954
+ },
955
+ minRows: 4,
956
+ required: true,
957
+ defaultValue: defaultRolesFieldValue(extras),
958
+ validate: validateRolesMatrix,
959
+ hooks: { beforeChange: [({ value }) => applyRoleSlugsFromNames(value)] },
960
+ admin: {
961
+ components: {
962
+ Field: ROLES_MATRIX_FIELD,
963
+ RowLabel: ROLES_ROW_LABEL
964
+ },
965
+ description: ROLES_FIELD_DESCRIPTION,
966
+ initCollapsed: false
967
+ },
968
+ fields: [
969
+ {
970
+ name: "role",
971
+ type: "text",
972
+ label: "Slug",
973
+ validate: roleSlugValidate,
974
+ admin: { hidden: true }
975
+ },
976
+ {
977
+ name: "label",
978
+ type: "text",
979
+ label: "Name",
980
+ required: true,
981
+ validate: roleNameValidate,
982
+ admin: { components: { Field: ROLE_NAME_FIELD } }
983
+ },
984
+ {
985
+ name: "grants",
986
+ type: "json",
987
+ label: "Grants",
988
+ defaultValue: [],
989
+ admin: { components: { Field: ROLES_GRANTS_FIELD } }
990
+ }
991
+ ]
884
992
  };
885
993
  return {
886
- ok: true,
887
- rows
994
+ slug: ROLES_SLUG,
995
+ label: "Roles",
996
+ admin: {
997
+ group: "Settings",
998
+ hidden: hideUnlessFeature("roles"),
999
+ description: ROLES_GLOBAL_DESCRIPTION
1000
+ },
1001
+ access: {
1002
+ read: (args) => canUseFeature("roles")(args),
1003
+ update: (args) => isAdmin(args)
1004
+ },
1005
+ fields: [rolesField]
888
1006
  };
889
1007
  }
890
- function hasPrivilegedUsersTick(rows) {
891
- return rows.some((row) => (row.role === "admin" || row.role === "developer") && row.users);
892
- }
893
- function validateRolesMatrix(value) {
894
- const parsed = parseRolesMatrix(value);
895
- if (!parsed.ok) return parsed.message;
896
- if (!hasPrivilegedUsersTick(parsed.rows)) return LAST_USERS_TICK_MESSAGE;
897
- return true;
898
- }
899
- /**
900
- * Developer is exclusive. Admin does not swallow Designer: both store.
901
- */
902
- function normalizeStoredRoles(value) {
903
- if (!Array.isArray(value)) return [];
904
- const roles = [...new Set(value.filter(isRole))];
905
- if (roles.includes("developer")) return ["developer"];
906
- return roles;
907
- }
908
- function roleSelectOptions(matrix = DEFAULT_ROLE_MATRIX) {
909
- return matrix.map((row) => ({
910
- label: ROLE_LABELS[row.role],
911
- value: row.role
912
- }));
913
- }
914
- /** Capability copy only. Developer names the API tab; nothing about MCP or seed. */
915
- function roleDescription(role, matrix = DEFAULT_ROLE_MATRIX) {
916
- if (role === "developer") return DEVELOPER_DESCRIPTION;
917
- const row = matrix.find((entry) => entry.role === role);
918
- if (!row) return "No capabilities";
919
- return CAPABILITIES.filter((capability) => row[capability]).map((capability) => CAPABILITY_LABELS[capability]).join(", ") || "No capabilities";
920
- }
921
1008
  //#endregion
922
- //#region src/roles/access.ts
923
- function isAccessArgs(value) {
924
- return typeof value === "object" && value !== null && "req" in value;
925
- }
926
- function storedRoles(user) {
927
- return Array.isArray(user?.roles) ? user.roles : [];
928
- }
929
- function hasRole(user, role) {
930
- return storedRoles(user).includes(role);
931
- }
932
- /** Not a tick. True only when `developer` is stored on the row. */
933
- function isDeveloper(user) {
934
- return hasRole(user, "developer");
935
- }
936
- function hasCapability(user, capability, matrix = DEFAULT_ROLE_MATRIX) {
937
- return storedRoles(user).some((role) => {
938
- return matrix.find((entry) => entry.role === role)?.[capability] === true;
939
- });
940
- }
1009
+ //#region src/roles/seed.ts
941
1010
  /**
942
- * Reads the saved Roles Global, falling back to the seed when the row is
943
- * empty, missing, or unreadable. Always override-access so a Designer
944
- * evaluating Theme does not have to read Settings → Roles.
1011
+ * Writes the default matrix (seed four plus any site extras). For a site's
1012
+ * migration `up()`. Never writes a user row — seeding `developer` on a
1013
+ * person stays a site concern.
945
1014
  */
946
- async function getRolesMatrix(req = {}) {
947
- const findGlobal = req.payload?.findGlobal;
948
- if (typeof findGlobal !== "function") return [...DEFAULT_ROLE_MATRIX];
949
- try {
950
- const doc = await findGlobal({
951
- slug: ROLES_SLUG,
952
- overrideAccess: true,
953
- req
954
- });
955
- const parsed = parseRolesMatrix(doc && typeof doc === "object" && "roles" in doc ? doc.roles : void 0);
956
- return parsed.ok ? parsed.rows : [...DEFAULT_ROLE_MATRIX];
957
- } catch {
958
- return [...DEFAULT_ROLE_MATRIX];
959
- }
960
- }
961
- function capabilityPredicate(capability) {
962
- function predicate(userOrArgs, matrix) {
963
- if (isAccessArgs(userOrArgs)) {
964
- const user = userOrArgs.req.user;
965
- if (matrix) return hasCapability(user, capability, matrix);
966
- return getRolesMatrix(userOrArgs.req).then((rows) => hasCapability(user, capability, rows));
967
- }
968
- return hasCapability(userOrArgs, capability, matrix ?? DEFAULT_ROLE_MATRIX);
969
- }
970
- return predicate;
971
- }
972
- const canManageContent = capabilityPredicate("content");
973
- const canManageBrand = capabilityPredicate("brand");
974
- const canPublish = capabilityPredicate("publish");
975
- /** Users tick on any held role. */
976
- const isAdmin = capabilityPredicate("users");
977
- /** This role id currently has the Users tick. */
978
- function isPrivilegedRole(role, matrix = DEFAULT_ROLE_MATRIX) {
979
- return isRole(role) && matrix.some((row) => row.role === role && row.users);
980
- }
981
- function isAuthenticated(userOrArgs) {
982
- if (isAccessArgs(userOrArgs)) return Boolean(userOrArgs.req.user);
983
- return Boolean(userOrArgs);
984
- }
985
- const isAdminOrSelf = async ({ req }) => {
986
- if (!req.user) return false;
987
- if (await isAdmin({ req })) return true;
988
- return { id: { equals: req.user.id } };
989
- };
990
- const authenticatedOrPublished = ({ req: { user } }) => {
991
- if (isAuthenticated(user)) return true;
992
- return { _status: { equals: "published" } };
993
- };
994
- /** API tab condition: Developer only, not the Users tick. */
995
- function isDeveloperTab({ req }) {
996
- return isDeveloper(req.user);
1015
+ async function seedRoles(payload, extras = []) {
1016
+ return payload.updateGlobal({
1017
+ slug: ROLES_SLUG,
1018
+ data: { roles: defaultRolesFieldValue(extras) }
1019
+ });
997
1020
  }
998
1021
  //#endregion
999
- //#region src/roles/fields.ts
1000
- const ROLES_MATRIX_FIELD = "@bison-lab/payload-core/admin#RolesMatrixField";
1001
- const ROLES_FIELD = "@bison-lab/payload-core/admin#RolesField";
1022
+ //#region src/features/fields.ts
1023
+ const FEATURES_MATRIX_FIELD = "@bison-lab/payload-core/admin#FeaturesMatrixField";
1002
1024
  //#endregion
1003
- //#region src/roles/global.ts
1025
+ //#region src/features/global.ts
1004
1026
  /**
1005
- * Settings → Roles. Rank is the array order (Payload's drag handle). Ticks
1006
- * are Content, Brand, Publish, Users. The API tab is not a column.
1027
+ * Settings → Features. One release switch per catalogue row. Off hides
1028
+ * the tick on Roles; Developer still has the feature. This screen is
1029
+ * not itself a row.
1007
1030
  */
1008
- function createRoles() {
1031
+ function createFeatures({ extras = [] } = {}) {
1032
+ const catalogue = featureCatalogue(extras);
1009
1033
  return {
1010
- slug: ROLES_SLUG,
1011
- label: "Roles",
1034
+ slug: FEATURES_SLUG,
1035
+ label: "Features",
1012
1036
  admin: {
1013
1037
  group: "Settings",
1014
- hidden: ({ user }) => !isAdmin(user),
1015
- description: "Who may do what. Drag to change rank. The API tab is locked to Developer."
1038
+ hidden: ({ user }) => !isDeveloper(user),
1039
+ description: "Which features other people may be granted on Roles."
1016
1040
  },
1017
1041
  access: {
1018
- read: (args) => isAdmin(args),
1019
- update: (args) => isAdmin(args)
1042
+ read: ({ req }) => isDeveloper(req.user),
1043
+ update: ({ req }) => isDeveloper(req.user)
1020
1044
  },
1021
1045
  fields: [{
1022
- name: "roles",
1046
+ name: "features",
1023
1047
  type: "array",
1024
- label: "Roles",
1048
+ label: "Features",
1025
1049
  labels: {
1026
- singular: "Role",
1027
- plural: "Roles"
1050
+ singular: "Feature",
1051
+ plural: "Features"
1028
1052
  },
1029
- minRows: 4,
1030
- maxRows: 4,
1053
+ minRows: catalogue.length,
1054
+ maxRows: catalogue.length,
1031
1055
  required: true,
1032
- defaultValue: defaultRolesFieldValue(),
1033
- validate: validateRolesMatrix,
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
+ }] },
1034
1062
  admin: {
1035
- components: { Field: ROLES_MATRIX_FIELD },
1036
- description: "Drag to change rank. Ticks are Content, Brand, Publish, and Users. The API tab is locked to Developer.",
1063
+ components: { Field: FEATURES_MATRIX_FIELD },
1064
+ description: "Release a feature so it can be assigned on Roles. Off keeps it Developer-only.",
1037
1065
  initCollapsed: false
1038
1066
  },
1039
1067
  fields: [
1040
1068
  {
1041
- name: "role",
1042
- type: "select",
1043
- label: "Role",
1069
+ name: "slug",
1070
+ type: "text",
1071
+ label: "Slug",
1044
1072
  required: true,
1045
- options: ROLES.map((value) => ({
1046
- label: ROLE_LABELS[value],
1047
- value
1048
- })),
1049
1073
  admin: { readOnly: true }
1050
1074
  },
1051
1075
  {
1052
- name: "content",
1053
- type: "checkbox",
1054
- label: "Content"
1055
- },
1056
- {
1057
- name: "brand",
1058
- type: "checkbox",
1059
- label: "Brand"
1076
+ name: "label",
1077
+ type: "text",
1078
+ label: "Name",
1079
+ required: true,
1080
+ admin: { readOnly: true }
1060
1081
  },
1061
1082
  {
1062
- name: "publish",
1063
- type: "checkbox",
1064
- label: "Publish"
1083
+ name: "group",
1084
+ type: "text",
1085
+ label: "Group",
1086
+ admin: { hidden: true }
1065
1087
  },
1066
1088
  {
1067
- name: "users",
1089
+ name: "released",
1068
1090
  type: "checkbox",
1069
- label: "Users"
1091
+ label: "Released",
1092
+ admin: { hidden: true }
1070
1093
  }
1071
1094
  ]
1072
1095
  }]
1073
1096
  };
1074
1097
  }
1075
1098
  //#endregion
1076
- //#region src/roles/seed.ts
1099
+ //#region src/features/seed.ts
1077
1100
  /**
1078
- * Writes the default matrix. For a site's migration `up()`. Never writes a
1079
- * user row seeding `developer` on a person stays a site concern.
1101
+ * Writes the default switchboard (package rows plus any site extras).
1102
+ * For a site's migration `up()`.
1080
1103
  */
1081
- async function seedRoles(payload) {
1104
+ async function seedFeatures(payload, extras = []) {
1082
1105
  return payload.updateGlobal({
1083
- slug: ROLES_SLUG,
1084
- data: { roles: defaultRolesFieldValue() }
1106
+ slug: FEATURES_SLUG,
1107
+ data: { features: defaultFeaturesFieldValue(extras) }
1085
1108
  });
1086
1109
  }
1087
1110
  //#endregion
@@ -1184,14 +1207,15 @@ function createPages({ heroBlocks, isReservedSlug, layoutBlocks, previewPath, pr
1184
1207
  "_status",
1185
1208
  "updatedAt"
1186
1209
  ],
1210
+ hidden: hideUnlessFeature("content"),
1187
1211
  preview: (doc) => previewPath(typeof doc.slug === "string" ? doc.slug : ""),
1188
1212
  ...previewButton ? { components: { edit: { PreviewButton: previewButton } } } : {}
1189
1213
  },
1190
1214
  access: {
1191
1215
  read: authenticatedOrPublished,
1192
1216
  readVersions: isAuthenticated,
1193
- create: canManageContent,
1194
- update: canManageContent,
1217
+ create: canUseFeature("content"),
1218
+ update: canUseFeature("content"),
1195
1219
  delete: isAdmin
1196
1220
  },
1197
1221
  versions: {
@@ -1262,7 +1286,7 @@ async function lockLastAdminDecision(req) {
1262
1286
  });
1263
1287
  }
1264
1288
  function privilegedRoles(matrix) {
1265
- return matrix.filter((row) => row.users).map((row) => row.role);
1289
+ return matrix.filter((row) => isPrivilegedRole(row.role, matrix)).map((row) => row.role);
1266
1290
  }
1267
1291
  function holdsPrivilegedRole(roles, matrix) {
1268
1292
  return Array.isArray(roles) && roles.some((role) => isPrivilegedRole(role, matrix));
@@ -1283,15 +1307,21 @@ async function adminCount(req, excluding) {
1283
1307
  });
1284
1308
  return totalDocs;
1285
1309
  }
1286
- const rolesValidate = async (value, options) => {
1287
- const builtIn = select(value, options);
1288
- if (builtIn !== true) return builtIn;
1289
- if (options.operation !== "update" || options.id === void 0) return true;
1290
- const matrix = await getRolesMatrix(options.req);
1291
- if (holdsPrivilegedRole(value, matrix) || !holdsPrivilegedRole(options.previousValue, matrix)) return true;
1292
- return await adminCount(options.req, options.id) > 0 ? true : LAST_ADMIN_DEMOTE_MESSAGE;
1293
- };
1294
- function createUsers({ secureCookies, rolesField }) {
1310
+ function rolesValidate(extras) {
1311
+ return async (value, options) => {
1312
+ const matrix = await getRolesMatrix(options.req, extras);
1313
+ const builtIn = select(value, {
1314
+ ...options,
1315
+ options: roleSelectOptions(matrix)
1316
+ });
1317
+ if (builtIn !== true) return builtIn;
1318
+ if (options.operation !== "update" || options.id === void 0) return true;
1319
+ if (holdsPrivilegedRole(value, matrix) || !holdsPrivilegedRole(options.previousValue, matrix)) return true;
1320
+ return await adminCount(options.req, options.id) > 0 ? true : LAST_ADMIN_DEMOTE_MESSAGE;
1321
+ };
1322
+ }
1323
+ function createUsers({ secureCookies, rolesField, extras = [] }) {
1324
+ const fallback = seedRoleRows(extras);
1295
1325
  return {
1296
1326
  slug: "users",
1297
1327
  auth: {
@@ -1304,6 +1334,7 @@ function createUsers({ secureCookies, rolesField }) {
1304
1334
  }
1305
1335
  },
1306
1336
  admin: {
1337
+ group: "Settings",
1307
1338
  useAsTitle: "email",
1308
1339
  defaultColumns: [
1309
1340
  "email",
@@ -1311,13 +1342,16 @@ function createUsers({ secureCookies, rolesField }) {
1311
1342
  "lastName",
1312
1343
  "roles"
1313
1344
  ],
1314
- hidden: ({ user }) => !isAdmin(user)
1345
+ hidden: hideUnlessFeature("users")
1315
1346
  },
1316
1347
  access: {
1317
1348
  create: isAdmin,
1318
1349
  delete: isAdmin,
1319
1350
  unlock: isAdmin,
1320
- read: isAdminOrSelf,
1351
+ read: async (args) => {
1352
+ if (await Promise.resolve(canUseFeature("users")(args))) return true;
1353
+ return isAdminOrSelf(args);
1354
+ },
1321
1355
  update: isAdminOrSelf
1322
1356
  },
1323
1357
  hooks: {
@@ -1329,7 +1363,7 @@ function createUsers({ secureCookies, rolesField }) {
1329
1363
  disableErrors: true,
1330
1364
  overrideAccess: true,
1331
1365
  req
1332
- })) || await adminCount(req, id) > 0) return;
1366
+ }), await getRolesMatrix(req, extras)) || await adminCount(req, id) > 0) return;
1333
1367
  throw new APIError(LAST_ADMIN_DELETE_MESSAGE, 400);
1334
1368
  }],
1335
1369
  afterOperation: [async (arg) => {
@@ -1349,14 +1383,17 @@ function createUsers({ secureCookies, rolesField }) {
1349
1383
  hasMany: true,
1350
1384
  required: true,
1351
1385
  defaultValue: ["author"],
1352
- options: roleSelectOptions(),
1386
+ options: roleSelectOptions(fallback),
1353
1387
  admin: {
1354
1388
  components: { Field: rolesField ?? "@bison-lab/payload-core/admin#RolesField" },
1355
1389
  description: "One person can hold several, e.g. Author + Designer."
1356
1390
  },
1357
1391
  access: { update: isAdmin },
1358
- validate: rolesValidate,
1359
- hooks: { beforeValidate: [({ value }) => Array.isArray(value) ? normalizeStoredRoles(value) : value] }
1392
+ validate: rolesValidate(extras),
1393
+ hooks: { beforeValidate: [async ({ value, req }) => {
1394
+ if (!Array.isArray(value)) return value;
1395
+ return normalizeStoredRoles(value, await getRolesMatrix(req, extras));
1396
+ }] }
1360
1397
  }]
1361
1398
  };
1362
1399
  }
@@ -1370,10 +1407,11 @@ function createUsers({ secureCookies, rolesField }) {
1370
1407
  function createMedia({ staticDir = "media", mimeTypes = ["image/*"], imageSizes } = {}) {
1371
1408
  return {
1372
1409
  slug: "media",
1410
+ admin: { hidden: hideUnlessFeature("media") },
1373
1411
  access: {
1374
1412
  read: () => true,
1375
- create: canManageContent,
1376
- update: canManageContent,
1413
+ create: canUseFeature("media"),
1414
+ update: canUseFeature("media"),
1377
1415
  delete: isAdmin
1378
1416
  },
1379
1417
  upload: {
@@ -1432,6 +1470,6 @@ const adminOnlyApiTab = definePlugin({
1432
1470
  })
1433
1471
  });
1434
1472
  //#endregion
1435
- export { BRAND_ASSETS_MIME_TYPES, BRAND_ASSETS_SLUG, CAPABILITIES, DEFAULT_ROLE_MATRIX, DESCRIPTION_LENGTH, DEVELOPER_DESCRIPTION, LAST_ADMIN_DELETE_MESSAGE, LAST_ADMIN_DEMOTE_MESSAGE, LAST_USERS_TICK_MESSAGE, LOOK_FIELD, PAGE_EDITOR_SYSTEM_KEYS, ROLES, ROLES_FIELD, ROLES_MATRIX_FIELD, ROLES_SLUG, ROLE_LABELS, 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_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, authenticatedOrPublished, canManageBrand, canManageContent, canPublish, colorTokenField, createBrandAssets, createMedia, createPages, createRoles, createTheme, createUsers, defaultRolesFieldValue, deleteLibraryColor, documentTitle, firstImageIn, getRolesMatrix, hasCapability, hasRole, isAdmin, isAdminOrSelf, isAuthenticated, isDeveloper, isDeveloperTab, isPrivilegedRole, isRole, lookField, noIndexField, normalizeSlug, normalizeStoredRoles, pageEditorLooks, pageEditorTokens, parseRolesMatrix, persistThemeChild, publishThemeChild, resolveThemeIdentity, rewriteColorToken, roleDescription, roleSelectOptions, sanitizeSvg, seedRoles, seedTheme, seoPlugin, slugField, storedRoles, themeColorKeys, themeLibraryFromDoc, titleTemplate, truncateAtWord, validateRolesMatrix };
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 };
1436
1474
 
1437
1475
  //# sourceMappingURL=index.mjs.map