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:
@@ -3,6 +3,7 @@
|
||||
const config = window.pageEditorConfig;
|
||||
const initialData = window.pageEditorData;
|
||||
const backendUrl = (window.pageEditorBackendUrl || "").replace(/\/$/, "");
|
||||
const admissionsUi = config?.editorUi || {};
|
||||
const form = document.getElementById("cmsEditorForm");
|
||||
const pageJsonInput = document.getElementById("pageJson");
|
||||
const activeTabInput = document.getElementById("activeTabInput");
|
||||
@@ -94,16 +95,39 @@
|
||||
});
|
||||
}
|
||||
|
||||
function getTabConfig(tabKey) {
|
||||
return (config.tabs || []).find((tab) => tab.key === tabKey) || {};
|
||||
}
|
||||
|
||||
function getObjectFieldConfig(tabKey, fieldKey) {
|
||||
const fields = getTabConfig(tabKey)?.schema?.fields || [];
|
||||
return fields.find((field) => field.key === fieldKey) || {};
|
||||
}
|
||||
|
||||
function getObjectListItemFieldConfig(tabKey, listKey, fieldKey) {
|
||||
const listField = getObjectFieldConfig(tabKey, listKey);
|
||||
const fields = listField?.itemSchema?.fields || [];
|
||||
return fields.find((field) => field.key === fieldKey) || {};
|
||||
}
|
||||
|
||||
function renderKeyDatesSection(container) {
|
||||
normalizeKeyDatesState();
|
||||
|
||||
const keyDates = state.keyDates;
|
||||
const keyDatesUi = admissionsUi.keyDates || {};
|
||||
const titleField = getObjectFieldConfig("keyDates", "title");
|
||||
const row = document.createElement("div");
|
||||
row.className = "row g-3";
|
||||
container.appendChild(row);
|
||||
|
||||
renderLeafField(
|
||||
{ key: "title", label: "Section title", type: "text", maxLength: 60 },
|
||||
{
|
||||
key: "title",
|
||||
label: titleField.label || "Section title",
|
||||
type: titleField.type || "text",
|
||||
maxLength: titleField.maxLength || 60,
|
||||
helpText: titleField.helpText,
|
||||
},
|
||||
row,
|
||||
keyDates,
|
||||
"title",
|
||||
@@ -118,15 +142,15 @@
|
||||
header.className = "d-flex flex-wrap justify-content-between align-items-center gap-3 mb-3";
|
||||
header.innerHTML = `
|
||||
<div>
|
||||
<label class="form-label fw-semibold mb-1">Key dates table</label>
|
||||
<div class="form-text mt-0">Manage the table directly by adding or removing columns and rows.</div>
|
||||
<label class="form-label fw-semibold mb-1">${escapeHtml(keyDatesUi.tableLabel || "Key dates table")}</label>
|
||||
<div class="form-text mt-0">${escapeHtml(keyDatesUi.tableHelpText || "Manage the table directly by adding or removing columns and rows.")}</div>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm" data-add-column="true">
|
||||
<i class="fas fa-table-columns me-1"></i>Add Column
|
||||
<i class="fas fa-table-columns me-1"></i>${escapeHtml(keyDatesUi.addColumnLabel || "Add Column")}
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-primary btn-sm" data-add-row="true">
|
||||
<i class="fas fa-plus me-1"></i>Add Row
|
||||
<i class="fas fa-plus me-1"></i>${escapeHtml(keyDatesUi.addRowLabel || "Add Row")}
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
@@ -150,8 +174,8 @@
|
||||
const input = document.createElement("input");
|
||||
input.type = "text";
|
||||
input.className = "form-control";
|
||||
input.maxLength = 40;
|
||||
input.placeholder = "Column name";
|
||||
input.maxLength = keyDatesUi.columnLabelMaxLength || 40;
|
||||
input.placeholder = keyDatesUi.columnPlaceholder || "Column name";
|
||||
input.value = column.label || "";
|
||||
input.addEventListener("input", function () {
|
||||
column.label = input.value;
|
||||
@@ -183,7 +207,7 @@
|
||||
const actionHead = document.createElement("th");
|
||||
actionHead.className = "text-end";
|
||||
actionHead.style.width = "72px";
|
||||
actionHead.textContent = "Actions";
|
||||
actionHead.textContent = keyDatesUi.actionsLabel || "Actions";
|
||||
headRow.appendChild(actionHead);
|
||||
thead.appendChild(headRow);
|
||||
table.appendChild(thead);
|
||||
@@ -194,7 +218,7 @@
|
||||
const emptyCell = document.createElement("td");
|
||||
emptyCell.colSpan = keyDates.columns.length + 1;
|
||||
emptyCell.className = "text-center text-muted py-4";
|
||||
emptyCell.textContent = "No rows yet.";
|
||||
emptyCell.textContent = keyDatesUi.emptyRowsText || "No rows yet.";
|
||||
emptyRow.appendChild(emptyCell);
|
||||
tbody.appendChild(emptyRow);
|
||||
} else {
|
||||
@@ -206,7 +230,7 @@
|
||||
const input = document.createElement("input");
|
||||
input.type = "text";
|
||||
input.className = "form-control";
|
||||
input.maxLength = 60;
|
||||
input.maxLength = keyDatesUi.cellMaxLength || 60;
|
||||
input.placeholder = column.label || `Column ${columnIndex + 1}`;
|
||||
input.value = rowItem.cells[columnIndex] || "";
|
||||
input.addEventListener("input", function () {
|
||||
@@ -389,18 +413,21 @@
|
||||
: Array.isArray(calculator.modelOptions)
|
||||
? calculator.modelOptions.map((label) => ({ label }))
|
||||
: [];
|
||||
const calculatorUi = admissionsUi.calculator || {};
|
||||
const defaultOption = calculatorUi.defaultOption || {};
|
||||
const optionLabelField = getObjectListItemFieldConfig("calculator", "options", "label");
|
||||
|
||||
calculator.options = rawOptions.slice(0, 3).map((option, index) => ({
|
||||
id: String(option?.id || sanitizeId(option?.label || `option-${index + 1}`) || `option-${index + 1}`),
|
||||
label: String(option?.label || `Option ${index + 1}`).slice(0, 12),
|
||||
paceLabel: String(option?.paceLabel || "Target Pace"),
|
||||
minPaceLabel: String(option?.minPaceLabel || "Relaxed"),
|
||||
maxPaceLabel: String(option?.maxPaceLabel || "Accelerated"),
|
||||
resultLabel: String(option?.resultLabel || "Estimated Monthly Payment"),
|
||||
monthlyAmount: (((String(option?.monthlyAmount || "299").match(/\d[\d,]*/) || [])[0] || "299").replace(/,/g, "")),
|
||||
monthlySuffix: String(option?.monthlySuffix || "/mo"),
|
||||
noteIcon: String(option?.noteIcon || "fa-bolt"),
|
||||
note: String(option?.note || ""),
|
||||
label: String(option?.label || `Option ${index + 1}`).slice(0, optionLabelField.maxLength || 12),
|
||||
paceLabel: String(option?.paceLabel || defaultOption.paceLabel || "Target Pace"),
|
||||
minPaceLabel: String(option?.minPaceLabel || defaultOption.minPaceLabel || "Relaxed"),
|
||||
maxPaceLabel: String(option?.maxPaceLabel || defaultOption.maxPaceLabel || "Accelerated"),
|
||||
resultLabel: String(option?.resultLabel || defaultOption.resultLabel || "Estimated Monthly Payment"),
|
||||
monthlyAmount: (((String(option?.monthlyAmount || defaultOption.monthlyAmount || "299").match(/\d[\d,]*/) || [])[0] || String(defaultOption.monthlyAmount || "299")).replace(/,/g, "")),
|
||||
monthlySuffix: String(option?.monthlySuffix || defaultOption.monthlySuffix || "/mo"),
|
||||
noteIcon: String(option?.noteIcon || defaultOption.noteIcon || "fa-bolt"),
|
||||
note: String(option?.note || defaultOption.note || ""),
|
||||
}));
|
||||
|
||||
delete calculator.modelOptions;
|
||||
@@ -418,19 +445,42 @@
|
||||
normalizeCalculatorState();
|
||||
|
||||
const calculator = state.calculator;
|
||||
const calculatorUi = admissionsUi.calculator || {};
|
||||
const titleField = getObjectFieldConfig("calculator", "title");
|
||||
const descriptionField = getObjectFieldConfig("calculator", "description");
|
||||
const ctaField = getObjectFieldConfig("calculator", "cta");
|
||||
const optionsField = getObjectFieldConfig("calculator", "options");
|
||||
const ctaLabelField = (ctaField.fields || []).find((field) => field.key === "label") || {};
|
||||
const ctaHrefField = (ctaField.fields || []).find((field) => field.key === "href") || {};
|
||||
const optionLabelField = getObjectListItemFieldConfig("calculator", "options", "label");
|
||||
const defaultOption = calculatorUi.defaultOption || {};
|
||||
const maxOptions = Number(calculatorUi.maxOptions) || 3;
|
||||
const row = document.createElement("div");
|
||||
row.className = "row g-3";
|
||||
container.appendChild(row);
|
||||
|
||||
renderLeafField(
|
||||
{ key: "title", label: "Card title", type: "text", maxLength: 60 },
|
||||
{
|
||||
key: "title",
|
||||
label: titleField.label || "Card title",
|
||||
type: titleField.type || "text",
|
||||
maxLength: titleField.maxLength || 60,
|
||||
helpText: titleField.helpText,
|
||||
},
|
||||
row,
|
||||
calculator,
|
||||
"title",
|
||||
{ path: "calculator.title", root: state, item: calculator },
|
||||
);
|
||||
renderLeafField(
|
||||
{ key: "description", label: "Card description", type: "textarea", maxLength: 120, rows: 3 },
|
||||
{
|
||||
key: "description",
|
||||
label: descriptionField.label || "Card description",
|
||||
type: descriptionField.type || "textarea",
|
||||
maxLength: descriptionField.maxLength || 120,
|
||||
rows: descriptionField.rows || 3,
|
||||
helpText: descriptionField.helpText,
|
||||
},
|
||||
row,
|
||||
calculator,
|
||||
"description",
|
||||
@@ -443,22 +493,34 @@
|
||||
const ctaHeader = document.createElement("div");
|
||||
ctaHeader.className = "mb-3";
|
||||
ctaHeader.innerHTML = `
|
||||
<label class="form-label fw-semibold mb-1">Primary button</label>
|
||||
<div class="form-text mt-0">This button appears at the bottom of the calculator card.</div>
|
||||
<label class="form-label fw-semibold mb-1">${escapeHtml(calculatorUi.ctaLabel || ctaField.label || "Primary button")}</label>
|
||||
<div class="form-text mt-0">${escapeHtml(calculatorUi.ctaHelpText || "This button appears at the bottom of the calculator card.")}</div>
|
||||
`;
|
||||
ctaCard.appendChild(ctaHeader);
|
||||
const ctaRow = document.createElement("div");
|
||||
ctaRow.className = "row g-3";
|
||||
ctaCard.appendChild(ctaRow);
|
||||
renderLeafField(
|
||||
{ key: "label", label: "Button label", type: "text", maxLength: 15 },
|
||||
{
|
||||
key: "label",
|
||||
label: ctaLabelField.label || "Button label",
|
||||
type: ctaLabelField.type || "text",
|
||||
maxLength: ctaLabelField.maxLength || 15,
|
||||
helpText: ctaLabelField.helpText,
|
||||
},
|
||||
ctaRow,
|
||||
calculator.cta,
|
||||
"label",
|
||||
{ path: "calculator.cta.label", root: state, item: calculator.cta },
|
||||
);
|
||||
renderLeafField(
|
||||
{ key: "href", label: "Button URL", type: "text", maxLength: 255 },
|
||||
{
|
||||
key: "href",
|
||||
label: ctaHrefField.label || "Button URL",
|
||||
type: ctaHrefField.type || "text",
|
||||
maxLength: ctaHrefField.maxLength || 255,
|
||||
helpText: ctaHrefField.helpText,
|
||||
},
|
||||
ctaRow,
|
||||
calculator.cta,
|
||||
"href",
|
||||
@@ -473,8 +535,8 @@
|
||||
const optionsHeader = document.createElement("div");
|
||||
optionsHeader.className = "mb-3";
|
||||
optionsHeader.innerHTML = `
|
||||
<label class="form-label fw-semibold mb-1">Calculator options</label>
|
||||
<div class="form-text mt-0">Each option has its own pricing labels, amount, note, and icon. Open the edit page to update the option details.</div>
|
||||
<label class="form-label fw-semibold mb-1">${escapeHtml(optionsField.label || calculatorUi.optionsLabel || "Calculator options")}</label>
|
||||
<div class="form-text mt-0">${escapeHtml(optionsField.helpText || calculatorUi.optionsHelpText || "Each option has its own pricing labels, amount, note, and icon. Open the edit page to update the option details.")}</div>
|
||||
`;
|
||||
optionsCard.appendChild(optionsHeader);
|
||||
|
||||
@@ -485,7 +547,7 @@
|
||||
if (!calculator.options.length) {
|
||||
const empty = document.createElement("div");
|
||||
empty.className = "text-muted small";
|
||||
empty.textContent = "No calculator options yet.";
|
||||
empty.textContent = optionsField.emptyText || calculatorUi.optionsEmptyText || "No calculator options yet.";
|
||||
list.appendChild(empty);
|
||||
} else {
|
||||
calculator.options.forEach((option, index) => {
|
||||
@@ -519,24 +581,24 @@
|
||||
});
|
||||
}
|
||||
|
||||
if (calculator.options.length < 3) {
|
||||
if (calculator.options.length < maxOptions) {
|
||||
const addButton = document.createElement("button");
|
||||
addButton.type = "button";
|
||||
addButton.className = "cms-add-button mt-3";
|
||||
addButton.innerHTML = '<i class="fas fa-plus me-2"></i>Add calculator option';
|
||||
addButton.innerHTML = `<i class="fas fa-plus me-2"></i>${escapeHtml(optionsField.addLabel || calculatorUi.addOptionLabel || "Add calculator option")}`;
|
||||
addButton.addEventListener("click", function () {
|
||||
const nextIndex = calculator.options.length + 1;
|
||||
calculator.options.push({
|
||||
id: `option-${Date.now()}`,
|
||||
label: `Option ${nextIndex}`.slice(0, 12),
|
||||
paceLabel: "Target Pace",
|
||||
minPaceLabel: "Relaxed",
|
||||
maxPaceLabel: "Accelerated",
|
||||
resultLabel: "Estimated Monthly Payment",
|
||||
monthlyAmount: "299",
|
||||
monthlySuffix: "/mo",
|
||||
noteIcon: "fa-bolt",
|
||||
note: "",
|
||||
label: String(`Option ${nextIndex}`).slice(0, optionLabelField.maxLength || 12),
|
||||
paceLabel: String(defaultOption.paceLabel || "Target Pace"),
|
||||
minPaceLabel: String(defaultOption.minPaceLabel || "Relaxed"),
|
||||
maxPaceLabel: String(defaultOption.maxPaceLabel || "Accelerated"),
|
||||
resultLabel: String(defaultOption.resultLabel || "Estimated Monthly Payment"),
|
||||
monthlyAmount: String(defaultOption.monthlyAmount || "299"),
|
||||
monthlySuffix: String(defaultOption.monthlySuffix || "/mo"),
|
||||
noteIcon: String(defaultOption.noteIcon || "fa-bolt"),
|
||||
note: String(defaultOption.note || ""),
|
||||
});
|
||||
renderSection("calculator");
|
||||
});
|
||||
@@ -544,7 +606,7 @@
|
||||
} else {
|
||||
const limitNote = document.createElement("div");
|
||||
limitNote.className = "form-text mt-3";
|
||||
limitNote.textContent = "You can add up to 3 calculator options.";
|
||||
limitNote.textContent = calculatorUi.limitHelpText || `You can add up to ${maxOptions} calculator options.`;
|
||||
optionsCard.appendChild(limitNote);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user