forked from UKSOURCE/cms.lams
feat(cms): refactor page content management and implement detailed editors
Refactor the CMS page content system to use a more modular configuration-driven approach. This replaces the monolithic `pageContentConfig.js` with individual configuration files for each page and introduces a shared field utility to standardize editor components. Key changes: - Implement a generic `_renderSingletonPageView` helper to reduce duplication across controllers. - Enhance `_createPageContentController` with `normalizeForEditor`, `normalizeForApi`, and `preparePayload` hooks for custom data transformation. - Create dedicated configuration and view structures for Partnerships, History, Accreditation, Admissions, and Policies pages. - Add a specialized section editor for Policies to manage complex nested content. - Improve the frontend `page-content-editor.js` with support for visibility logic, auto-sequencing for arrays, and URL synchronization for active tabs. - Update data JSON files to align with the new schema.
This commit is contained in:
@@ -8,12 +8,16 @@ function createPageContentController({
|
||||
modelName,
|
||||
auditAction,
|
||||
editorConfig,
|
||||
normalizeForEditor,
|
||||
normalizeForApi,
|
||||
preparePayload,
|
||||
}) {
|
||||
return {
|
||||
async index(req, res) {
|
||||
try {
|
||||
const doc = await model.getSingle();
|
||||
const data = doc.toObject();
|
||||
const rawData = doc.toObject();
|
||||
const data = normalizeForEditor ? normalizeForEditor(rawData, req) : rawData;
|
||||
const frontendUrl = process.env.FRONTEND_URL || "http://localhost:3000";
|
||||
const backendUrl =
|
||||
process.env.BACKEND_URL ?? `${req.protocol}://${req.get("host")}`;
|
||||
@@ -40,7 +44,7 @@ function createPageContentController({
|
||||
|
||||
async update(req, res) {
|
||||
try {
|
||||
const payload =
|
||||
const rawPayload =
|
||||
typeof req.body.pageJson === "string"
|
||||
? JSON.parse(req.body.pageJson)
|
||||
: req.body.pageJson || {};
|
||||
@@ -48,6 +52,9 @@ function createPageContentController({
|
||||
const activeTab = req.body.activeTab || editorConfig.tabs[0].key;
|
||||
const doc = await model.getSingle();
|
||||
const beforeData = JSON.parse(JSON.stringify(doc.toObject()));
|
||||
const payload = preparePayload
|
||||
? preparePayload(rawPayload, { req, doc, beforeData })
|
||||
: rawPayload;
|
||||
|
||||
doc.set(payload);
|
||||
Object.keys(payload).forEach((key) => doc.markModified(key));
|
||||
@@ -98,7 +105,8 @@ function createPageContentController({
|
||||
const rawData = doc.toObject();
|
||||
const backendUrl =
|
||||
process.env.BACKEND_URL ?? `${req.protocol}://${req.get("host")}`;
|
||||
const processed = addBaseUrlToImages(rawData, backendUrl);
|
||||
const normalized = normalizeForApi ? normalizeForApi(rawData, req) : rawData;
|
||||
const processed = addBaseUrlToImages(normalized, backendUrl);
|
||||
return res.json(processed);
|
||||
} catch (error) {
|
||||
console.error(`${editorConfig.key} api error:`, error);
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
function createRenderSingletonPageView({
|
||||
model,
|
||||
editorConfig,
|
||||
view,
|
||||
normalizeForEditor,
|
||||
}) {
|
||||
return async function renderSingletonPageView(req, res) {
|
||||
const doc = await model.getSingle();
|
||||
const rawData = doc.toObject();
|
||||
const data = normalizeForEditor ? normalizeForEditor(rawData, req) : rawData;
|
||||
const frontendUrl = process.env.FRONTEND_URL || "http://localhost:3000";
|
||||
const backendUrl =
|
||||
process.env.BACKEND_URL ?? `${req.protocol}://${req.get("host")}`;
|
||||
|
||||
return res.render(view, {
|
||||
layout: "layouts/main",
|
||||
title: editorConfig.title,
|
||||
subtitle: editorConfig.subtitle,
|
||||
data,
|
||||
editorConfig,
|
||||
activeTab: req.query.tab || editorConfig.tabs[0].key,
|
||||
frontendUrl,
|
||||
backendUrl,
|
||||
previewUrl: `${frontendUrl}${editorConfig.previewPath}`,
|
||||
currentPath: req.path,
|
||||
user: req.session.user,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = createRenderSingletonPageView;
|
||||
@@ -1,11 +1,28 @@
|
||||
const AccreditationPage = require("../models/accreditationPage");
|
||||
const AUDIT_ACTIONS = require("../constants/auditAction");
|
||||
const pageContentConfig = require("../utils/pageContentConfig");
|
||||
const accreditationConfig = require("../utils/contentEditors/accreditationConfig");
|
||||
const createPageContentController = require("./_createPageContentController");
|
||||
const createRenderSingletonPageView = require("./_renderSingletonPageView");
|
||||
|
||||
module.exports = createPageContentController({
|
||||
const controller = createPageContentController({
|
||||
model: AccreditationPage,
|
||||
modelName: "AccreditationPage",
|
||||
auditAction: AUDIT_ACTIONS.UPDATE_ACCREDITATION,
|
||||
editorConfig: pageContentConfig.accreditation,
|
||||
editorConfig: accreditationConfig,
|
||||
});
|
||||
|
||||
controller.index = async function index(req, res) {
|
||||
try {
|
||||
return await createRenderSingletonPageView({
|
||||
model: AccreditationPage,
|
||||
editorConfig: accreditationConfig,
|
||||
view: "admin/accreditation/index",
|
||||
})(req, res);
|
||||
} catch (error) {
|
||||
console.error("accreditation index error:", error);
|
||||
req.flash("error_msg", "Error loading Accreditation Management");
|
||||
return req.session.save(() => res.redirect("/admin/dashboard"));
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = controller;
|
||||
|
||||
@@ -1,11 +1,28 @@
|
||||
const AdmissionsPage = require("../models/admissionsPage");
|
||||
const AUDIT_ACTIONS = require("../constants/auditAction");
|
||||
const pageContentConfig = require("../utils/pageContentConfig");
|
||||
const admissionsConfig = require("../utils/contentEditors/admissionsConfig");
|
||||
const createPageContentController = require("./_createPageContentController");
|
||||
const createRenderSingletonPageView = require("./_renderSingletonPageView");
|
||||
|
||||
module.exports = createPageContentController({
|
||||
const controller = createPageContentController({
|
||||
model: AdmissionsPage,
|
||||
modelName: "AdmissionsPage",
|
||||
auditAction: AUDIT_ACTIONS.UPDATE_ADMISSIONS,
|
||||
editorConfig: pageContentConfig.admissions,
|
||||
editorConfig: admissionsConfig,
|
||||
});
|
||||
|
||||
controller.index = async function index(req, res) {
|
||||
try {
|
||||
return await createRenderSingletonPageView({
|
||||
model: AdmissionsPage,
|
||||
editorConfig: admissionsConfig,
|
||||
view: "admin/admissions/index",
|
||||
})(req, res);
|
||||
} catch (error) {
|
||||
console.error("admissions index error:", error);
|
||||
req.flash("error_msg", "Error loading Admissions Management");
|
||||
return req.session.save(() => res.redirect("/admin/dashboard"));
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = controller;
|
||||
|
||||
@@ -1,11 +1,28 @@
|
||||
const HistoryPage = require("../models/historyPage");
|
||||
const AUDIT_ACTIONS = require("../constants/auditAction");
|
||||
const pageContentConfig = require("../utils/pageContentConfig");
|
||||
const historyConfig = require("../utils/contentEditors/historyConfig");
|
||||
const createPageContentController = require("./_createPageContentController");
|
||||
const createRenderSingletonPageView = require("./_renderSingletonPageView");
|
||||
|
||||
module.exports = createPageContentController({
|
||||
const controller = createPageContentController({
|
||||
model: HistoryPage,
|
||||
modelName: "HistoryPage",
|
||||
auditAction: AUDIT_ACTIONS.UPDATE_HISTORY,
|
||||
editorConfig: pageContentConfig.history,
|
||||
editorConfig: historyConfig,
|
||||
});
|
||||
|
||||
controller.index = async function index(req, res) {
|
||||
try {
|
||||
return await createRenderSingletonPageView({
|
||||
model: HistoryPage,
|
||||
editorConfig: historyConfig,
|
||||
view: "admin/history/index",
|
||||
})(req, res);
|
||||
} catch (error) {
|
||||
console.error("history index error:", error);
|
||||
req.flash("error_msg", "Error loading History Management");
|
||||
return req.session.save(() => res.redirect("/admin/dashboard"));
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = controller;
|
||||
|
||||
@@ -1,11 +1,102 @@
|
||||
const PartnershipsPage = require("../models/partnerships");
|
||||
const AUDIT_ACTIONS = require("../constants/auditAction");
|
||||
const pageContentConfig = require("../utils/pageContentConfig");
|
||||
const partnershipsConfig = require("../utils/contentEditors/partnershipsConfig");
|
||||
const createPageContentController = require("./_createPageContentController");
|
||||
const createRenderSingletonPageView = require("./_renderSingletonPageView");
|
||||
|
||||
module.exports = createPageContentController({
|
||||
function toInquiryField(id, field, type, width) {
|
||||
return {
|
||||
id,
|
||||
label: field?.label || "",
|
||||
placeholder: field?.placeholder || "",
|
||||
type,
|
||||
width,
|
||||
required: true,
|
||||
options: Array.isArray(field?.options) ? field.options : [],
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeInquiryForm(data) {
|
||||
if (!data?.inquiryForm) {
|
||||
return data;
|
||||
}
|
||||
|
||||
if (Array.isArray(data.inquiryForm.fields)) {
|
||||
return data;
|
||||
}
|
||||
|
||||
const legacyFields = data.inquiryForm.fields || {};
|
||||
|
||||
return {
|
||||
...data,
|
||||
inquiryForm: {
|
||||
...data.inquiryForm,
|
||||
fields: [
|
||||
toInquiryField("firstName", legacyFields.firstName, "text", "half"),
|
||||
toInquiryField("lastName", legacyFields.lastName, "text", "half"),
|
||||
toInquiryField("organization", legacyFields.organization, "text", "full"),
|
||||
toInquiryField(
|
||||
"partnershipType",
|
||||
legacyFields.partnershipType,
|
||||
"select",
|
||||
"full",
|
||||
),
|
||||
toInquiryField("message", legacyFields.message, "textarea", "full"),
|
||||
].filter((field) => field.label || field.placeholder || field.id),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function prepareInquiryPayload(payload) {
|
||||
const normalized = normalizeInquiryForm(payload);
|
||||
const fields = Array.isArray(normalized?.inquiryForm?.fields)
|
||||
? normalized.inquiryForm.fields
|
||||
: [];
|
||||
|
||||
return {
|
||||
...normalized,
|
||||
inquiryForm: {
|
||||
...normalized.inquiryForm,
|
||||
fields: fields.map((field, index) => ({
|
||||
id:
|
||||
String(field.id || `field-${index + 1}`)
|
||||
.trim()
|
||||
.replace(/\s+/g, "")
|
||||
.replace(/[^a-zA-Z0-9_-]/g, "") || `field-${index + 1}`,
|
||||
label: field.label || "",
|
||||
placeholder: field.placeholder || "",
|
||||
type: field.type || "text",
|
||||
width: field.width || "full",
|
||||
required: Boolean(field.required),
|
||||
options: Array.isArray(field.options) ? field.options.filter(Boolean) : [],
|
||||
})),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const controller = createPageContentController({
|
||||
model: PartnershipsPage,
|
||||
modelName: "PartnershipsPage",
|
||||
auditAction: AUDIT_ACTIONS.UPDATE_PARTNERSHIPS,
|
||||
editorConfig: pageContentConfig.partnerships,
|
||||
editorConfig: partnershipsConfig,
|
||||
normalizeForEditor: normalizeInquiryForm,
|
||||
normalizeForApi: normalizeInquiryForm,
|
||||
preparePayload: prepareInquiryPayload,
|
||||
});
|
||||
|
||||
controller.index = async function index(req, res) {
|
||||
try {
|
||||
return await createRenderSingletonPageView({
|
||||
model: PartnershipsPage,
|
||||
editorConfig: partnershipsConfig,
|
||||
view: "admin/partnerships/index",
|
||||
normalizeForEditor: normalizeInquiryForm,
|
||||
})(req, res);
|
||||
} catch (error) {
|
||||
console.error("partnerships index error:", error);
|
||||
req.flash("error_msg", "Error loading Partnerships Management");
|
||||
return req.session.save(() => res.redirect("/admin/dashboard"));
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = controller;
|
||||
|
||||
@@ -1,11 +1,156 @@
|
||||
const PoliciesPage = require("../models/policiesPage");
|
||||
const AUDIT_ACTIONS = require("../constants/auditAction");
|
||||
const pageContentConfig = require("../utils/pageContentConfig");
|
||||
const {
|
||||
baseConfig: policiesConfig,
|
||||
createPoliciesSectionEditorConfig,
|
||||
} = require("../utils/contentEditors/policiesConfig");
|
||||
const createPageContentController = require("./_createPageContentController");
|
||||
const createRenderSingletonPageView = require("./_renderSingletonPageView");
|
||||
const writeAuditLog = require("../audit/writeAuditLog");
|
||||
const diffObject = require("../audit/diffObject");
|
||||
const jsonHelper = require("../utils/jsonHelper");
|
||||
|
||||
module.exports = createPageContentController({
|
||||
function formatLastUpdated(date = new Date()) {
|
||||
return `Last updated: ${new Intl.DateTimeFormat("en-US", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
}).format(date)}`;
|
||||
}
|
||||
|
||||
function withLastUpdated(payload) {
|
||||
return {
|
||||
...payload,
|
||||
hero: {
|
||||
...(payload.hero || {}),
|
||||
lastUpdated: formatLastUpdated(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const baseController = createPageContentController({
|
||||
model: PoliciesPage,
|
||||
modelName: "PoliciesPage",
|
||||
auditAction: AUDIT_ACTIONS.UPDATE_POLICIES,
|
||||
editorConfig: pageContentConfig.policies,
|
||||
editorConfig: policiesConfig,
|
||||
preparePayload: withLastUpdated,
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
...baseController,
|
||||
async index(req, res) {
|
||||
try {
|
||||
return await createRenderSingletonPageView({
|
||||
model: PoliciesPage,
|
||||
editorConfig: policiesConfig,
|
||||
view: "admin/policies/index",
|
||||
})(req, res);
|
||||
} catch (error) {
|
||||
console.error("policies index error:", error);
|
||||
req.flash("error_msg", "Error loading Policies Management");
|
||||
return req.session.save(() => res.redirect("/admin/dashboard"));
|
||||
}
|
||||
},
|
||||
|
||||
async editSections(req, res) {
|
||||
try {
|
||||
const doc = await PoliciesPage.getSingle();
|
||||
const data = doc.toObject();
|
||||
const policy = (data.policies || []).find(
|
||||
(item) => item.id === req.params.policyId,
|
||||
);
|
||||
|
||||
if (!policy) {
|
||||
req.flash("error_msg", "Policy not found");
|
||||
return req.session.save(() => res.redirect("/admin/policies"));
|
||||
}
|
||||
|
||||
const frontendUrl = process.env.FRONTEND_URL || "http://localhost:3000";
|
||||
const backendUrl =
|
||||
process.env.BACKEND_URL ?? `${req.protocol}://${req.get("host")}`;
|
||||
const editorConfig = createPoliciesSectionEditorConfig(
|
||||
policy,
|
||||
data.policies || [],
|
||||
);
|
||||
|
||||
return res.render("admin/policies/sections", {
|
||||
layout: "layouts/main",
|
||||
title: editorConfig.title,
|
||||
subtitle: editorConfig.subtitle,
|
||||
data: { sections: policy.sections || [] },
|
||||
editorConfig,
|
||||
activeTab: "sections",
|
||||
frontendUrl,
|
||||
backendUrl,
|
||||
previewUrl: `${frontendUrl}${editorConfig.previewPath}`,
|
||||
currentPath: req.path,
|
||||
user: req.session.user,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("policies section index error:", error);
|
||||
req.flash("error_msg", "Error loading policy sections");
|
||||
return req.session.save(() => res.redirect("/admin/policies"));
|
||||
}
|
||||
},
|
||||
|
||||
async updateSections(req, res) {
|
||||
try {
|
||||
const payload =
|
||||
typeof req.body.pageJson === "string"
|
||||
? JSON.parse(req.body.pageJson)
|
||||
: req.body.pageJson || {};
|
||||
const doc = await PoliciesPage.getSingle();
|
||||
const beforeData = JSON.parse(JSON.stringify(doc.toObject()));
|
||||
const policyIndex = (doc.policies || []).findIndex(
|
||||
(item) => item.id === req.params.policyId,
|
||||
);
|
||||
|
||||
if (policyIndex === -1) {
|
||||
req.flash("error_msg", "Policy not found");
|
||||
return req.session.save(() => res.redirect("/admin/policies"));
|
||||
}
|
||||
|
||||
doc.policies[policyIndex].sections = Array.isArray(payload.sections)
|
||||
? payload.sections
|
||||
: [];
|
||||
doc.hero = {
|
||||
...(doc.hero || {}),
|
||||
lastUpdated: formatLastUpdated(),
|
||||
};
|
||||
doc.markModified("policies");
|
||||
doc.markModified("hero");
|
||||
await doc.save();
|
||||
|
||||
const afterData = JSON.parse(JSON.stringify(doc.toObject()));
|
||||
const changes = diffObject(beforeData, afterData);
|
||||
|
||||
if (changes.length > 0) {
|
||||
await writeAuditLog({
|
||||
model: "PoliciesPage",
|
||||
documentId: doc._id,
|
||||
action: AUDIT_ACTIONS.UPDATE_POLICIES,
|
||||
before: beforeData,
|
||||
after: afterData,
|
||||
changes,
|
||||
req,
|
||||
});
|
||||
}
|
||||
|
||||
const finalData = await PoliciesPage.findOne()
|
||||
.select("-_id -__v -createdAt -updatedAt")
|
||||
.lean();
|
||||
jsonHelper.writeJsonFile(policiesConfig.dataFile, finalData);
|
||||
|
||||
req.flash("success_msg", "Policy sections updated successfully");
|
||||
return req.session.save(() =>
|
||||
res.redirect(`/admin/policies/${req.params.policyId}/section`),
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("policies section update error:", error);
|
||||
req.flash("error_msg", `Error updating policy sections: ${error.message}`);
|
||||
return req.session.save(() =>
|
||||
res.redirect(`/admin/policies/${req.params.policyId}/section`),
|
||||
);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user