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:
Tống Thành Đạt
2026-04-22 18:06:03 +07:00
parent a44d005c13
commit a39c336dd4
19 changed files with 790 additions and 228 deletions
@@ -36,7 +36,22 @@
});
});
form.addEventListener("submit", function () {
form.addEventListener("submit", function (event) {
const duplicateTabs = getDuplicateTabs(state?.grid?.tabs);
clearCategoryTabsValidation();
if (duplicateTabs.length > 0) {
event.preventDefault();
highlightDuplicateCategoryTabs(duplicateTabs);
showToast(
"Duplicate category tab",
`Category tab "${duplicateTabs[0]}" already exists. Please use unique tab names before saving.`,
"danger",
);
return;
}
pageJsonInput.value = JSON.stringify(state);
});
@@ -52,6 +67,62 @@
config.tabs.forEach((tab) => renderSection(tab.key));
}
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 clearCategoryTabsValidation() {
const tabsList = document.querySelector(
'[data-section-key="grid"] .page-editor-array-list',
);
tabsList
?.querySelectorAll(".is-invalid")
.forEach((element) => element.classList.remove("is-invalid"));
}
function highlightDuplicateCategoryTabs(duplicateTabs) {
const tabsList = document.querySelector(
'[data-section-key="grid"] .page-editor-array-list',
);
const normalizedDuplicates = new Set(
duplicateTabs.map((tab) => String(tab || "").trim().toLowerCase()),
);
const duplicateInputs = Array.from(
tabsList?.querySelectorAll(".form-control") || [],
).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 updateTabUrl(tabKey) {
const url = new URL(window.location.href);
url.searchParams.set("tab", tabKey);