refactor(policies): migrate to block-based content editor

Replace the legacy section-based policy editor with a flexible block-based system. This transition enables real-time previews, a wider variety of content types (headings, quotes, callouts), and improved data normalization.
- Implement `policies-block-editor.js` for dynamic content management
- Add `normalizePoliciesDocument` and `validateContent` utilities to handle data migration and integrity
- Update `policiesController.js` to support the new block structure and validation logic
- Migrate `policies.json` from `sections` array to a `content.blocks` structure
- Remove obsolete section editor views and partials
- Update `policiesConfig.js` to reflect the new editor capabilities
This commit is contained in:
Tống Thành Đạt
2026-04-22 10:14:54 +07:00
parent f26b072954
commit cdaddddfeb
10 changed files with 2438 additions and 1265 deletions
+7 -107
View File
@@ -13,7 +13,7 @@ const {
const baseConfig = {
key: "policies",
title: "Policies Management",
subtitle: "Manage the content for the policies page",
subtitle: "Manage policy metadata and block-based content for the policies page",
routeBase: "/admin/policies",
apiPath: "/api/policies",
previewPath: "/policies",
@@ -69,7 +69,7 @@ const baseConfig = {
emptyText: "No policies yet.",
itemActions: [
{
label: "Edit Sections",
label: "Edit Content",
icon: "fas fa-pen-to-square",
hrefTemplate: "/admin/policies/{id}/section",
className: "btn btn-outline-primary btn-sm",
@@ -90,114 +90,14 @@ function createPoliciesSectionEditorConfig(policy, allPolicies = []) {
}));
return {
key: "policySections",
title: `Policy Sections: ${policy.title || policy.id}`,
subtitle: "Manage section content and paragraph order for this policy",
key: "policyContent",
title: `Policy Content: ${policy.title || policy.id}`,
subtitle: "Edit block-based policy content with realtime preview",
routeBase: `/admin/policies/${policy.id}/section`,
previewPath: "/policies",
imageType: "policies",
tabs: [
{
key: "sections",
label: "Sections",
icon: "fas fa-file-lines",
schema: variantList(
"sections",
"Sections",
{
text: {
label: "Text section",
schema: object("section", "Text section", [
text("heading", "Section heading", { maxLength: 60 }),
objectList(
"paragraphs",
"Paragraphs",
[
textarea("text", "Paragraph text", {
maxLength: 500,
rows: 4,
}),
objectList(
"links",
"Inline links",
[
text("label", "Link label", { maxLength: 50 }),
url("href", "Link URL", { maxLength: 255 }),
combobox("tabId", "Switch to policy", {
maxLength: 40,
options: policyOptions,
helpText:
"Optional. Choose another policy to open when this link is clicked.",
}),
],
{
itemLabel: "Link",
sortable: true,
emptyText: "No inline links yet.",
},
),
],
{
itemLabel: "Paragraph",
sortable: true,
emptyText: "No paragraphs yet.",
},
),
]),
},
list: {
label: "List section",
schema: object("section", "List section", [
text("heading", "Section heading", { maxLength: 60 }),
textarea("intro", "Intro text", { maxLength: 220, rows: 3 }),
stringList("items", "List items", {
itemLabel: "List item",
maxLength: 180,
fieldType: "textarea",
sortable: true,
emptyText: "No list items yet.",
}),
]),
},
cards: {
label: "Card section",
schema: object("section", "Card section", [
objectList(
"cards",
"Cards",
[
icon("icon", "Card icon"),
text("title", "Card title", { maxLength: 50 }),
textarea("description", "Card description", {
maxLength: 220,
rows: 4,
}),
object("link", "Card link", [
text("label", "Link label", { maxLength: 50 }),
url("href", "Link URL", { maxLength: 255 }),
combobox("tabId", "Switch to policy", {
maxLength: 40,
options: policyOptions,
}),
]),
],
{
itemLabel: "Card",
sortable: true,
emptyText: "No cards yet.",
},
),
]),
},
},
{
itemLabel: "Section",
sortable: true,
emptyText: "No sections yet.",
},
),
},
],
policyId: policy.id,
policyOptions,
};
}