sc-theme-builder/lib/configWorkflow.js
2026-07-01 20:07:28 -05:00

112 lines
4.5 KiB
JavaScript

// lib/configWorkflow.js
// The presence of configuration_workflow is the single switch that makes every
// facility (headers/routes/layout) a (cfg)=>value factory (ARCHITECTURE.md 3.1/5.1).
// This workflow mounts:
// (1) an informational step linking to the builder SPA (URL_PREFIX + "/editor"), and
// (2) the layoutMode toggle, HARD-BLOCKED unless layout_by_role covers every live
// role so the last-installed fallback is never the live selector and per-request
// overlay suppression stays deterministic (ARCHITECTURE.md 5.7 / 12.3).
const Workflow = require("@saltcorn/data/models/workflow");
const Form = require("@saltcorn/data/models/form");
const Role = require("@saltcorn/data/models/role");
const { getState } = require("@saltcorn/data/db/state");
const { PLUGIN_NAME, URL_PREFIX, CFG } = require("./constants");
const EDITOR_URL = URL_PREFIX + "/editor";
// Every role row in _sc_roles is a "live" role. A role is covered when
// layout_by_role names theme-builder for it (keys are JSON strings, role.id is
// numeric -> normalize to String on both sides). Returns the list of uncovered
// role names; empty means full coverage (ARCHITECTURE.md 5.7 / 12.3).
async function uncoveredRoles() {
const byRole = getState().getConfig("layout_by_role", {}) || {};
const roles = await Role.find({}, { orderBy: "id" });
const out = [];
for (const role of roles) {
const mapped = byRole[role.id] != null ? byRole[role.id] : byRole[String(role.id)];
if (mapped !== PLUGIN_NAME) {
out.push(role.role);
}
}
return out;
}
function configuration_workflow() {
return new Workflow({
steps: [
{
name: "Theme builder",
form: async () =>
new Form({
blurb:
"Design and manage themes in the visual builder, then activate one " +
"from the theme list. The builder opens in a full-page editor.",
fields: [
{
name: "_builder_link",
label: "Open the builder",
input_type: "custom_html",
attributes: {
html:
'<a class="btn btn-primary" target="_blank" rel="noopener" href="' +
EDITOR_URL +
'">Open theme builder &raquo;</a>',
},
},
],
}),
},
{
name: "Layout mode",
form: async () => {
// Resolve coverage up front so the (synchronous) form validator can
// hard-block enabling layoutMode when any live role is uncovered.
const uncovered = await uncoveredRoles();
return new Form({
blurb:
"By default theme-builder only RECOLORS your current theme (a CSS " +
"overlay on top of your active layout). Turn this on to let " +
"theme-builder provide the PAGE LAYOUT itself (navbar / sidebar / " +
"content), making it a selectable site layout. It stays off until " +
"you opt in so that installing the plugin never changes your site's " +
"layout (Saltcorn uses the last-installed layout as the default). " +
"When on, assign theme-builder to roles under 'layout by role'; it " +
"must cover every role before this can be enabled." +
(uncovered.length
? '<div class="alert alert-warning mt-2">Roles not yet mapped to ' +
"theme-builder: " +
uncovered.join(", ") +
". Map them under Settings -> Users and security -> Roles before " +
"enabling layout mode.</div>"
: ""),
fields: [
{
name: CFG.LAYOUT_MODE,
label: "Use theme-builder as the page layout",
type: "Bool",
default: false,
sublabel:
"Hard-blocked unless layout_by_role covers every live role.",
},
],
validator(values) {
if (values[CFG.LAYOUT_MODE] && uncovered.length) {
return (
"Cannot use theme-builder as the page layout yet: assign it to " +
"every role under 'layout by role' first. Uncovered roles: " +
uncovered.join(", ") +
". (This prevents some roles from silently falling back to a " +
"different layout.)"
);
}
},
});
},
},
],
});
}
module.exports = configuration_workflow;