forked from UKSOURCE/cms.lams
feat(cms): enhance content editors with dynamic UI configs and validation
Implement a more flexible, configuration-driven approach for CMS editors across accreditation, admissions, and partnerships modules. - Move UI labels, help texts, and field limits from hardcoded views to `editorUi` configurations in config files. - Add server-side and client-side validation to prevent duplicate category tabs in accreditation and partnerships editors. - Refactor partnership and admission views to dynamically render tabs and fields based on the provided configuration. - Update field length constraints and default values across multiple content editors to better align with frontend requirements. - Improve the admissions calculator editor with dynamic field configurations and default value fallbacks.
This commit is contained in:
@@ -2,6 +2,8 @@
|
||||
(function () {
|
||||
const initialData = window.partnershipsPageData;
|
||||
const backendUrl = (window.partnershipsBackendUrl || "").replace(/\/$/, "");
|
||||
const editorConfig = window.partnershipsEditorConfig || {};
|
||||
const editorUi = window.partnershipsEditorUi || {};
|
||||
const form = document.getElementById("cmsEditorForm");
|
||||
const pageJsonInput = document.getElementById("pageJson");
|
||||
const activeTabInput = document.getElementById("activeTabInput");
|
||||
@@ -14,6 +16,10 @@
|
||||
}
|
||||
|
||||
const state = JSON.parse(JSON.stringify(initialData));
|
||||
const directoryUi = editorUi.directory || {};
|
||||
const inquiryUi = editorUi.inquiryForm || {};
|
||||
const partnerFields = directoryUi.partnerFields || {};
|
||||
const inquiryFieldFields = inquiryUi.fieldFields || {};
|
||||
const slugifyValue = (value, fallback) =>
|
||||
String(value || "")
|
||||
.trim()
|
||||
@@ -32,6 +38,42 @@
|
||||
renderAll();
|
||||
initStaticCounters();
|
||||
|
||||
function getTabConfig(tabKey) {
|
||||
return (editorConfig.tabs || []).find((tab) => tab.key === tabKey) || {};
|
||||
}
|
||||
|
||||
function getObjectFieldConfig(tabKey, fieldKey) {
|
||||
const fields = getTabConfig(tabKey)?.schema?.fields || [];
|
||||
return fields.find((field) => field.key === fieldKey) || {};
|
||||
}
|
||||
|
||||
function getDefaultPartner() {
|
||||
return {
|
||||
name: "",
|
||||
category: "",
|
||||
summary: "",
|
||||
logo: "",
|
||||
logoAlt: "",
|
||||
about: "",
|
||||
collabType: "",
|
||||
benefits: "",
|
||||
};
|
||||
}
|
||||
|
||||
function getDefaultInquiryField() {
|
||||
const typeOptions = inquiryFieldFields.type?.options || [];
|
||||
const widthOptions = inquiryFieldFields.width?.options || [];
|
||||
|
||||
return {
|
||||
label: "",
|
||||
placeholder: "",
|
||||
type: typeOptions[0]?.value || "text",
|
||||
width: widthOptions[0]?.value || "full",
|
||||
required: true,
|
||||
options: [],
|
||||
};
|
||||
}
|
||||
|
||||
function bindStaticEvents() {
|
||||
document.querySelectorAll("[data-tab-key]").forEach((tabTrigger) => {
|
||||
tabTrigger.addEventListener("shown.bs.tab", function () {
|
||||
@@ -50,29 +92,13 @@
|
||||
});
|
||||
|
||||
document.getElementById("addPartnerBtn")?.addEventListener("click", function () {
|
||||
state.directory.partners.push({
|
||||
name: "",
|
||||
category: "",
|
||||
summary: "",
|
||||
logo: "",
|
||||
logoAlt: "",
|
||||
about: "",
|
||||
collabType: "",
|
||||
benefits: "",
|
||||
});
|
||||
state.directory.partners.push(getDefaultPartner());
|
||||
ensurePartnershipIds();
|
||||
renderPartners();
|
||||
});
|
||||
|
||||
document.getElementById("addInquiryFieldBtn")?.addEventListener("click", function () {
|
||||
state.inquiryForm.fields.push({
|
||||
label: "",
|
||||
placeholder: "",
|
||||
type: "text",
|
||||
width: "full",
|
||||
required: true,
|
||||
options: [],
|
||||
});
|
||||
state.inquiryForm.fields.push(getDefaultInquiryField());
|
||||
ensurePartnershipIds();
|
||||
renderInquiryFields();
|
||||
});
|
||||
@@ -81,8 +107,24 @@
|
||||
window.location.reload();
|
||||
});
|
||||
|
||||
form.addEventListener("submit", function () {
|
||||
form.addEventListener("submit", function (event) {
|
||||
syncStaticFields();
|
||||
const duplicateTabs = getDuplicateTabs(state?.directory?.tabs);
|
||||
|
||||
clearDirectoryTabsValidation();
|
||||
|
||||
if (duplicateTabs.length > 0) {
|
||||
const tabsLabel = directoryUi.tabs?.label || "Category tab";
|
||||
event.preventDefault();
|
||||
highlightDuplicateDirectoryTabs(duplicateTabs);
|
||||
showToast(
|
||||
`Duplicate ${tabsLabel.toLowerCase()}`,
|
||||
`${tabsLabel} "${duplicateTabs[0]}" already exists. Please use unique tab names before saving.`,
|
||||
"danger",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
ensurePartnershipIds();
|
||||
pageJsonInput.value = JSON.stringify(state);
|
||||
});
|
||||
@@ -152,6 +194,55 @@
|
||||
});
|
||||
}
|
||||
|
||||
function getDuplicateTabs(tabs) {
|
||||
if (!Array.isArray(tabs)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const seen = new Set();
|
||||
const duplicates = [];
|
||||
|
||||
tabs.forEach((tab) => {
|
||||
const trimmedTab = String(tab || "").trim();
|
||||
const normalizedTab = trimmedTab.toLowerCase();
|
||||
|
||||
if (!normalizedTab) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (seen.has(normalizedTab)) {
|
||||
duplicates.push(trimmedTab);
|
||||
return;
|
||||
}
|
||||
|
||||
seen.add(normalizedTab);
|
||||
});
|
||||
|
||||
return duplicates;
|
||||
}
|
||||
|
||||
function clearDirectoryTabsValidation() {
|
||||
directoryTabsList
|
||||
?.querySelectorAll(".is-invalid")
|
||||
.forEach((element) => element.classList.remove("is-invalid"));
|
||||
}
|
||||
|
||||
function highlightDuplicateDirectoryTabs(duplicateTabs) {
|
||||
const normalizedDuplicates = new Set(
|
||||
duplicateTabs.map((tab) => String(tab || "").trim().toLowerCase()),
|
||||
);
|
||||
|
||||
const duplicateInputs = Array.from(
|
||||
directoryTabsList?.querySelectorAll('[data-field="label"]') || [],
|
||||
).filter((input) => {
|
||||
const inputValue = String(input.value || "").trim().toLowerCase();
|
||||
return inputValue && normalizedDuplicates.has(inputValue);
|
||||
});
|
||||
|
||||
duplicateInputs.forEach((input) => input.classList.add("is-invalid"));
|
||||
duplicateInputs[0]?.focus();
|
||||
}
|
||||
|
||||
function renderAll() {
|
||||
renderDirectoryTabs();
|
||||
renderPartners();
|
||||
@@ -205,7 +296,7 @@
|
||||
input.addEventListener("input", function () {
|
||||
partner[field] = input.value;
|
||||
if (field === "name") {
|
||||
title.textContent = input.value || `Partner ${index + 1}`;
|
||||
title.textContent = input.value || `${partnerFields.name?.label || "Partner"} ${index + 1}`;
|
||||
}
|
||||
if (field === "category") {
|
||||
subtitle.textContent = input.value || "";
|
||||
@@ -287,8 +378,8 @@
|
||||
input.value = field[key] || "";
|
||||
input.addEventListener("input", function () {
|
||||
field[key] = input.value;
|
||||
if (key === "label") {
|
||||
title.textContent = input.value || `Field ${index + 1}`;
|
||||
if (key === "label") {
|
||||
title.textContent = input.value || `${inquiryFieldFields.label?.label || "Field"} ${index + 1}`;
|
||||
}
|
||||
if (key === "type") {
|
||||
subtitle.textContent = input.value || "";
|
||||
|
||||
Reference in New Issue
Block a user