diff --git a/core/src/main/java/ca/yukon/aem/core/models/ReviewSummaryModel.java b/core/src/main/java/ca/yukon/aem/core/models/ReviewSummaryModel.java new file mode 100644 index 0000000..bc31fe5 --- /dev/null +++ b/core/src/main/java/ca/yukon/aem/core/models/ReviewSummaryModel.java @@ -0,0 +1,65 @@ +package ca.yukon.aem.core.models; + +import com.adobe.cq.export.json.ExporterConstants; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.sling.api.SlingHttpServletRequest; +import org.apache.sling.models.annotations.DefaultInjectionStrategy; +import org.apache.sling.models.annotations.Exporter; +import org.apache.sling.models.annotations.Model; +import org.apache.sling.models.annotations.injectorspecific.ValueMapValue; + +import javax.annotation.PostConstruct; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** + * Sling Model for the Review Summary component. + * + * Reads authoring dialog properties and exposes them to the HTL template + * as data attributes consumed by review-summary.js at runtime. + * + * Resource type: yourproject/components/form/review-summary + */ +@Model( + adaptables = SlingHttpServletRequest.class, + defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL +) +@Exporter(name = ExporterConstants.SLING_MODEL_EXPORTER_NAME, + extensions = ExporterConstants.SLING_MODEL_EXTENSION) +public class ReviewSummaryModel { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + /** Comma-separated field names (or Somerset names) to suppress from the review. */ + @ValueMapValue + private String[] excludedFields; + + /** Whether to render an "Edit" link next to each section heading. Default: true. */ + @ValueMapValue + private boolean showEditLinks = true; + + private String excludedFieldsJson; + + @PostConstruct + protected void init() { + List fields = (excludedFields != null) + ? Arrays.asList(excludedFields) + : Collections.emptyList(); + + try { + excludedFieldsJson = MAPPER.writeValueAsString(fields); + } catch (JsonProcessingException e) { + excludedFieldsJson = "[]"; + } + } + + public String getExcludedFieldsJson() { + return excludedFieldsJson; + } + + public boolean isShowEditLinks() { + return showEditLinks; + } +} diff --git a/ui.apps/src/main/content/jcr_root/apps/yukon-forms/components/adaptiveForm/review-summary/.content.xml b/ui.apps/src/main/content/jcr_root/apps/yukon-forms/components/adaptiveForm/review-summary/.content.xml new file mode 100644 index 0000000..b298260 --- /dev/null +++ b/ui.apps/src/main/content/jcr_root/apps/yukon-forms/components/adaptiveForm/review-summary/.content.xml @@ -0,0 +1,10 @@ + + \ No newline at end of file diff --git a/ui.apps/src/main/content/jcr_root/apps/yukon-forms/components/adaptiveForm/review-summary/_cq_dialog/.content.xml b/ui.apps/src/main/content/jcr_root/apps/yukon-forms/components/adaptiveForm/review-summary/_cq_dialog/.content.xml new file mode 100644 index 0000000..fb52096 --- /dev/null +++ b/ui.apps/src/main/content/jcr_root/apps/yukon-forms/components/adaptiveForm/review-summary/_cq_dialog/.content.xml @@ -0,0 +1,49 @@ + + + + + + +
+ + + + + + + + + +
+
+
+
+
+
\ No newline at end of file diff --git a/ui.apps/src/main/content/jcr_root/apps/yukon-forms/components/adaptiveForm/review-summary/clientlibs/.content.xml b/ui.apps/src/main/content/jcr_root/apps/yukon-forms/components/adaptiveForm/review-summary/clientlibs/.content.xml new file mode 100644 index 0000000..27ed626 --- /dev/null +++ b/ui.apps/src/main/content/jcr_root/apps/yukon-forms/components/adaptiveForm/review-summary/clientlibs/.content.xml @@ -0,0 +1,6 @@ + + diff --git a/ui.apps/src/main/content/jcr_root/apps/yukon-forms/components/adaptiveForm/review-summary/clientlibs/css.txt b/ui.apps/src/main/content/jcr_root/apps/yukon-forms/components/adaptiveForm/review-summary/clientlibs/css.txt new file mode 100644 index 0000000..86358c3 --- /dev/null +++ b/ui.apps/src/main/content/jcr_root/apps/yukon-forms/components/adaptiveForm/review-summary/clientlibs/css.txt @@ -0,0 +1,3 @@ +#base=css + +review-summary.css diff --git a/ui.apps/src/main/content/jcr_root/apps/yukon-forms/components/adaptiveForm/review-summary/clientlibs/css/review-summary.css b/ui.apps/src/main/content/jcr_root/apps/yukon-forms/components/adaptiveForm/review-summary/clientlibs/css/review-summary.css new file mode 100644 index 0000000..7dad895 --- /dev/null +++ b/ui.apps/src/main/content/jcr_root/apps/yukon-forms/components/adaptiveForm/review-summary/clientlibs/css/review-summary.css @@ -0,0 +1,100 @@ +/* + * review-summary.css + * + * Styles for the flat-list Review Summary component. + */ + +/* ── Design tokens ── */ +.review-summary { + --rs-label-color: #6b7280; + --rs-value-color: #555; + --rs-panel-color: #1f2937; + --rs-divider-color: #e5e7eb; + --rs-font-size-sm: 0.875rem; + --rs-font-size-base: 1rem; + --rs-font-size-panel: 1rem; +} + +/* ── Loading state ── */ +.review-summary__loading { + color: var(--rs-label-color); + font-size: var(--rs-font-size-sm); + padding: 1rem 0; +} + +/* ── Empty state ── */ +.review-summary__empty { + color: var(--rs-label-color); + font-style: italic; +} + +/* ── Sections ── */ +.rs-panel-block + .rs-panel-block, +.rs-panel-block .rs-panel-block { + margin-top: 1rem; +} + +.rs-section-header { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 0.75rem; + border-bottom: 1px solid var(--rs-divider-color); + padding-bottom: 0.25rem; +} + +.rs-section-title { + margin: 0; + color: var(--rs-panel-color); + font-weight: 600; + font-size: var(--rs-font-size-panel); +} + +.rs-page-header { + font-size: 1.25rem; + padding: 10px 10px 10px 0; + font-weight: bold; + color: #000; + border-width: 0 0 4px; + border-style: solid; + border-color: #f1ab00; +} + +.rs-page-header .rs-section-title { + font-size: inherit; +} + +.rs-panel-block button.rs-page-goto-btn { + display: block; + margin-top: 0.75rem; + font-size: var(--rs-font-size-sm); + text-align: left; + cursor: pointer; + border-color: #00616d; + border-style: solid; + border-radius: 8px; + background: #ffffff; + color: #00616d; +} + +.rs-field { + display: block; + padding: 0.375rem 0; +} + +.rs-label { + font-size: var(--rs-font-size-base); + width: 100%; + color: black; +} + +.rs-value { + color: var(--rs-value-color); + font-size: var(--rs-font-size-sm); + word-break: break-word; + width: 100%; + margin-top: .25rem; + padding-right: .75rem; + padding-top: .5rem; + margin-bottom: .25rem; +} diff --git a/ui.apps/src/main/content/jcr_root/apps/yukon-forms/components/adaptiveForm/review-summary/clientlibs/js.txt b/ui.apps/src/main/content/jcr_root/apps/yukon-forms/components/adaptiveForm/review-summary/clientlibs/js.txt new file mode 100644 index 0000000..c5541b6 --- /dev/null +++ b/ui.apps/src/main/content/jcr_root/apps/yukon-forms/components/adaptiveForm/review-summary/clientlibs/js.txt @@ -0,0 +1,3 @@ +#base=js + +review-summary.js diff --git a/ui.apps/src/main/content/jcr_root/apps/yukon-forms/components/adaptiveForm/review-summary/clientlibs/js/review-summary.js b/ui.apps/src/main/content/jcr_root/apps/yukon-forms/components/adaptiveForm/review-summary/clientlibs/js/review-summary.js new file mode 100644 index 0000000..1cf27e1 --- /dev/null +++ b/ui.apps/src/main/content/jcr_root/apps/yukon-forms/components/adaptiveForm/review-summary/clientlibs/js/review-summary.js @@ -0,0 +1,249 @@ +(function (window, document, $) { + "use strict"; + + // ─── Constants ──────────────────────────────────────────────────────────── + + var BRIDGE_READY_EVENT = "bridgeInitializeStart"; + + var INPUT_CLASSES = [ + "guideTextBox", + "guideTermsAndConditions", + "guideTelephone", + "guideSwitch", + "guideRadioButton", + "guidePasswordBox", + "guideNumericBox", + "guideCheckBox", + "guideFileUpload", + "guideDropDownList", + "guideDatePicker" + ]; + + // ─── Model helpers ──────────────────────────────────────────────────────── + + function allParentsVisible(node) { + var parent = node.parent; + while (parent) { + if (parent.visible === false) return false; + parent = parent.parent; + } + return true; + } + + function getDisplayValue(field) { + var raw = field.value; + var display = field.displayValue; + if (raw === null || raw === undefined || raw === "") return null; + if (field.className === "guideCheckBox" && field.options && field.options.jsonModel && field.options.jsonModel.options) { + var value = null; + field.options.jsonModel.options.forEach(item => { + var nameValues = item.split('='); + if (nameValues.length === 2) { + if (raw === nameValues[0]) { + value = nameValues[1]; + } + } + }); + if (value) { + return value; + } + } + if (Array.isArray(raw) && raw.length === 0) return null; + if (Array.isArray(display)) return display.join(", ") || null; + if (display !== null && display !== undefined && display !== "") return String(display); + if (Array.isArray(raw)) return raw.join(", ") || null; + return String(raw); + } + + function maybeMask(field, value) { + if (field.jsonModel && field.jsonModel.sensitive === true) return "••••••••"; + return value; + } + + // ─── Flat list rendering ────────────────────────────────────────────────── + + function buildSection(container, excludedFields, showEditLinks, isTopLevelContainer) { + var section = document.createElement("div"); + section.className = "rs-section"; + + container.items.forEach(function (item) { + if (item.type === "panel") { + var panel = item.node.panel; + if (excludedFields.indexOf(panel.name) >= 0) return; + + var panelName = panel.name; + var panelSom = panel.somExpression || panelName; + var title = panel.title || panelName; + + var header = null; + if (panel.title) { + header = document.createElement("div"); + header.className = "rs-section-header" + (isTopLevelContainer ? " rs-page-header" : ""); + + var heading = document.createElement("h3"); + heading.className = "rs-section-title"; + heading.textContent = title; + header.appendChild(heading); + } + + var childSection = buildSection(item.node, excludedFields, showEditLinks, false); + + var panelBlock = document.createElement("div"); + panelBlock.className = "rs-panel-block"; + if (header) panelBlock.appendChild(header); + panelBlock.appendChild(childSection); + + if (isTopLevelContainer && showEditLinks) { + var rootItems = panel.parent && panel.parent.items; + var pageNumber = rootItems ? rootItems.indexOf(panel) : null; + + var goBackBtn = document.createElement("button"); + goBackBtn.type = "button"; + goBackBtn.className = "rs-page-goto-btn"; + goBackBtn.textContent = "Go back to page " + pageNumber + " to edit your " + title; + goBackBtn.dataset.rsPanel = panelName; + goBackBtn.dataset.rsPanelSom = panelSom; + panelBlock.appendChild(goBackBtn); + } + + section.appendChild(panelBlock); + } else { + var row = document.createElement("div"); + row.className = "rs-field"; + + var label = document.createElement("div"); + label.className = "rs-label"; + label.textContent = item.label; + + var value = document.createElement("div"); + value.className = "rs-value"; + value.textContent = item.value; + + row.appendChild(label); + row.appendChild(value); + section.appendChild(row); + } + }); + + return section; + } + + // ─── Core renderer ──────────────────────────────────────────────────────── + + function render(root, guideBridge) { + var excludedFields = []; + var showEditLinks = root.dataset.showEditLinks !== "false"; + + try { + excludedFields = JSON.parse(root.dataset.excludedFields || "[]"); + } catch (e) { /* ignore */ } + + // ── Build tree data structure ───────────────────────────────────────── + // children[] uses panel object identity as the key so repeatable panel + // instances (same name, different object) each get their own tree node. + var treeRoot = { items: [], children: [] }; + + function findOrCreateChild(container, panel) { + for (var i = 0; i < container.children.length; i++) { + if (container.children[i].panel === panel) return container.children[i].node; + } + var treeNode = { panel: panel, items: [], children: [] }; + container.children.push({ panel: panel, node: treeNode }); + container.items.push({ type: "panel", node: treeNode }); + return treeNode; + } + + guideBridge.visit(function (node) { + if (INPUT_CLASSES.indexOf(node.className) < 0) return; + if (!node.visible) return; + if (!allParentsVisible(node)) return; + if (!node.parent) return; + if (excludedFields.indexOf(node.name) >= 0) return; + + var value = getDisplayValue(node); + if (value === null) return; + value = maybeMask(node, value); + + // Panel chain from top-level panel down to node.parent, + // stopping before the guideContainer root (no parent of its own). + var chain = []; + var p = node.parent; + while (p) { + if (!p.parent) break; // p is guideContainer — stop + if (!p.parent.parent) break; // p is root panel — stop + chain.unshift(p); + p = p.parent; + } + + var current = treeRoot; + chain.forEach(function (panel) { + current = findOrCreateChild(current, panel); + }); + + current.items.push({ type: "field", label: node.title || node.name, value: value }); + }); + + // ── Render flat sections ────────────────────────────────────────────── + var $root = $(root); + $root.off(".rs"); + root.innerHTML = ""; + + if (!treeRoot.items.length) { + var empty = document.createElement("p"); + empty.className = "review-summary__empty"; + empty.textContent = "No responses to display."; + root.appendChild(empty); + return; + } + + root.appendChild(buildSection(treeRoot, excludedFields, showEditLinks, true)); + + if (showEditLinks) { + // Delegated handler on $root catches clicks regardless of DOM re-renders + $root.on("click.rs", ".rs-page-goto-btn", function (e) { + e.preventDefault(); + e.stopPropagation(); + var panelName = this.dataset.rsPanel; + var panelSom = this.dataset.rsPanelSom || panelName; + guideBridge.setFocus(panelSom); + setTimeout(function () { + var el = document.getElementById(panelName); + if (el) el.scrollIntoView({ behavior: "smooth", block: "start" }); + }, 300); + }); + } + } + + // ─── Initialisation ─────────────────────────────────────────────────────── + + function init() { + var roots = document.querySelectorAll(".review-summary[id='review-summary-root']"); + if (!roots.length) return; + + function attachBridge(guideBridge) { + guideBridge.connect(function () { + roots.forEach(function (root) { + guideBridge.on("elementFocusChanged", function () { + if (root.offsetParent !== null) render(root, guideBridge); + }); + if (root.offsetParent !== null) render(root, guideBridge); + }); + }); + } + + if (window.guideBridge && window.guideBridge.isConnected()) { + attachBridge(window.guideBridge); + } else { + window.addEventListener(BRIDGE_READY_EVENT, function (e) { + attachBridge(e.detail.guideBridge || window.guideBridge); + }); + } + } + + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", init); + } else { + init(); + } + +}(window, document, window.jQuery)); diff --git a/ui.apps/src/main/content/jcr_root/apps/yukon-forms/components/adaptiveForm/review-summary/review-summary.html b/ui.apps/src/main/content/jcr_root/apps/yukon-forms/components/adaptiveForm/review-summary/review-summary.html new file mode 100644 index 0000000..02e243f --- /dev/null +++ b/ui.apps/src/main/content/jcr_root/apps/yukon-forms/components/adaptiveForm/review-summary/review-summary.html @@ -0,0 +1,25 @@ + +
+ + +
+ Loading your responses… +
+