Files
Tống Thành Đạt 5d0fae6d51 feat(cms): enhance content editors and implement automatic ID generation
Improve the CMS administration interface across multiple pages (Accreditation, Admissions, History, Partnerships, and Policies) with a focus on usability and data integrity.
Key changes include:
- Implement `ensureUniqueIds` utility to automatically generate and maintain unique slugs for content items, removing the need for manual ID entry in the UI.
- Refactor the Admissions calculator to support detailed per-option editing via a new dedicated view and routes.
- Replace basic datalists with a custom, searchable icon combobox component for better visual selection.
- Update `_renderSingletonPageView` to handle active tab persistence via query parameters.
- Streamline editor configurations by removing redundant fields and improving help text.
- Enhance the Admissions "Key Dates" editor with a dynamic table interface for managing columns and rows.
- Normalize data payloads in controllers to ensure consistent API responses and internal linking.
2026-04-21 12:37:19 +07:00

126 lines
3.7 KiB
JavaScript

const HistoryPage = require("../models/historyPage");
const AUDIT_ACTIONS = require("../constants/auditAction");
const historyConfig = require("../utils/contentEditors/historyConfig");
const createPageContentController = require("./_createPageContentController");
const createRenderSingletonPageView = require("./_renderSingletonPageView");
const { ensureUniqueIds } = require("../utils/contentEditorIds");
function getFrontendUrl(req) {
return (process.env.FRONTEND_URL || "http://localhost:3000").replace(/\/$/, "");
}
function normalizeInternalHistoryHref(rawHref, req) {
const href = String(rawHref || "").trim();
if (!href) {
return "";
}
if (href.startsWith("#")) {
return href;
}
if (href.startsWith("/")) {
return href;
}
const frontendUrl = getFrontendUrl(req);
try {
const url = new URL(href);
const frontendOrigin = new URL(frontendUrl).origin;
if (url.origin !== frontendOrigin) {
throw new Error("Highlight link only supports internal anchors or frontend paths.");
}
return `${url.pathname}${url.search}${url.hash}` || "/";
} catch (error) {
if (href.startsWith("http://") || href.startsWith("https://")) {
throw new Error("Highlight link only supports internal anchors or frontend paths.");
}
return href.startsWith("?") ? `/about/history${href}` : `/${href.replace(/^\/+/, "")}`;
}
}
function normalizeHistoryForApi(rawData, req) {
const data = JSON.parse(JSON.stringify(rawData || {}));
const href = data?.highlight?.href;
const frontendUrl = getFrontendUrl(req);
const backendUrl = `${req.protocol}://${req.get("host")}`.replace(/\/$/, "");
if (!href || href.startsWith("#")) {
return data;
}
if (/^https?:\/\//i.test(href)) {
try {
const url = new URL(href);
const frontendOrigin = new URL(frontendUrl).origin;
const backendOrigin = new URL(backendUrl).origin;
if (url.origin === frontendOrigin) {
data.highlight.href = `${frontendUrl}${url.pathname}${url.search}${url.hash}`;
return data;
}
if (url.origin === backendOrigin) {
data.highlight.href = `${frontendUrl}/about/history${url.hash || ""}`;
return data;
}
} catch {
data.highlight.href = `${frontendUrl}/about/history`;
return data;
}
data.highlight.href = `${frontendUrl}/about/history`;
return data;
}
data.highlight.href = `${frontendUrl}${href.startsWith("/") ? href : `/${href}`}`;
return data;
}
const controller = createPageContentController({
model: HistoryPage,
modelName: "HistoryPage",
auditAction: AUDIT_ACTIONS.UPDATE_HISTORY,
editorConfig: historyConfig,
preparePayload(rawPayload, { req }) {
const payload = JSON.parse(JSON.stringify(rawPayload || {}));
if (payload.highlight) {
payload.highlight.href = normalizeInternalHistoryHref(payload.highlight.href, req);
}
if (payload.timeline && Array.isArray(payload.timeline.items)) {
payload.timeline.items = ensureUniqueIds(
payload.timeline.items,
(item) => item.id,
(item, index) => item.title || item.year || `milestone-${index + 1}`,
"milestone",
);
}
return payload;
},
normalizeForApi: normalizeHistoryForApi,
});
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;