@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/README.md +70 -16
- package/dist/access-B3LglN2J.mjs +662 -0
- package/dist/access-B3LglN2J.mjs.map +1 -0
- package/dist/admin.d.mts +49 -10
- package/dist/admin.d.mts.map +1 -1
- package/dist/admin.mjs +761 -73
- package/dist/admin.mjs.map +1 -1
- package/dist/{identity-DZOb3_Gk.mjs → identity-Dwyq8519.mjs} +48 -2
- package/dist/identity-Dwyq8519.mjs.map +1 -0
- package/dist/index.d.mts +309 -64
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +316 -278
- package/dist/index.mjs.map +1 -1
- package/dist/looks-BbL309kO.mjs.map +1 -1
- package/dist/{looks-CLxXwASa.d.mts → looks-Xs6v5vYU.d.mts} +21 -2
- package/dist/looks-Xs6v5vYU.d.mts.map +1 -0
- package/dist/metadata.d.mts +1 -1
- package/dist/metadata.mjs +1 -1
- package/dist/{share-image-C4ILz4p2.mjs → share-image-BlFAJLnd.mjs} +1 -1
- package/dist/{share-image-C4ILz4p2.mjs.map → share-image-BlFAJLnd.mjs.map} +1 -1
- package/dist/theme.d.mts +2 -2
- package/dist/theme.mjs +2 -2
- package/dist/{title-B2-gWQZd.d.mts → title-sWwcCqHT.d.mts} +1 -1
- package/dist/{title-B2-gWQZd.d.mts.map → title-sWwcCqHT.d.mts.map} +1 -1
- package/package.json +3 -3
- package/dist/identity-DZOb3_Gk.mjs.map +0 -1
- package/dist/looks-CLxXwASa.d.mts.map +0 -1
package/dist/admin.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { a as colorDocFromState, i as SYSTEM_COLOR_KEYS, n as pageEditorLooks, r as pageEditorTokens, s as stateFromColorDoc } from "./looks-BbL309kO.mjs";
|
|
3
|
-
import { ArrayField, CheckboxInput, FieldDescription, FieldError, FieldLabel, fieldBaseClass, useConfig, useField, useForm, useFormFields } from "@payloadcms/ui";
|
|
3
|
+
import { ArrayField, CheckboxInput, FieldDescription, FieldError, FieldLabel, fieldBaseClass, useAuth, useConfig, useField, useForm, useFormFields, useRowLabel } from "@payloadcms/ui";
|
|
4
4
|
import { GREY_SCALES, SHADE_STEPS, contrastForeground, contrastRatio, createColorScale, deriveDarkPalette, deriveLightPalette, hexToHSL, hslToString, includedShadeSteps, isThemeHex, presetHints, setColorScaleInclude } from "@bison-lab/tokens";
|
|
5
5
|
import { useCallback, useEffect, useId, useMemo, useRef, useState } from "react";
|
|
6
6
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
@@ -1010,15 +1010,19 @@ function deleteLibraryColor(doc, key, replacement) {
|
|
|
1010
1010
|
return next;
|
|
1011
1011
|
}
|
|
1012
1012
|
//#endregion
|
|
1013
|
+
//#region src/theme/color-usages.ts
|
|
1014
|
+
/** Theme-store REST path. LibraryField calls `/api/globals/theme` + this. */
|
|
1015
|
+
const COLOR_USAGES_PATH = "/color-usages";
|
|
1016
|
+
//#endregion
|
|
1013
1017
|
//#region src/admin/library-field.tsx
|
|
1014
1018
|
function slugify(value) {
|
|
1015
1019
|
return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
1016
1020
|
}
|
|
1017
1021
|
/**
|
|
1018
|
-
* Custom colors: the same scale controls as system colors.
|
|
1019
|
-
*
|
|
1020
|
-
*
|
|
1021
|
-
*
|
|
1022
|
+
* Custom colors: the same scale controls as system colors. Unused: the
|
|
1023
|
+
* Theme store's color-usages endpoint returns nothing and the row is
|
|
1024
|
+
* gone. In use: pick a live replacement; confirm rewrites stored tokens
|
|
1025
|
+
* then drops the row. Success and Destructive stay off that list.
|
|
1022
1026
|
*/
|
|
1023
1027
|
const LibraryField = ({ field, path, readOnly }) => {
|
|
1024
1028
|
const dataPath = typeof field.admin?.custom?.dataPath === "string" ? field.admin.custom.dataPath : path;
|
|
@@ -1027,14 +1031,21 @@ const LibraryField = ({ field, path, readOnly }) => {
|
|
|
1027
1031
|
hasRows: true
|
|
1028
1032
|
});
|
|
1029
1033
|
const { getDataByPath } = useForm();
|
|
1034
|
+
const { config } = useConfig();
|
|
1030
1035
|
const rows = asLibraryRows(value).length > 0 ? asLibraryRows(value) : asLibraryRows(getDataByPath?.(dataPath));
|
|
1031
1036
|
const [adding, setAdding] = useState(false);
|
|
1032
1037
|
const [name, setName] = useState("");
|
|
1033
1038
|
const [hex, setHex] = useState("");
|
|
1034
1039
|
const [sourceStep, setSourceStep] = useState(500);
|
|
1040
|
+
const [replacing, setReplacing] = useState(null);
|
|
1041
|
+
const [replacement, setReplacement] = useState("");
|
|
1042
|
+
const [busy, setBusy] = useState(false);
|
|
1035
1043
|
function write(next) {
|
|
1036
1044
|
setValue(next);
|
|
1037
1045
|
}
|
|
1046
|
+
function usagesUrl(key) {
|
|
1047
|
+
return `${config.serverURL ?? ""}${config.routes?.api ?? "/api"}/globals/theme${COLOR_USAGES_PATH}?key=${encodeURIComponent(key)}`;
|
|
1048
|
+
}
|
|
1038
1049
|
function add() {
|
|
1039
1050
|
const key = slugify(name);
|
|
1040
1051
|
if (!key || !isThemeHex(hex) || rows.some((row) => row.key === key)) return;
|
|
@@ -1048,9 +1059,42 @@ const LibraryField = ({ field, path, readOnly }) => {
|
|
|
1048
1059
|
setSourceStep(500);
|
|
1049
1060
|
setAdding(false);
|
|
1050
1061
|
}
|
|
1051
|
-
function remove(row) {
|
|
1052
|
-
|
|
1062
|
+
async function remove(row) {
|
|
1063
|
+
const key = row.key ?? "";
|
|
1064
|
+
const response = await fetch(usagesUrl(key), { credentials: "include" });
|
|
1065
|
+
if (!response.ok) return;
|
|
1066
|
+
const usages = (await response.json()).usages ?? [];
|
|
1067
|
+
if (usages.length === 0) {
|
|
1068
|
+
write(deleteLibraryColor({ colors: { library: rows } }, key).colors?.library ?? []);
|
|
1069
|
+
return;
|
|
1070
|
+
}
|
|
1071
|
+
setReplacement("");
|
|
1072
|
+
setReplacing({
|
|
1073
|
+
row,
|
|
1074
|
+
usages
|
|
1075
|
+
});
|
|
1076
|
+
}
|
|
1077
|
+
async function confirmReplace() {
|
|
1078
|
+
if (!replacing || !replacement || busy) return;
|
|
1079
|
+
const key = replacing.row.key ?? "";
|
|
1080
|
+
setBusy(true);
|
|
1081
|
+
try {
|
|
1082
|
+
if (!(await fetch(usagesUrl(key), {
|
|
1083
|
+
method: "POST",
|
|
1084
|
+
credentials: "include",
|
|
1085
|
+
headers: { "Content-Type": "application/json" },
|
|
1086
|
+
body: JSON.stringify({
|
|
1087
|
+
key,
|
|
1088
|
+
replacement
|
|
1089
|
+
})
|
|
1090
|
+
})).ok) return;
|
|
1091
|
+
write(deleteLibraryColor({ colors: { library: rows } }, key, replacement).colors?.library ?? []);
|
|
1092
|
+
setReplacing(null);
|
|
1093
|
+
} finally {
|
|
1094
|
+
setBusy(false);
|
|
1095
|
+
}
|
|
1053
1096
|
}
|
|
1097
|
+
const replacementOptions = replacing ? pageEditorLooks({ colors: { library: rows.filter((row) => row.key !== replacing.row.key) } }) : [];
|
|
1054
1098
|
return /* @__PURE__ */ jsxs("div", {
|
|
1055
1099
|
className: "field-type bl-library",
|
|
1056
1100
|
children: [
|
|
@@ -1136,6 +1180,69 @@ const LibraryField = ({ field, path, readOnly }) => {
|
|
|
1136
1180
|
]
|
|
1137
1181
|
})
|
|
1138
1182
|
}) }) : null,
|
|
1183
|
+
replacing ? /* @__PURE__ */ jsx(ThemePortal, { children: /* @__PURE__ */ jsx("div", {
|
|
1184
|
+
className: "bl-library__modal",
|
|
1185
|
+
role: "dialog",
|
|
1186
|
+
"aria-modal": "true",
|
|
1187
|
+
"aria-labelledby": "bl-library-replace-title",
|
|
1188
|
+
onClick: (event) => {
|
|
1189
|
+
if (event.target === event.currentTarget && !busy) setReplacing(null);
|
|
1190
|
+
},
|
|
1191
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
1192
|
+
className: "bl-library__panel",
|
|
1193
|
+
children: [
|
|
1194
|
+
/* @__PURE__ */ jsxs("div", {
|
|
1195
|
+
className: "bl-library__panel-head",
|
|
1196
|
+
children: [/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsxs("h2", {
|
|
1197
|
+
id: "bl-library-replace-title",
|
|
1198
|
+
children: ["Replace ", replacing.row.label ?? replacing.row.key]
|
|
1199
|
+
}), /* @__PURE__ */ jsx("p", { children: replacing.usages.length === 1 ? "1 document uses this color. Pick a replacement at the same step." : `${replacing.usages.length} documents use this color. Pick a replacement at the same step.` })] }), /* @__PURE__ */ jsx("button", {
|
|
1200
|
+
type: "button",
|
|
1201
|
+
onClick: () => setReplacing(null),
|
|
1202
|
+
"aria-label": "Close",
|
|
1203
|
+
disabled: busy,
|
|
1204
|
+
children: "×"
|
|
1205
|
+
})]
|
|
1206
|
+
}),
|
|
1207
|
+
/* @__PURE__ */ jsx("ul", {
|
|
1208
|
+
className: "bl-library__usages",
|
|
1209
|
+
children: replacing.usages.map((usage, index) => /* @__PURE__ */ jsxs("li", { children: [
|
|
1210
|
+
usage.collection ?? usage.global,
|
|
1211
|
+
usage.id != null ? ` #${usage.id}` : "",
|
|
1212
|
+
" · ",
|
|
1213
|
+
usage.path,
|
|
1214
|
+
" · ",
|
|
1215
|
+
usage.token
|
|
1216
|
+
] }, `${usage.collection ?? usage.global}-${usage.id ?? ""}-${usage.path}-${index}`))
|
|
1217
|
+
}),
|
|
1218
|
+
/* @__PURE__ */ jsxs("label", { children: ["Replacement", /* @__PURE__ */ jsxs("select", {
|
|
1219
|
+
value: replacement,
|
|
1220
|
+
onChange: (event) => setReplacement(event.target.value),
|
|
1221
|
+
children: [/* @__PURE__ */ jsx("option", {
|
|
1222
|
+
value: "",
|
|
1223
|
+
children: "Select a color…"
|
|
1224
|
+
}), replacementOptions.map((option) => /* @__PURE__ */ jsx("option", {
|
|
1225
|
+
value: option.value,
|
|
1226
|
+
children: option.label
|
|
1227
|
+
}, option.value))]
|
|
1228
|
+
})] }),
|
|
1229
|
+
/* @__PURE__ */ jsxs("div", {
|
|
1230
|
+
className: "bl-library__actions",
|
|
1231
|
+
children: [/* @__PURE__ */ jsx("button", {
|
|
1232
|
+
type: "button",
|
|
1233
|
+
onClick: () => setReplacing(null),
|
|
1234
|
+
disabled: busy,
|
|
1235
|
+
children: "Cancel"
|
|
1236
|
+
}), /* @__PURE__ */ jsx("button", {
|
|
1237
|
+
type: "button",
|
|
1238
|
+
disabled: !replacement || busy,
|
|
1239
|
+
onClick: () => void confirmReplace(),
|
|
1240
|
+
children: "Replace and delete"
|
|
1241
|
+
})]
|
|
1242
|
+
})
|
|
1243
|
+
]
|
|
1244
|
+
})
|
|
1245
|
+
}) }) : null,
|
|
1139
1246
|
/* @__PURE__ */ jsx("div", {
|
|
1140
1247
|
className: "bl-library__list",
|
|
1141
1248
|
children: rows.map((row, index) => /* @__PURE__ */ jsx(ColorScaleCard, {
|
|
@@ -1152,7 +1259,7 @@ const LibraryField = ({ field, path, readOnly }) => {
|
|
|
1152
1259
|
};
|
|
1153
1260
|
write(copy);
|
|
1154
1261
|
},
|
|
1155
|
-
onDelete: readOnly ? void 0 : () => remove(row),
|
|
1262
|
+
onDelete: readOnly ? void 0 : () => void remove(row),
|
|
1156
1263
|
readOnly
|
|
1157
1264
|
}, row.key ?? index))
|
|
1158
1265
|
})
|
|
@@ -1173,6 +1280,8 @@ const CSS$3 = `
|
|
|
1173
1280
|
.bl-library__panel p{margin:.5rem 0 0;color:#475569;font-size:14px}
|
|
1174
1281
|
.bl-library__panel label{display:flex;flex-direction:column;gap:.4rem;margin-top:1rem;font-size:13px;font-weight:600}
|
|
1175
1282
|
.bl-library__panel input,.bl-library__panel select{min-height:2.25rem;border:1px solid #cbd5e1;border-radius:8px;padding:.35rem .5rem;background:#fff;color:#0f172a}
|
|
1283
|
+
.bl-library__usages{margin:1rem 0 0;padding-left:1.1rem;color:#475569;font-size:13px;font-weight:400}
|
|
1284
|
+
.bl-library__usages li{margin:.25rem 0}
|
|
1176
1285
|
.bl-library__actions{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:.5rem;margin-top:1rem}
|
|
1177
1286
|
.bl-library__actions button:last-child{border:0;background:#0f172a;color:#fff;font-weight:600}
|
|
1178
1287
|
.bl-library__actions button:last-child:disabled{opacity:.4;cursor:not-allowed}
|
|
@@ -2406,6 +2515,159 @@ const LookField = ({ field, path, readOnly }) => {
|
|
|
2406
2515
|
});
|
|
2407
2516
|
};
|
|
2408
2517
|
//#endregion
|
|
2518
|
+
//#region src/features/types.ts
|
|
2519
|
+
const FEATURES_SLUG = "features";
|
|
2520
|
+
const FEATURE_GROUPS = [
|
|
2521
|
+
"pages",
|
|
2522
|
+
"media",
|
|
2523
|
+
"theme",
|
|
2524
|
+
"users"
|
|
2525
|
+
];
|
|
2526
|
+
const FEATURE_GROUP_LABELS = {
|
|
2527
|
+
pages: "Pages",
|
|
2528
|
+
media: "Media",
|
|
2529
|
+
theme: "Theme",
|
|
2530
|
+
users: "Users"
|
|
2531
|
+
};
|
|
2532
|
+
/**
|
|
2533
|
+
* Package catalogue. Features itself is not a row — that screen is
|
|
2534
|
+
* Developer-only in code. Empty Global falls back to these defaults.
|
|
2535
|
+
*/
|
|
2536
|
+
const PACKAGE_FEATURES = [
|
|
2537
|
+
{
|
|
2538
|
+
slug: "content",
|
|
2539
|
+
label: "Content",
|
|
2540
|
+
group: "pages",
|
|
2541
|
+
defaultReleased: true
|
|
2542
|
+
},
|
|
2543
|
+
{
|
|
2544
|
+
slug: "publish",
|
|
2545
|
+
label: "Publish",
|
|
2546
|
+
group: "pages",
|
|
2547
|
+
defaultReleased: true
|
|
2548
|
+
},
|
|
2549
|
+
{
|
|
2550
|
+
slug: "api-tab",
|
|
2551
|
+
label: "API tab",
|
|
2552
|
+
group: "pages",
|
|
2553
|
+
defaultReleased: false
|
|
2554
|
+
},
|
|
2555
|
+
{
|
|
2556
|
+
slug: "media",
|
|
2557
|
+
label: "Media",
|
|
2558
|
+
group: "media",
|
|
2559
|
+
defaultReleased: true
|
|
2560
|
+
},
|
|
2561
|
+
{
|
|
2562
|
+
slug: "theme-colors",
|
|
2563
|
+
label: "Colors",
|
|
2564
|
+
group: "theme",
|
|
2565
|
+
defaultReleased: true
|
|
2566
|
+
},
|
|
2567
|
+
{
|
|
2568
|
+
slug: "theme-typography",
|
|
2569
|
+
label: "Typography",
|
|
2570
|
+
group: "theme",
|
|
2571
|
+
defaultReleased: true
|
|
2572
|
+
},
|
|
2573
|
+
{
|
|
2574
|
+
slug: "theme-appearance",
|
|
2575
|
+
label: "Appearance",
|
|
2576
|
+
group: "theme",
|
|
2577
|
+
defaultReleased: true
|
|
2578
|
+
},
|
|
2579
|
+
{
|
|
2580
|
+
slug: "theme-identity",
|
|
2581
|
+
label: "Identity",
|
|
2582
|
+
group: "theme",
|
|
2583
|
+
defaultReleased: true
|
|
2584
|
+
},
|
|
2585
|
+
{
|
|
2586
|
+
slug: "brand-assets",
|
|
2587
|
+
label: "Brand assets",
|
|
2588
|
+
group: "theme",
|
|
2589
|
+
defaultReleased: true
|
|
2590
|
+
},
|
|
2591
|
+
{
|
|
2592
|
+
slug: "users",
|
|
2593
|
+
label: "Users",
|
|
2594
|
+
group: "users",
|
|
2595
|
+
defaultReleased: true
|
|
2596
|
+
},
|
|
2597
|
+
{
|
|
2598
|
+
slug: "roles",
|
|
2599
|
+
label: "Roles",
|
|
2600
|
+
group: "users",
|
|
2601
|
+
defaultReleased: true
|
|
2602
|
+
}
|
|
2603
|
+
];
|
|
2604
|
+
function isFeatureGroupId(value) {
|
|
2605
|
+
return typeof value === "string" && FEATURE_GROUPS.includes(value);
|
|
2606
|
+
}
|
|
2607
|
+
function isFeatureSlug(value) {
|
|
2608
|
+
return typeof value === "string" && /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/.test(value);
|
|
2609
|
+
}
|
|
2610
|
+
//#endregion
|
|
2611
|
+
//#region src/features/matrix.ts
|
|
2612
|
+
const MISSING_PACKAGE_FEATURES_MESSAGE = "Features must include Content, Publish, API tab, Media, Theme screens, Brand assets, Users, and Roles.";
|
|
2613
|
+
function featureCatalogue(extras = []) {
|
|
2614
|
+
return [...PACKAGE_FEATURES.map((feature) => ({ ...feature })), ...extras.map((extra) => ({
|
|
2615
|
+
slug: extra.slug,
|
|
2616
|
+
label: extra.label,
|
|
2617
|
+
group: extra.group ?? "users",
|
|
2618
|
+
defaultReleased: extra.defaultReleased ?? false
|
|
2619
|
+
}))];
|
|
2620
|
+
}
|
|
2621
|
+
function defaultFeaturesFieldValue(extras = []) {
|
|
2622
|
+
return featureCatalogue(extras).map((feature) => ({
|
|
2623
|
+
id: feature.slug,
|
|
2624
|
+
slug: feature.slug,
|
|
2625
|
+
label: feature.label,
|
|
2626
|
+
group: feature.group,
|
|
2627
|
+
released: feature.defaultReleased
|
|
2628
|
+
}));
|
|
2629
|
+
}
|
|
2630
|
+
function parseFeaturesMatrix(value) {
|
|
2631
|
+
if (!Array.isArray(value) || value.length === 0) return {
|
|
2632
|
+
ok: false,
|
|
2633
|
+
message: MISSING_PACKAGE_FEATURES_MESSAGE
|
|
2634
|
+
};
|
|
2635
|
+
const rows = [];
|
|
2636
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2637
|
+
for (const item of value) {
|
|
2638
|
+
if (!item || typeof item !== "object") continue;
|
|
2639
|
+
const slug = "slug" in item ? item.slug : void 0;
|
|
2640
|
+
if (!isFeatureSlug(slug) || seen.has(slug)) continue;
|
|
2641
|
+
seen.add(slug);
|
|
2642
|
+
const pack = PACKAGE_FEATURES.find((feature) => feature.slug === slug);
|
|
2643
|
+
const label = "label" in item && typeof item.label === "string" ? item.label : pack?.label ?? slug;
|
|
2644
|
+
const group = "group" in item && isFeatureGroupId(item.group) ? item.group : pack?.group ?? "users";
|
|
2645
|
+
rows.push({
|
|
2646
|
+
id: slug,
|
|
2647
|
+
slug,
|
|
2648
|
+
label,
|
|
2649
|
+
group,
|
|
2650
|
+
released: Boolean("released" in item && item.released)
|
|
2651
|
+
});
|
|
2652
|
+
}
|
|
2653
|
+
if (PACKAGE_FEATURES.some((feature) => !seen.has(feature.slug))) return {
|
|
2654
|
+
ok: false,
|
|
2655
|
+
message: MISSING_PACKAGE_FEATURES_MESSAGE
|
|
2656
|
+
};
|
|
2657
|
+
return {
|
|
2658
|
+
ok: true,
|
|
2659
|
+
rows
|
|
2660
|
+
};
|
|
2661
|
+
}
|
|
2662
|
+
function featureGroupLabel(group) {
|
|
2663
|
+
return isFeatureGroupId(group) ? FEATURE_GROUP_LABELS[group] : group;
|
|
2664
|
+
}
|
|
2665
|
+
function packageFeatureLabel(slug, extras = []) {
|
|
2666
|
+
const pack = PACKAGE_FEATURES.find((feature) => feature.slug === slug);
|
|
2667
|
+
if (pack) return pack.label;
|
|
2668
|
+
return extras.find((feature) => feature.slug === slug)?.label ?? slug;
|
|
2669
|
+
}
|
|
2670
|
+
//#endregion
|
|
2409
2671
|
//#region src/roles/types.ts
|
|
2410
2672
|
/**
|
|
2411
2673
|
* The Roles Global a site's generated types will describe. Optional and
|
|
@@ -2424,108 +2686,352 @@ const ROLE_LABELS = {
|
|
|
2424
2686
|
designer: "Designer",
|
|
2425
2687
|
author: "Author"
|
|
2426
2688
|
};
|
|
2427
|
-
const
|
|
2428
|
-
"
|
|
2429
|
-
"
|
|
2430
|
-
"
|
|
2431
|
-
"
|
|
2689
|
+
const THEME_FEATURE_SLUGS = [
|
|
2690
|
+
"theme-colors",
|
|
2691
|
+
"theme-typography",
|
|
2692
|
+
"theme-appearance",
|
|
2693
|
+
"theme-identity",
|
|
2694
|
+
"brand-assets"
|
|
2432
2695
|
];
|
|
2696
|
+
const DEFAULT_ROLE_GRANTS = {
|
|
2697
|
+
developer: [],
|
|
2698
|
+
admin: [
|
|
2699
|
+
"content",
|
|
2700
|
+
"publish",
|
|
2701
|
+
"media",
|
|
2702
|
+
"users",
|
|
2703
|
+
"roles"
|
|
2704
|
+
],
|
|
2705
|
+
designer: [
|
|
2706
|
+
"content",
|
|
2707
|
+
"media",
|
|
2708
|
+
...THEME_FEATURE_SLUGS,
|
|
2709
|
+
"users",
|
|
2710
|
+
"roles"
|
|
2711
|
+
],
|
|
2712
|
+
author: ["content", "media"]
|
|
2713
|
+
};
|
|
2433
2714
|
function isRole(value) {
|
|
2434
2715
|
return typeof value === "string" && ROLES.includes(value);
|
|
2435
2716
|
}
|
|
2717
|
+
/** Lowercase slug from a letters-and-spaces name: `designer`, `the-designer`. */
|
|
2718
|
+
function isRoleSlug(value) {
|
|
2719
|
+
return typeof value === "string" && /^[a-z]+(-[a-z]+)*$/.test(value);
|
|
2720
|
+
}
|
|
2721
|
+
/** Strip digits and punctuation as the name is typed. Trailing space stays. */
|
|
2722
|
+
function sanitizeRoleNameInput(value) {
|
|
2723
|
+
return value.replace(/[^A-Za-z ]/g, "").replace(/ {2,}/g, " ");
|
|
2724
|
+
}
|
|
2725
|
+
function roleLabel(role, label) {
|
|
2726
|
+
const trimmed = typeof label === "string" ? label.trim() : "";
|
|
2727
|
+
if (trimmed) return trimmed;
|
|
2728
|
+
return isRole(role) ? ROLE_LABELS[role] : role;
|
|
2729
|
+
}
|
|
2730
|
+
function defaultGrantsForRole(role, extraGrants) {
|
|
2731
|
+
if (isRole(role)) return [...DEFAULT_ROLE_GRANTS[role]];
|
|
2732
|
+
return extraGrants ? [...extraGrants] : [];
|
|
2733
|
+
}
|
|
2436
2734
|
//#endregion
|
|
2437
2735
|
//#region src/roles/matrix.ts
|
|
2736
|
+
const ROLES_SLUG = "roles";
|
|
2438
2737
|
/**
|
|
2439
|
-
* Default rank and
|
|
2440
|
-
*
|
|
2738
|
+
* Default rank and grants. Developer is implicit (empty grants). Brand
|
|
2739
|
+
* screens default to Designer. The API tab is not granted until released.
|
|
2441
2740
|
*/
|
|
2442
|
-
const DEFAULT_ROLE_MATRIX =
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
users: "Users"
|
|
2478
|
-
};
|
|
2479
|
-
function defaultRolesFieldValue() {
|
|
2480
|
-
return DEFAULT_ROLE_MATRIX.map((row) => ({
|
|
2741
|
+
const DEFAULT_ROLE_MATRIX = ROLES.map((role) => ({
|
|
2742
|
+
role,
|
|
2743
|
+
grants: defaultGrantsForRole(role)
|
|
2744
|
+
}));
|
|
2745
|
+
const DEVELOPER_DESCRIPTION = "Everything, including features not released for assignment.";
|
|
2746
|
+
const MISSING_SEED_ROLES_MESSAGE = "Roles must include Developer, Admin, Designer, and Author.";
|
|
2747
|
+
const DUPLICATE_ROLE_SLUG_MESSAGE = "Each role needs a unique slug.";
|
|
2748
|
+
function uniqueSlugs(values) {
|
|
2749
|
+
const slugs = [];
|
|
2750
|
+
for (const value of values) if (typeof value === "string" && isFeatureSlug(value) && !slugs.includes(value)) slugs.push(value);
|
|
2751
|
+
return slugs;
|
|
2752
|
+
}
|
|
2753
|
+
/** Read stored grants, or rebuild them from the old capability ticks. */
|
|
2754
|
+
function grantsFromStoredRole(item) {
|
|
2755
|
+
if ("grants" in item && Array.isArray(item.grants)) return uniqueSlugs(item.grants);
|
|
2756
|
+
const grants = [];
|
|
2757
|
+
if ("content" in item && item.content) grants.push("content", "media");
|
|
2758
|
+
if ("publish" in item && item.publish) grants.push("publish");
|
|
2759
|
+
if ("users" in item && item.users) grants.push("users", "roles");
|
|
2760
|
+
if ("brand" in item && item.brand) grants.push(...THEME_FEATURE_SLUGS);
|
|
2761
|
+
return uniqueSlugs(grants);
|
|
2762
|
+
}
|
|
2763
|
+
function seedRoleRows(extras = []) {
|
|
2764
|
+
return [...DEFAULT_ROLE_MATRIX.map((row) => ({
|
|
2765
|
+
...row,
|
|
2766
|
+
grants: [...row.grants],
|
|
2767
|
+
label: isRole(row.role) ? ROLE_LABELS[row.role] : row.role
|
|
2768
|
+
})), ...extras.map((extra) => ({
|
|
2769
|
+
role: extra.role,
|
|
2770
|
+
label: extra.label,
|
|
2771
|
+
grants: extra.grants ? [...extra.grants] : []
|
|
2772
|
+
}))];
|
|
2773
|
+
}
|
|
2774
|
+
function defaultRolesFieldValue(extras = []) {
|
|
2775
|
+
return seedRoleRows(extras).map((row) => ({
|
|
2481
2776
|
id: row.role,
|
|
2482
2777
|
...row
|
|
2483
2778
|
}));
|
|
2484
2779
|
}
|
|
2780
|
+
function readRoleRow(item) {
|
|
2781
|
+
const role = "role" in item ? item.role : void 0;
|
|
2782
|
+
if (typeof role !== "string" || role.trim() === "") return { error: DUPLICATE_ROLE_SLUG_MESSAGE };
|
|
2783
|
+
if (!isRoleSlug(role)) return { error: DUPLICATE_ROLE_SLUG_MESSAGE };
|
|
2784
|
+
return {
|
|
2785
|
+
id: role,
|
|
2786
|
+
role,
|
|
2787
|
+
label: roleLabel(role, "label" in item && typeof item.label === "string" ? item.label : void 0),
|
|
2788
|
+
grants: grantsFromStoredRole(item)
|
|
2789
|
+
};
|
|
2790
|
+
}
|
|
2791
|
+
function parseRolesMatrix(value) {
|
|
2792
|
+
if (!Array.isArray(value)) return {
|
|
2793
|
+
ok: false,
|
|
2794
|
+
message: MISSING_SEED_ROLES_MESSAGE
|
|
2795
|
+
};
|
|
2796
|
+
const rows = [];
|
|
2797
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2798
|
+
for (const item of value) {
|
|
2799
|
+
if (!item || typeof item !== "object") continue;
|
|
2800
|
+
const role = "role" in item ? item.role : void 0;
|
|
2801
|
+
if (typeof role !== "string" || role.trim() === "") continue;
|
|
2802
|
+
const parsed = readRoleRow(item);
|
|
2803
|
+
if ("error" in parsed) return {
|
|
2804
|
+
ok: false,
|
|
2805
|
+
message: parsed.error
|
|
2806
|
+
};
|
|
2807
|
+
if (seen.has(parsed.role)) return {
|
|
2808
|
+
ok: false,
|
|
2809
|
+
message: DUPLICATE_ROLE_SLUG_MESSAGE
|
|
2810
|
+
};
|
|
2811
|
+
seen.add(parsed.role);
|
|
2812
|
+
rows.push(parsed);
|
|
2813
|
+
}
|
|
2814
|
+
if (ROLES.some((role) => !seen.has(role))) return {
|
|
2815
|
+
ok: false,
|
|
2816
|
+
message: MISSING_SEED_ROLES_MESSAGE
|
|
2817
|
+
};
|
|
2818
|
+
return {
|
|
2819
|
+
ok: true,
|
|
2820
|
+
rows
|
|
2821
|
+
};
|
|
2822
|
+
}
|
|
2485
2823
|
function roleSelectOptions(matrix = DEFAULT_ROLE_MATRIX) {
|
|
2486
2824
|
return matrix.map((row) => ({
|
|
2487
|
-
label:
|
|
2825
|
+
label: roleLabel(row.role, row.label),
|
|
2488
2826
|
value: row.role
|
|
2489
2827
|
}));
|
|
2490
2828
|
}
|
|
2491
|
-
/**
|
|
2829
|
+
/** Grant copy only. Developer names the unreleased catalogue; nothing about MCP or seed. */
|
|
2492
2830
|
function roleDescription(role, matrix = DEFAULT_ROLE_MATRIX) {
|
|
2493
2831
|
if (role === "developer") return DEVELOPER_DESCRIPTION;
|
|
2494
2832
|
const row = matrix.find((entry) => entry.role === role);
|
|
2495
2833
|
if (!row) return "No capabilities";
|
|
2496
|
-
return
|
|
2834
|
+
return row.grants.map((slug) => packageFeatureLabel(slug)).join(", ") || "No capabilities";
|
|
2835
|
+
}
|
|
2836
|
+
/**
|
|
2837
|
+
* Seed grants and package labels come back; extra rows stay as they are.
|
|
2838
|
+
*/
|
|
2839
|
+
function resetRolesMatrix(current) {
|
|
2840
|
+
const extras = [];
|
|
2841
|
+
if (Array.isArray(current)) for (const item of current) {
|
|
2842
|
+
if (!item || typeof item !== "object") continue;
|
|
2843
|
+
const parsed = readRoleRow(item);
|
|
2844
|
+
if ("error" in parsed || isRole(parsed.role)) continue;
|
|
2845
|
+
extras.push(parsed);
|
|
2846
|
+
}
|
|
2847
|
+
return [...defaultRolesFieldValue(), ...extras];
|
|
2497
2848
|
}
|
|
2498
2849
|
//#endregion
|
|
2499
2850
|
//#region src/admin/roles-matrix-field.tsx
|
|
2500
|
-
const HIDE_ROW_CHROME = `.bl-roles-matrix .array-
|
|
2851
|
+
const HIDE_ROW_CHROME = `.bl-roles-matrix .array-actions__add,.bl-roles-matrix .array-actions__duplicate,.bl-roles-matrix .array-actions__copy{display:none}.bl-roles-matrix .array-field__add-row{display:none}.bl-roles-matrix--hide-developer .array-field__row:has([data-role-seed="developer"]),.bl-roles-matrix--hide-developer .collapsible:has([data-role-seed="developer"]){display:none}.bl-roles-matrix__toolbar{display:flex;justify-content:flex-end;gap:.5rem;margin-block-end:.75rem}.bl-roles-matrix__reset,.bl-roles-matrix__add{min-height:2.25rem;padding:.35rem .9rem;border:1px solid var(--theme-elevation-150,#e2e8f0);border-radius:8px;background:transparent;color:inherit;font:inherit;cursor:pointer}body.bl-roles-matrix-seed-menu .array-actions__remove{display:none}`;
|
|
2852
|
+
function seedRoleFromTrigger(trigger) {
|
|
2853
|
+
let node = trigger.parentElement;
|
|
2854
|
+
while (node) {
|
|
2855
|
+
const slugs = node.querySelectorAll("[data-role-slug]");
|
|
2856
|
+
if (slugs.length === 1) return isRole(slugs[0]?.getAttribute("data-role-seed") || slugs[0]?.getAttribute("data-role-slug"));
|
|
2857
|
+
if (slugs.length > 1) return false;
|
|
2858
|
+
node = node.parentElement;
|
|
2859
|
+
}
|
|
2860
|
+
return false;
|
|
2861
|
+
}
|
|
2862
|
+
function viewerIsDeveloper(user) {
|
|
2863
|
+
if (!user || typeof user !== "object" || !("roles" in user)) return false;
|
|
2864
|
+
const roles = user.roles;
|
|
2865
|
+
return Array.isArray(roles) && roles.includes("developer");
|
|
2866
|
+
}
|
|
2501
2867
|
/**
|
|
2502
|
-
* Payload's array field: drag rank stays, add
|
|
2503
|
-
*
|
|
2868
|
+
* Payload's array field: drag rank stays, add is for custom rows, seed
|
|
2869
|
+
* rows have no remove. Developer stays in the saved array and is shown
|
|
2870
|
+
* only to a Developer. Reset restores seed ticks and package labels.
|
|
2504
2871
|
*/
|
|
2505
2872
|
const RolesMatrixField = (props) => {
|
|
2506
|
-
const { setValue } = useField({ path: props.path });
|
|
2873
|
+
const { setValue, value } = useField({ path: props.path });
|
|
2874
|
+
const { user } = useAuth();
|
|
2875
|
+
const hideDeveloper = !viewerIsDeveloper(user);
|
|
2876
|
+
const rootRef = useRef(null);
|
|
2877
|
+
const seedMenu = useRef(false);
|
|
2878
|
+
const onClickCapture = useCallback((event) => {
|
|
2879
|
+
const target = event.target;
|
|
2880
|
+
if (!(target instanceof Element)) return;
|
|
2881
|
+
const trigger = target.closest(".array-actions__button");
|
|
2882
|
+
if (trigger) {
|
|
2883
|
+
const owner = trigger.closest(".array-field");
|
|
2884
|
+
seedMenu.current = owner !== null && owner === rootRef.current?.querySelector(".array-field") && seedRoleFromTrigger(trigger);
|
|
2885
|
+
document.body.classList.toggle("bl-roles-matrix-seed-menu", seedMenu.current);
|
|
2886
|
+
return;
|
|
2887
|
+
}
|
|
2888
|
+
if (!target.closest(".array-actions__remove")) return;
|
|
2889
|
+
if (!seedMenu.current) return;
|
|
2890
|
+
event.preventDefault();
|
|
2891
|
+
event.stopPropagation();
|
|
2892
|
+
}, []);
|
|
2507
2893
|
return /* @__PURE__ */ jsxs("div", {
|
|
2508
|
-
className: "bl-roles-matrix",
|
|
2894
|
+
className: ["bl-roles-matrix", hideDeveloper && "bl-roles-matrix--hide-developer"].filter(Boolean).join(" "),
|
|
2895
|
+
ref: rootRef,
|
|
2896
|
+
onClickCapture,
|
|
2509
2897
|
children: [
|
|
2510
2898
|
/* @__PURE__ */ jsx("style", {
|
|
2511
2899
|
href: "bl-roles-matrix",
|
|
2512
2900
|
precedence: "default",
|
|
2513
2901
|
children: HIDE_ROW_CHROME
|
|
2514
2902
|
}),
|
|
2515
|
-
/* @__PURE__ */
|
|
2903
|
+
/* @__PURE__ */ jsxs("div", {
|
|
2516
2904
|
className: "bl-roles-matrix__toolbar",
|
|
2517
|
-
children: /* @__PURE__ */ jsx("button", {
|
|
2905
|
+
children: [/* @__PURE__ */ jsx("button", {
|
|
2906
|
+
type: "button",
|
|
2907
|
+
className: "bl-roles-matrix__add",
|
|
2908
|
+
onClick: () => rootRef.current?.querySelector(".array-field__add-row")?.click(),
|
|
2909
|
+
children: "Add Role"
|
|
2910
|
+
}), /* @__PURE__ */ jsx("button", {
|
|
2518
2911
|
type: "button",
|
|
2519
2912
|
className: "bl-roles-matrix__reset",
|
|
2520
|
-
onClick: () => setValue(
|
|
2913
|
+
onClick: () => setValue(resetRolesMatrix(value)),
|
|
2521
2914
|
children: "Reset to defaults"
|
|
2522
|
-
})
|
|
2915
|
+
})]
|
|
2523
2916
|
}),
|
|
2524
2917
|
/* @__PURE__ */ jsx(ArrayField, { ...props })
|
|
2525
2918
|
]
|
|
2526
2919
|
});
|
|
2527
2920
|
};
|
|
2528
2921
|
//#endregion
|
|
2922
|
+
//#region src/admin/roles-row-label.tsx
|
|
2923
|
+
/**
|
|
2924
|
+
* Array row header. Shows the current display name, not “Role 01”.
|
|
2925
|
+
*/
|
|
2926
|
+
function RolesRowLabel() {
|
|
2927
|
+
const { data } = useRowLabel();
|
|
2928
|
+
const role = typeof data.role === "string" ? data.role : "";
|
|
2929
|
+
const seed = typeof data.seed === "string" && data.seed ? data.seed : isRole(role) ? role : "";
|
|
2930
|
+
const label = roleLabel(role, data.label) || "Role";
|
|
2931
|
+
return /* @__PURE__ */ jsx("span", {
|
|
2932
|
+
"data-role-slug": role,
|
|
2933
|
+
"data-role-seed": seed,
|
|
2934
|
+
children: label
|
|
2935
|
+
});
|
|
2936
|
+
}
|
|
2937
|
+
//#endregion
|
|
2938
|
+
//#region src/admin/role-slug-field.tsx
|
|
2939
|
+
/**
|
|
2940
|
+
* Seed slugs stay read-only. A custom row's slug is typed here.
|
|
2941
|
+
*/
|
|
2942
|
+
const RoleSlugField = ({ field, path, readOnly }) => {
|
|
2943
|
+
const { value, setValue, showError, errorMessage } = useField({ path });
|
|
2944
|
+
const locked = Boolean(readOnly) || isRole(value);
|
|
2945
|
+
const required = Boolean(field.required);
|
|
2946
|
+
const id = `field-${path.replace(/\./g, "__")}`;
|
|
2947
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
2948
|
+
className: [
|
|
2949
|
+
fieldBaseClass,
|
|
2950
|
+
"text",
|
|
2951
|
+
locked && "read-only"
|
|
2952
|
+
].filter(Boolean).join(" "),
|
|
2953
|
+
children: [
|
|
2954
|
+
/* @__PURE__ */ jsx(FieldLabel, {
|
|
2955
|
+
htmlFor: id,
|
|
2956
|
+
label: field.label,
|
|
2957
|
+
required
|
|
2958
|
+
}),
|
|
2959
|
+
/* @__PURE__ */ jsxs("div", {
|
|
2960
|
+
className: `${fieldBaseClass}__wrap`,
|
|
2961
|
+
children: [/* @__PURE__ */ jsx(FieldError, {
|
|
2962
|
+
message: errorMessage,
|
|
2963
|
+
path,
|
|
2964
|
+
showError
|
|
2965
|
+
}), /* @__PURE__ */ jsx("input", {
|
|
2966
|
+
id,
|
|
2967
|
+
"data-role-slug": value ?? "",
|
|
2968
|
+
type: "text",
|
|
2969
|
+
autoComplete: "off",
|
|
2970
|
+
spellCheck: false,
|
|
2971
|
+
readOnly: locked,
|
|
2972
|
+
disabled: locked,
|
|
2973
|
+
value: value ?? "",
|
|
2974
|
+
onChange: (event) => {
|
|
2975
|
+
if (!locked) setValue(event.target.value);
|
|
2976
|
+
}
|
|
2977
|
+
})]
|
|
2978
|
+
}),
|
|
2979
|
+
/* @__PURE__ */ jsx(FieldDescription, {
|
|
2980
|
+
description: field.admin?.description,
|
|
2981
|
+
path
|
|
2982
|
+
})
|
|
2983
|
+
]
|
|
2984
|
+
});
|
|
2985
|
+
};
|
|
2986
|
+
//#endregion
|
|
2987
|
+
//#region src/admin/role-name-field.tsx
|
|
2988
|
+
/**
|
|
2989
|
+
* Role name. Letters and spaces only, so the hidden slug stays
|
|
2990
|
+
* `the-greatest-designer` from `The Greatest Designer`.
|
|
2991
|
+
*/
|
|
2992
|
+
const RoleNameField = ({ field, path, readOnly }) => {
|
|
2993
|
+
const { value, setValue, showError, errorMessage } = useField({ path });
|
|
2994
|
+
const required = Boolean(field.required);
|
|
2995
|
+
const id = `field-${path.replace(/\./g, "__")}`;
|
|
2996
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
2997
|
+
className: [
|
|
2998
|
+
fieldBaseClass,
|
|
2999
|
+
"text",
|
|
3000
|
+
readOnly && "read-only"
|
|
3001
|
+
].filter(Boolean).join(" "),
|
|
3002
|
+
children: [
|
|
3003
|
+
/* @__PURE__ */ jsx(FieldLabel, {
|
|
3004
|
+
htmlFor: id,
|
|
3005
|
+
label: field.label,
|
|
3006
|
+
required
|
|
3007
|
+
}),
|
|
3008
|
+
/* @__PURE__ */ jsxs("div", {
|
|
3009
|
+
className: `${fieldBaseClass}__wrap`,
|
|
3010
|
+
children: [/* @__PURE__ */ jsx(FieldError, {
|
|
3011
|
+
message: errorMessage,
|
|
3012
|
+
path,
|
|
3013
|
+
showError
|
|
3014
|
+
}), /* @__PURE__ */ jsx("input", {
|
|
3015
|
+
id,
|
|
3016
|
+
type: "text",
|
|
3017
|
+
autoComplete: "off",
|
|
3018
|
+
spellCheck: false,
|
|
3019
|
+
readOnly: Boolean(readOnly),
|
|
3020
|
+
disabled: Boolean(readOnly),
|
|
3021
|
+
value: value ?? "",
|
|
3022
|
+
onChange: (event) => {
|
|
3023
|
+
if (!readOnly) setValue(sanitizeRoleNameInput(event.target.value));
|
|
3024
|
+
}
|
|
3025
|
+
})]
|
|
3026
|
+
}),
|
|
3027
|
+
/* @__PURE__ */ jsx(FieldDescription, {
|
|
3028
|
+
description: field.admin?.description,
|
|
3029
|
+
path
|
|
3030
|
+
})
|
|
3031
|
+
]
|
|
3032
|
+
});
|
|
3033
|
+
};
|
|
3034
|
+
//#endregion
|
|
2529
3035
|
//#region src/admin/roles-field.tsx
|
|
2530
3036
|
const GRID_CSS = `.bl-roles__grid{display:grid;gap:.75rem;grid-template-columns:repeat(auto-fit,minmax(12rem,1fr));margin-block:.35rem 0}.bl-roles__option{display:flex;flex-direction:column;gap:.25rem;padding:.65rem .75rem;border:1px solid var(--theme-elevation-150,#e2e8f0);border-radius:8px}.bl-roles__option.is-checked{border-color:var(--theme-elevation-400,#94a3b8)}.bl-roles__option.is-included{opacity:.7}.bl-roles__what{font-size:.8rem;line-height:1.35;color:var(--theme-elevation-500,#64748b)}`;
|
|
2531
3037
|
function optionValue(option) {
|
|
@@ -2534,17 +3040,36 @@ function optionValue(option) {
|
|
|
2534
3040
|
function optionLabel(option) {
|
|
2535
3041
|
return typeof option === "string" ? option : String(option.label);
|
|
2536
3042
|
}
|
|
3043
|
+
function useOfferedRoles(fallback) {
|
|
3044
|
+
const { config } = useConfig();
|
|
3045
|
+
const [live, setLive] = useState(null);
|
|
3046
|
+
const api = config?.routes?.api;
|
|
3047
|
+
const serverURL = config?.serverURL ?? "";
|
|
3048
|
+
useEffect(() => {
|
|
3049
|
+
if (!api) return;
|
|
3050
|
+
let cancelled = false;
|
|
3051
|
+
fetch(`${serverURL}${api}/globals/${ROLES_SLUG}?depth=0`, { credentials: "include" }).then((response) => response.ok ? response.json() : null).then((doc) => {
|
|
3052
|
+
if (cancelled || !doc) return;
|
|
3053
|
+
const parsed = parseRolesMatrix(doc.roles);
|
|
3054
|
+
if (parsed.ok) setLive(roleSelectOptions(parsed.rows));
|
|
3055
|
+
}).catch(() => void 0);
|
|
3056
|
+
return () => {
|
|
3057
|
+
cancelled = true;
|
|
3058
|
+
};
|
|
3059
|
+
}, [api, serverURL]);
|
|
3060
|
+
return live ?? fallback;
|
|
3061
|
+
}
|
|
2537
3062
|
/**
|
|
2538
3063
|
* Users Roles checklist. Order and labels come from the field options (the
|
|
2539
|
-
* Roles Global, or the seed). Copy is `roleDescription`.
|
|
2540
|
-
* exclusive;
|
|
3064
|
+
* Roles Global, or the seed plus extras). Copy is `roleDescription`.
|
|
3065
|
+
* Developer is exclusive; a custom role does not clear Designer.
|
|
2541
3066
|
*/
|
|
2542
3067
|
const RolesField = ({ field, path: pathFromProps, readOnly, validate }) => {
|
|
2543
3068
|
const { admin: { description } = {}, label, localized, options = [], required } = field;
|
|
2544
|
-
const offered = (options.length ? options : roleSelectOptions()).map((option) => ({
|
|
3069
|
+
const offered = useOfferedRoles((options.length ? options : roleSelectOptions()).map((option) => ({
|
|
2545
3070
|
value: optionValue(option),
|
|
2546
3071
|
label: optionLabel(option)
|
|
2547
|
-
}));
|
|
3072
|
+
})));
|
|
2548
3073
|
const { disabled, errorMessage, path, setValue, showError, value } = useField({
|
|
2549
3074
|
potentiallyStalePath: pathFromProps,
|
|
2550
3075
|
validate: useCallback((value, validationOptions) => {
|
|
@@ -2607,7 +3132,7 @@ const RolesField = ({ field, path: pathFromProps, readOnly, validate }) => {
|
|
|
2607
3132
|
className: "bl-roles__grid",
|
|
2608
3133
|
role: "group",
|
|
2609
3134
|
children: offered.map((option) => {
|
|
2610
|
-
const role =
|
|
3135
|
+
const role = option.value;
|
|
2611
3136
|
if (!role) return null;
|
|
2612
3137
|
const included = exclusive !== null && role !== exclusive;
|
|
2613
3138
|
const checked = selected.includes(role);
|
|
@@ -2620,7 +3145,7 @@ const RolesField = ({ field, path: pathFromProps, readOnly, validate }) => {
|
|
|
2620
3145
|
children: [/* @__PURE__ */ jsx(CheckboxInput, {
|
|
2621
3146
|
checked,
|
|
2622
3147
|
id: `${path}-${role}`,
|
|
2623
|
-
label: option.label ||
|
|
3148
|
+
label: option.label || roleLabel(role),
|
|
2624
3149
|
onToggle: () => toggle(role),
|
|
2625
3150
|
readOnly: locked || included
|
|
2626
3151
|
}), /* @__PURE__ */ jsx("span", {
|
|
@@ -2639,6 +3164,169 @@ const RolesField = ({ field, path: pathFromProps, readOnly, validate }) => {
|
|
|
2639
3164
|
});
|
|
2640
3165
|
};
|
|
2641
3166
|
//#endregion
|
|
2642
|
-
|
|
3167
|
+
//#region src/admin/roles-grants-field.tsx
|
|
3168
|
+
const LIST_CSS$1 = `.bl-role-grants{margin-block:.2rem 0;font-size:16px}.bl-role-grants__group{margin-block:.75rem 0}.bl-role-grants__group-title{font-size:13px;letter-spacing:.02em;color:var(--theme-elevation-400,#94a3b8);font-weight:650;margin-block-end:.25rem}.bl-role-grants__row{display:flex;align-items:center;gap:.65rem;padding:.35rem 0}.bl-role-grants__row.is-implicit{opacity:.85}.bl-role-grants input{inline-size:1.15rem;block-size:1.15rem}.bl-role-grants input:disabled{cursor:default}.bl-role-grants__note{margin-block:.35rem 0;font-size:13px;color:var(--theme-elevation-500,#64748b)}`;
|
|
3169
|
+
function asGrants(value) {
|
|
3170
|
+
if (!Array.isArray(value)) return [];
|
|
3171
|
+
return value.filter((entry) => typeof entry === "string");
|
|
3172
|
+
}
|
|
3173
|
+
function rolePathFromGrants(path) {
|
|
3174
|
+
return path.replace(/\.grants$/, ".role");
|
|
3175
|
+
}
|
|
3176
|
+
/**
|
|
3177
|
+
* Released catalogue rows as ticks. Unreleased features do not appear,
|
|
3178
|
+
* except on Developer: every catalogue row is shown granted and locked
|
|
3179
|
+
* so a new Feature is visibly included.
|
|
3180
|
+
*/
|
|
3181
|
+
const RolesGrantsField = (props) => {
|
|
3182
|
+
const { setValue, value } = useField({ path: props.path });
|
|
3183
|
+
const { value: role } = useField({ path: rolePathFromGrants(props.path) });
|
|
3184
|
+
const { config } = useConfig();
|
|
3185
|
+
const [catalogue, setCatalogue] = useState(defaultFeaturesFieldValue());
|
|
3186
|
+
const api = config?.routes?.api;
|
|
3187
|
+
const serverURL = config?.serverURL ?? "";
|
|
3188
|
+
const implicitAll = role === "developer";
|
|
3189
|
+
useEffect(() => {
|
|
3190
|
+
if (!api) return;
|
|
3191
|
+
let cancelled = false;
|
|
3192
|
+
fetch(`${serverURL}${api}/globals/${FEATURES_SLUG}?depth=0`, { credentials: "include" }).then((response) => response.ok ? response.json() : null).then((doc) => {
|
|
3193
|
+
if (cancelled || !doc) return;
|
|
3194
|
+
const parsed = parseFeaturesMatrix(doc.features);
|
|
3195
|
+
if (parsed.ok) setCatalogue(parsed.rows);
|
|
3196
|
+
}).catch(() => void 0);
|
|
3197
|
+
return () => {
|
|
3198
|
+
cancelled = true;
|
|
3199
|
+
};
|
|
3200
|
+
}, [api, serverURL]);
|
|
3201
|
+
const grants = asGrants(value);
|
|
3202
|
+
const shown = implicitAll ? catalogue : catalogue.filter((row) => row.released);
|
|
3203
|
+
function toggle(slug, checked) {
|
|
3204
|
+
if (implicitAll) return;
|
|
3205
|
+
const next = new Set(grants);
|
|
3206
|
+
if (checked) next.add(slug);
|
|
3207
|
+
else next.delete(slug);
|
|
3208
|
+
setValue([...next]);
|
|
3209
|
+
}
|
|
3210
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
3211
|
+
className: "bl-role-grants",
|
|
3212
|
+
children: [
|
|
3213
|
+
/* @__PURE__ */ jsx("style", {
|
|
3214
|
+
href: "bl-role-grants",
|
|
3215
|
+
precedence: "default",
|
|
3216
|
+
children: LIST_CSS$1
|
|
3217
|
+
}),
|
|
3218
|
+
implicitAll ? /* @__PURE__ */ jsx("p", {
|
|
3219
|
+
className: "bl-role-grants__note",
|
|
3220
|
+
children: "Always granted, including features not yet released."
|
|
3221
|
+
}) : null,
|
|
3222
|
+
FEATURE_GROUPS.map((group) => {
|
|
3223
|
+
const members = shown.filter((row) => (row.group ?? "users") === group);
|
|
3224
|
+
if (members.length === 0) return null;
|
|
3225
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
3226
|
+
className: "bl-role-grants__group",
|
|
3227
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
3228
|
+
className: "bl-role-grants__group-title",
|
|
3229
|
+
children: featureGroupLabel(group)
|
|
3230
|
+
}), members.map((row) => /* @__PURE__ */ jsxs("label", {
|
|
3231
|
+
className: ["bl-role-grants__row", implicitAll && "is-implicit"].filter(Boolean).join(" "),
|
|
3232
|
+
children: [/* @__PURE__ */ jsx("input", {
|
|
3233
|
+
type: "checkbox",
|
|
3234
|
+
"aria-label": `${row.label || row.slug}`,
|
|
3235
|
+
checked: implicitAll || grants.includes(row.slug),
|
|
3236
|
+
disabled: implicitAll,
|
|
3237
|
+
onChange: (event) => toggle(row.slug, event.target.checked)
|
|
3238
|
+
}), /* @__PURE__ */ jsx("span", { children: row.label || row.slug })]
|
|
3239
|
+
}, row.slug))]
|
|
3240
|
+
}, group);
|
|
3241
|
+
})
|
|
3242
|
+
]
|
|
3243
|
+
});
|
|
3244
|
+
};
|
|
3245
|
+
//#endregion
|
|
3246
|
+
//#region src/admin/features-matrix-field.tsx
|
|
3247
|
+
const LIST_CSS = `.bl-features{margin-block:.35rem 0;font-size:16px}.bl-features__group{margin-block:1rem 0}.bl-features__group-title{font-size:14px;letter-spacing:.02em;color:var(--theme-elevation-400,#94a3b8);font-weight:650;margin-block-end:.35rem}.bl-features__row{display:flex;align-items:center;justify-content:space-between;gap:1rem;padding:.55rem 0;border-bottom:1px solid var(--theme-elevation-150,#e2e8f0)}.bl-features__row:last-child{border-bottom:0}.bl-features input{inline-size:1.15rem;block-size:1.15rem}`;
|
|
3248
|
+
function asRows(value) {
|
|
3249
|
+
return Array.isArray(value) ? value : [];
|
|
3250
|
+
}
|
|
3251
|
+
function FeatureReleaseRow({ path, index, row, rows, setRows }) {
|
|
3252
|
+
const { setValue: setReleased } = useField({ path: `${path}.${index}.released` });
|
|
3253
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
3254
|
+
className: "bl-features__row",
|
|
3255
|
+
children: [/* @__PURE__ */ jsx("span", { children: row.label || row.slug }), /* @__PURE__ */ jsx("input", {
|
|
3256
|
+
type: "checkbox",
|
|
3257
|
+
"aria-label": `Release ${row.label || row.slug}`,
|
|
3258
|
+
checked: Boolean(row.released),
|
|
3259
|
+
onChange: (event) => {
|
|
3260
|
+
const released = event.target.checked;
|
|
3261
|
+
setReleased(released);
|
|
3262
|
+
setRows(rows.map((entry) => entry.slug === row.slug ? {
|
|
3263
|
+
...entry,
|
|
3264
|
+
released
|
|
3265
|
+
} : entry));
|
|
3266
|
+
}
|
|
3267
|
+
})]
|
|
3268
|
+
});
|
|
3269
|
+
}
|
|
3270
|
+
/**
|
|
3271
|
+
* One release switch per catalogue row, grouped. Off hides the tick on
|
|
3272
|
+
* Roles; Developer still has the feature.
|
|
3273
|
+
*
|
|
3274
|
+
* Writes both the array value and each row's `released` checkbox path.
|
|
3275
|
+
* Payload still registers those child fields; toggling only the parent
|
|
3276
|
+
* array leaves them false and Save stores the old flags.
|
|
3277
|
+
*/
|
|
3278
|
+
const FeaturesMatrixField = (props) => {
|
|
3279
|
+
const { setValue, value } = useField({ path: props.path });
|
|
3280
|
+
const { config } = useConfig();
|
|
3281
|
+
const api = config?.routes?.api;
|
|
3282
|
+
const serverURL = config?.serverURL ?? "";
|
|
3283
|
+
useEffect(() => {
|
|
3284
|
+
if (!api || asRows(value).length > 0) return;
|
|
3285
|
+
let cancelled = false;
|
|
3286
|
+
fetch(`${serverURL}${api}/globals/${FEATURES_SLUG}?depth=0`, { credentials: "include" }).then((response) => response.ok ? response.json() : null).then((doc) => {
|
|
3287
|
+
if (cancelled || !doc) return;
|
|
3288
|
+
const incoming = asRows(doc.features);
|
|
3289
|
+
if (incoming.length > 0) setValue(incoming, true);
|
|
3290
|
+
}).catch(() => void 0);
|
|
3291
|
+
return () => {
|
|
3292
|
+
cancelled = true;
|
|
3293
|
+
};
|
|
3294
|
+
}, [
|
|
3295
|
+
api,
|
|
3296
|
+
serverURL,
|
|
3297
|
+
setValue,
|
|
3298
|
+
value
|
|
3299
|
+
]);
|
|
3300
|
+
const rows = asRows(value);
|
|
3301
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
3302
|
+
className: "bl-features",
|
|
3303
|
+
children: [/* @__PURE__ */ jsx("style", {
|
|
3304
|
+
href: "bl-features",
|
|
3305
|
+
precedence: "default",
|
|
3306
|
+
children: LIST_CSS
|
|
3307
|
+
}), FEATURE_GROUPS.map((group) => {
|
|
3308
|
+
const members = rows.map((row, index) => ({
|
|
3309
|
+
row,
|
|
3310
|
+
index
|
|
3311
|
+
})).filter(({ row }) => (row.group ?? "users") === group);
|
|
3312
|
+
if (members.length === 0) return null;
|
|
3313
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
3314
|
+
className: "bl-features__group",
|
|
3315
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
3316
|
+
className: "bl-features__group-title",
|
|
3317
|
+
children: featureGroupLabel(group)
|
|
3318
|
+
}), members.map(({ row, index }) => /* @__PURE__ */ jsx(FeatureReleaseRow, {
|
|
3319
|
+
path: props.path,
|
|
3320
|
+
index,
|
|
3321
|
+
row,
|
|
3322
|
+
rows,
|
|
3323
|
+
setRows: (next) => setValue(next)
|
|
3324
|
+
}, row.slug))]
|
|
3325
|
+
}, group);
|
|
3326
|
+
})]
|
|
3327
|
+
});
|
|
3328
|
+
};
|
|
3329
|
+
//#endregion
|
|
3330
|
+
export { AppearanceField, ColorField, ColorScaleField, ContrastReport, FeaturesMatrixField, FontField, GreyScaleField, HiddenSaveButton, IdentityFallback, LibraryField, LookField, PairingField, PublishChild, RoleNameField, RoleSlugField, RolesField, RolesGrantsField, RolesMatrixField, RolesRowLabel, SectionHeading, ThemeDocumentControls, ThemeSaveButton };
|
|
2643
3331
|
|
|
2644
3332
|
//# sourceMappingURL=admin.mjs.map
|