From 96d9d9cfcd2a68964a4d50a4cd61b0baaadcec31 Mon Sep 17 00:00:00 2001 From: joshua_cm Date: Tue, 7 Apr 2026 23:43:16 +0700 Subject: [PATCH 1/6] feat(dataview): migrate dataview to plugin --- AppBuilder/ABFactory.js | 4 +- AppBuilder/core | 2 +- AppBuilder/platform/ABClassManager.js | 17 +- AppBuilder/platform/plugins/included/index.js | 2 + .../ABViewPropertyLinkPageLocal.js | 42 ++ .../view_dataview/FNAbviewdataview.js | 158 ++++++ .../FNAbviewdataviewComponent.js | 452 ++++++++++++++++++ 7 files changed, 668 insertions(+), 9 deletions(-) create mode 100644 AppBuilder/platform/plugins/included/view_dataview/ABViewPropertyLinkPageLocal.js create mode 100644 AppBuilder/platform/plugins/included/view_dataview/FNAbviewdataview.js create mode 100644 AppBuilder/platform/plugins/included/view_dataview/FNAbviewdataviewComponent.js diff --git a/AppBuilder/ABFactory.js b/AppBuilder/ABFactory.js index 72349e2f..300c7baf 100644 --- a/AppBuilder/ABFactory.js +++ b/AppBuilder/ABFactory.js @@ -32,7 +32,9 @@ import Network from "../resources/Network.js"; import Storage from "../resources/Storage.js"; // Storage: manages our interface for local storage -import ABViewManager from "./core/ABViewManagerCore"; +// Use platform ABViewManager so Class.ABViewManager.viewClass() resolves +// plugin-registered views (e.g. dataview) via ClassManager, not only core AllViews. +import ABViewManager from "./platform/ABViewManager.js"; import Tenant from "../resources/Tenant.js"; // Tenant: manages the Tenant information of the current instance diff --git a/AppBuilder/core b/AppBuilder/core index a7b2f392..6d31c41e 160000 --- a/AppBuilder/core +++ b/AppBuilder/core @@ -1 +1 @@ -Subproject commit a7b2f392277e743ea89d4494094533d290d34729 +Subproject commit 6d31c41ed52fd74a7b67785f3246b814f1fe8952 diff --git a/AppBuilder/platform/ABClassManager.js b/AppBuilder/platform/ABClassManager.js index c9fa8e0a..dbe82ec2 100644 --- a/AppBuilder/platform/ABClassManager.js +++ b/AppBuilder/platform/ABClassManager.js @@ -19,7 +19,9 @@ import ABViewRuleListFormRecordRules from "../rules/ABViewRuleListFormRecordRule import ABViewRuleListFormSubmitRules from "../rules/ABViewRuleListFormSubmitRules"; // MIGRATION: ABViewManager is depreciated. Use ABClassManager instead. -import ABViewManager from "./ABViewManager.js"; +// Resolve legacy-only views via core AllViews hash (not platform ABViewManager, +// which would recurse back into ClassManager.viewClass). +import ABViewManagerCore from "../core/ABViewManagerCore.js"; const classRegistry = { ObjectTypes: new Map(), @@ -108,13 +110,14 @@ export function allObjectProperties() { export function viewClass(type) { var ViewClass = classRegistry.ViewTypes.get(type); - if (!ViewClass) { - ViewClass = ABViewManager.viewClass(type, false); - if (!ViewClass) { - throw new Error(`Unknown View type: ${type}`); - } + if (ViewClass) { + return ViewClass; } - return ViewClass; + ViewClass = ABViewManagerCore.viewClass(type); + if (ViewClass) { + return ViewClass; + } + throw new Error(`Unknown View type: ${type}`); } export function viewCreate(type, config, application, parent) { diff --git a/AppBuilder/platform/plugins/included/index.js b/AppBuilder/platform/plugins/included/index.js index 2ad265f0..e8a76e63 100644 --- a/AppBuilder/platform/plugins/included/index.js +++ b/AppBuilder/platform/plugins/included/index.js @@ -3,6 +3,7 @@ import viewComment from "./view_comment/FNAbviewcomment.js"; import viewCsvExporter from "./view_csvExporter/FNAbviewcsvexporter.js"; import viewCsvImporter from "./view_csvImporter/FNAbviewcsvimporter.js"; import viewDataSelect from "./view_data-select/FNAbviewdataselect.js"; +import viewDataview from "./view_dataview/FNAbviewdataview.js"; import viewDetail from "./view_detail/FNAbviewdetail.js"; import viewImage from "./view_image/FNAbviewimage.js"; import viewLabel from "./view_label/FNAbviewlabel.js"; @@ -19,6 +20,7 @@ const AllPlugins = [ viewCsvImporter, viewCsvImporter, viewDataSelect, + viewDataview, viewDetail, viewImage, viewLabel, diff --git a/AppBuilder/platform/plugins/included/view_dataview/ABViewPropertyLinkPageLocal.js b/AppBuilder/platform/plugins/included/view_dataview/ABViewPropertyLinkPageLocal.js new file mode 100644 index 00000000..2a30671e --- /dev/null +++ b/AppBuilder/platform/plugins/included/view_dataview/ABViewPropertyLinkPageLocal.js @@ -0,0 +1,42 @@ +// Local copy of link-page helper logic so the plugin remains self-contained. +class ABViewPropertyLinkPageComponentLocal { + constructor() { + this.view = null; + this.datacollection = null; + } + + ui() { + return {}; + } + + init(options = {}) { + if (options.view) this.view = options.view; + if (options.datacollection) this.datacollection = options.datacollection; + } + + changePage(pageId, rowId) { + if (this.datacollection) { + this.datacollection.once("changeCursor", () => { + this.view?.changePage(pageId); + }); + this.datacollection.setCursor(rowId); + } else { + this.view?.changePage(pageId); + } + } +} + +export default class ABViewPropertyLinkPageLocal { + component(v1App = false) { + const component = new ABViewPropertyLinkPageComponentLocal(); + + if (!v1App) return component; + + return { + ui: component.ui(), + init: (...params) => component.init(...params), + onShow: (...params) => component.onShow?.(...params), + changePage: (...params) => component.changePage(...params), + }; + } +} diff --git a/AppBuilder/platform/plugins/included/view_dataview/FNAbviewdataview.js b/AppBuilder/platform/plugins/included/view_dataview/FNAbviewdataview.js new file mode 100644 index 00000000..ffe65d05 --- /dev/null +++ b/AppBuilder/platform/plugins/included/view_dataview/FNAbviewdataview.js @@ -0,0 +1,158 @@ +import FNAbviewdataviewComponent from "./FNAbviewdataviewComponent.js"; + +// Dataview plugin: replaces ABViewDataviewCore + ABViewDataview. +// All runtime logic is kept in this plugin module. +export default function FNAbviewdataview({ + ABViewContainer, + ABViewContainerComponent, + ABViewComponentPlugin, +}) { + const ABAbviewdataviewComponent = FNAbviewdataviewComponent({ + ABViewContainerComponent, + ABViewComponentPlugin, + }); + + const ABViewDataviewPropertyComponentDefaults = { + xCount: 1, // Number of columns per row (must be >= 1). + detailsPage: "", + detailsTab: "", + editPage: "", + editTab: "", + dataviewID: null, + showLabel: true, + labelPosition: "left", + labelWidth: 120, + height: 0, + }; + + const ABViewDataviewDefaults = { + key: "dataview", + icon: "th", + labelKey: "Data view(plugin)", + }; + + return class ABViewDataviewPlugin extends ABViewContainer { + constructor(values, application, parent, defaultValues) { + super( + values, + application, + parent, + defaultValues ?? ABViewDataviewDefaults + ); + } + + static getPluginType() { + return "view"; + } + + static getPluginKey() { + return this.common().key; + } + + static common() { + return ABViewDataviewDefaults; + } + + static defaultValues() { + return ABViewDataviewPropertyComponentDefaults; + } + + fromValues(values) { + super.fromValues(values); + + this.settings.xCount = parseInt( + this.settings.xCount || ABViewDataviewPropertyComponentDefaults.xCount + ); + + this.settings.detailsPage = + this.settings.detailsPage ?? + ABViewDataviewPropertyComponentDefaults.detailsPage; + this.settings.editPage = + this.settings.editPage ?? + ABViewDataviewPropertyComponentDefaults.editPage; + this.settings.detailsTab = + this.settings.detailsTab ?? + ABViewDataviewPropertyComponentDefaults.detailsTab; + this.settings.editTab = + this.settings.editTab ?? ABViewDataviewPropertyComponentDefaults.editTab; + + this.settings.labelPosition = + this.settings.labelPosition || + ABViewDataviewPropertyComponentDefaults.labelPosition; + this.settings.showLabel = JSON.parse( + this.settings.showLabel != null + ? this.settings.showLabel + : ABViewDataviewPropertyComponentDefaults.showLabel + ); + this.settings.labelWidth = parseInt( + this.settings.labelWidth || + ABViewDataviewPropertyComponentDefaults.labelWidth + ); + this.settings.height = parseInt( + this.settings.height ?? + ABViewDataviewPropertyComponentDefaults.height + ); + } + + component(parentId) { + return new ABAbviewdataviewComponent(this, parentId); + } + + // Dataview behaves like Detail and allows detail field views. + componentList() { + const viewsToAllow = ["label", "text"]; + const allComponents = this.application.viewAll(); + return allComponents.filter((c) => + viewsToAllow.includes(c.common().key) + ); + } + + addFieldToDetail(field, yPosition) { + if (field == null) return; + + const newView = field.detailComponent().newInstance(this.application, this); + if (newView == null) return; + + // Keep the same field wiring behavior as Detail so ABDesigner can + // auto-build field cards after a Data Source is selected. + newView.settings = newView.settings ?? {}; + newView.settings.fieldId = field.id; + newView.settings.labelWidth = + this.settings.labelWidth || + ABViewDataviewPropertyComponentDefaults.labelWidth; + + // Preserve alias support for query-based sources: [alias].[columnName]. + newView.settings.alias = field.alias; + newView.position.y = yPosition; + + this._views.push(newView); + return newView; + } + + parentDetailComponent() { + let dataview = null; + let curr = this; + + while (curr.key != "dataview" && !curr.isRoot() && curr.parent) { + curr = curr.parent; + } + + if (curr.key == "dataview") { + dataview = curr; + } + + return dataview; + } + + warningsEval() { + super.warningsEval(); + + const DC = this.datacollection; + if (!DC) { + this.warningsMessage( + `can't resolve it's datacollection[${this.settings.dataviewID}]` + ); + } + } + }; +} diff --git a/AppBuilder/platform/plugins/included/view_dataview/FNAbviewdataviewComponent.js b/AppBuilder/platform/plugins/included/view_dataview/FNAbviewdataviewComponent.js new file mode 100644 index 00000000..c8fa84f5 --- /dev/null +++ b/AppBuilder/platform/plugins/included/view_dataview/FNAbviewdataviewComponent.js @@ -0,0 +1,452 @@ +import ABViewPropertyLinkPageLocal from "./ABViewPropertyLinkPageLocal.js"; + +function FNAbviewdataviewDetailComponent({ + ABViewContainerComponent, + ABViewComponentPlugin, +}) { + const ContainerComponent = + ABViewContainerComponent?.default ?? ABViewContainerComponent; + const Base = ContainerComponent ?? ABViewComponentPlugin; + + return class ABAbviewdataviewDetailComponent extends Base { + constructor(baseView, idBase, ids) { + super( + baseView, + idBase || `ABViewDetail_${baseView.id}`, + Object.assign({ detail: "" }, ids) + ); + this.idBase = idBase || `ABViewDetail_${baseView.id}`; + } + + displayData(rowData = {}) { + const views = (this.view.views() || []).sort((a, b) => { + if (!a?.field?.() || !b?.field?.()) return 0; + if (a.field().key === "formula" && b.field().key === "calculate") + return -1; + if (a.field().key === "calculate" && b.field().key === "formula") + return 1; + return 0; + }); + + views.forEach((f) => { + let val; + + if (f.field) { + const field = f.field(); + if (!field) return; + + switch (field.key) { + case "connectObject": + val = field.pullRelationValues(rowData); + break; + case "list": + val = rowData?.[field.columnName]; + if (!val || (Array.isArray(val) && val.length === 0)) { + val = ""; + break; + } + if (field.settings.isMultiple === 0) { + let myVal = ""; + (field.settings.options || []).forEach((opt) => { + if (opt.id === val) myVal = opt.text; + }); + if (field.settings.hasColors) { + let hasCustomColor = ""; + (field.settings.options || []).forEach((h) => { + if (h.text === myVal) hasCustomColor = "hascustomcolor"; + }); + const hex = (field.settings.options || []).find( + (o) => o.text === myVal + )?.hex; + myVal = `${myVal}`; + } + val = myVal; + } else { + const items = val.map((value) => { + let myVal = ""; + (field.settings.options || []).forEach((opt) => { + if (opt.id === value.id) myVal = opt.text; + }); + const optionHex = + field.settings.hasColors && value.hex + ? `background: ${value.hex};` + : ""; + const hasCustomColor = + field.settings.hasColors && value.hex + ? "hascustomcolor" + : ""; + return `${myVal}`; + }); + val = items.join(""); + } + break; + case "user": + val = field.pullRelationValues(rowData); + break; + case "file": + val = rowData?.[field.columnName] ?? ""; + break; + case "formula": + val = rowData ? field.format(rowData, false) : ""; + break; + default: + val = field.format(rowData); + } + } + + const vComponent = + this.viewComponents?.[f.id] ?? f.component(this.idBase); + vComponent?.setValue?.(val); + vComponent?.displayText?.(rowData); + }); + } + }; +} + +export default function FNAbviewdataviewComponent({ + ABViewContainerComponent, + ABViewComponentPlugin, +}) { + const DetailComponent = FNAbviewdataviewDetailComponent({ + ABViewContainerComponent, + ABViewComponentPlugin, + }); + + return class ABAbviewdataviewComponent extends ABViewComponentPlugin { + constructor(baseView, idBase, ids) { + super( + baseView, + idBase || `ABViewDataview_${baseView.id}`, + Object.assign( + { + dataview: "", + reload: "", + }, + ids + ) + ); + + this.linkPage = null; + } + + ui() { + // Initialize detail component here because item template depends + // on rendered width/height values from its generated card UI. + this.initDetailComponent(); + + const ids = this.ids; + const L = (...params) => (this.AB ?? AB).Multilingual.label(...params); + return super.ui([ + { + view: "layout", + rows: [ + { + id: ids.reload, + view: "button", + value: L("New data available. Click to reload."), + css: "webix_primary webix_warn", + hidden: true, + click: () => { + this.reloadData(); + }, + }, + { + id: ids.dataview, + view: "dataview", + scroll: "y", + sizeToContent: true, + css: "borderless transparent", + xCount: this.settings.xCount != 1 ? this.settings.xCount : 0, + height: this.settings.height, + template: (item) => this.itemTemplate(item), + on: { + onAfterRender: () => { + this.applyClickEvent(); + this.addCyAttribute(); + }, + }, + }, + ], + }, + ]); + } + + async init(AB) { + await super.init(AB); + + const dc = this.datacollection; + if (!dc) return; + + this.linkPage = this.linkPageHelper.component(); + this.linkPage.init({ + view: this.view, + datacollection: dc, + }); + + const $dataView = $$(this.ids.dataview); + AB.Webix.extend($dataView, AB.Webix.ProgressBar); + dc.bind($dataView); + + this.initRefreshWarning(); + + window.addEventListener("resize", () => { + clearTimeout(this._resizeEvent); + this._resizeEvent = setTimeout(() => { + this.resize($dataView.getParentView()); + delete this._resizeEvent; + }, 20); + }); + } + + initRefreshWarning() { + const dc = this.datacollection; + const includeInQuery = + (dc?.settings?.objectWorkspace?.filterConditions?.rules ?? []).filter( + (r) => + [ + "in_query", + "not_in_query", + "in_query_field", + "not_in_query_field", + ].includes(r.rule) + ).length > 0; + + if (!includeInQuery) return; + + ["ab.datacollection.create", "ab.datacollection.update", "ab.datacollection.delete"].forEach( + (eventKey) => { + dc.on(eventKey, (data) => { + if (data.objectId == dc.datasource.id) this.showRefreshWarning(); + }); + } + ); + } + + showRefreshWarning() { + if (this.__throttleRefreshWarning) + clearTimeout(this.__throttleRefreshWarning); + this.__throttleRefreshWarning = setTimeout(() => { + $$(this.ids.reload)?.show(); + }, 200); + } + + reloadData() { + this.datacollection?.reloadData(); + $$(this.ids.reload)?.hide(); + } + + onShow() { + super.onShow(); + this.resize(); + } + + resize(baseElement) { + const $dataview = $$(this.ids.dataview); + if (!$dataview) { + this.AB.notify.developer( + new Error("Resize called on missing dataview component"), + { context: "ABViewDataviewComponent.resize()", ids: this.ids } + ); + return; + } + + $dataview.resize(); + const itemWidth = this.getItemWidth(baseElement); + $dataview.customize({ width: itemWidth }); + $dataview.getTopParentView?.().resize?.(); + } + + initDetailComponent() { + this._detail_ui = this.AB.Webix.ui(this.getDetailUI()); + this.detailComponent.init(null, 2); + } + + getDetailUI() { + const detailCom = this.detailComponent; + const _ui = detailCom.ui(); + _ui.type = "clean"; + _ui.css = "ab-detail-view"; + + if (this.settings.detailsPage || this.settings.editPage) { + _ui.css += " ab-detail-hover ab-record-#itemId#"; + if (this.settings.detailsPage) _ui.css += " ab-detail-page"; + if (this.settings.editPage) _ui.css += " ab-edit-page"; + } + + return _ui; + } + + itemTemplate(item) { + const detailCom = this.detailComponent; + const $dataview = $$(this.ids.dataview); + const $detailItem = this._detail_ui; + + // Mock data ensures card template has dimensions before data exists. + if (!item || !Object.keys(item).length) { + item = item ?? {}; + this.datacollection?.datasource?.fields().forEach((f) => { + switch (f.key) { + case "string": + case "LongText": + item[f.columnName] = "Lorem Ipsum"; + break; + case "date": + case "datetime": + item[f.columnName] = new Date(); + break; + case "number": + item[f.columnName] = 7; + break; + default: + break; + } + }); + } + + detailCom.displayData(item); + + const itemWidth = + $dataview.data.count() > 0 + ? $dataview.type.width + : ($detailItem.$width - 20) / this.settings.xCount; + const itemHeight = + $dataview.data.count() > 0 + ? $dataview.type.height + : $detailItem.getChildViews?.()?.[0]?.$height; + + const tmpDom = document.createElement("div"); + tmpDom.appendChild($detailItem.$view); + + $detailItem.define("width", itemWidth - 24); + $detailItem.define("height", itemHeight + 15); + $detailItem.adjust(); + + this.addCyItemAttributes(tmpDom, item); + return tmpDom.innerHTML.replace(/#itemId#/g, item.id); + } + + getItemWidth(baseElement) { + const $dataview = $$(this.ids.dataview); + let currElem = baseElement ?? $dataview; + let parentWidth = currElem?.$width; + + while (currElem) { + if (currElem.config.view == "scrollview" || currElem.config.view == "layout") + parentWidth = + currElem?.$width < parentWidth ? currElem?.$width : parentWidth; + currElem = currElem?.getParentView?.(); + } + + if (!parentWidth) { + parentWidth = $dataview?.getParentView?.().$width || window.innerWidth; + } + if (parentWidth > window.innerWidth) parentWidth = window.innerWidth; + + // Browser chrome can reduce available width; subtract sidebar when needed. + if (window.innerWidth - 19 <= parentWidth) { + const $sidebar = this.getTabSidebar(); + if ($sidebar) parentWidth -= $sidebar.$width; + } + + return Math.floor(parentWidth / this.settings.xCount); + } + + getTabSidebar() { + const $dataview = $$(this.ids.dataview); + let $sidebar; + let currElem = $dataview; + while (currElem && !$sidebar) { + $sidebar = (currElem.getChildViews?.() ?? []).filter( + (item) => item?.config?.view == "sidebar" + )[0]; + currElem = currElem?.getParentView?.(); + } + return $sidebar; + } + + applyClickEvent() { + const editPage = this.settings.editPage; + const detailsPage = this.settings.detailsPage; + if (!detailsPage && !editPage) return; + + const $dataview = $$(this.ids.dataview); + if (!$dataview) return; + + $dataview.$view.onclick = (e) => { + let clicked = false; + let divs = e.path ?? []; + + // Some browsers do not support Event.path. + if (!divs.length) { + divs.push(e.target); + divs.push(e.target.parentNode); + } + + if (editPage) { + for (const p of divs) { + if (p.className && p.className.indexOf("webix_accordionitem_header") > -1) { + clicked = true; + p.parentNode.parentNode.classList.forEach((c) => { + if (c.indexOf("ab-record-") > -1) { + this.linkPage.changePage(editPage, c.replace("ab-record-", "")); + } + }); + break; + } + } + } + + if (detailsPage && !clicked) { + for (const p of divs) { + if (p.className && p.className.indexOf("webix_accordionitem") > -1) { + p.parentNode.parentNode.classList.forEach((c) => { + if (c.indexOf("ab-record-") > -1) { + this.linkPage.changePage( + detailsPage, + c.replace("ab-record-", "") + ); + } + }); + break; + } + } + } + }; + } + + addCyAttribute() { + const baseView = this.view; + const $dataview = $$(this.ids.dataview); + const name = (baseView.name ?? "").replace(".dataview", ""); + $dataview.$view.setAttribute( + "data-cy", + `dataview container ${name} ${baseView.id}` + ); + } + + addCyItemAttributes(dom, item) { + const baseView = this.view; + const uuid = item.uuid; + const name = (baseView.name ?? "").replace(".dataview", ""); + dom.querySelector(".webix_accordionitem_body")?.setAttribute( + "data-cy", + `dataview item ${name} ${uuid} ${baseView.id}` + ); + dom.querySelector(".webix_accordionitem_button")?.setAttribute( + "data-cy", + `dataview item button ${name} ${uuid} ${baseView.id}` + ); + } + + get detailComponent() { + return (this._detailComponent = + this._detailComponent ?? + new DetailComponent(this.view, `${this.ids.component}_detail_view`)); + } + + get linkPageHelper() { + return (this.__linkPageHelper = + this.__linkPageHelper || new ABViewPropertyLinkPageLocal()); + } + }; +} From 3f469923f2ea0cbb8d390287fef5ca1d84778aae Mon Sep 17 00:00:00 2001 From: joshua_cm Date: Thu, 9 Apr 2026 20:14:59 +0700 Subject: [PATCH 2/6] fix(dataview): fix E2E timing and data-cy issues --- .../ABViewPropertyLinkPageLocal.js | 16 ++++++- .../FNAbviewdataviewComponent.js | 43 ++++++++++++++++--- 2 files changed, 50 insertions(+), 9 deletions(-) diff --git a/AppBuilder/platform/plugins/included/view_dataview/ABViewPropertyLinkPageLocal.js b/AppBuilder/platform/plugins/included/view_dataview/ABViewPropertyLinkPageLocal.js index 2a30671e..7e651f39 100644 --- a/AppBuilder/platform/plugins/included/view_dataview/ABViewPropertyLinkPageLocal.js +++ b/AppBuilder/platform/plugins/included/view_dataview/ABViewPropertyLinkPageLocal.js @@ -16,10 +16,22 @@ class ABViewPropertyLinkPageComponentLocal { changePage(pageId, rowId) { if (this.datacollection) { - this.datacollection.once("changeCursor", () => { + const dc = this.datacollection; + const cur = dc.getCursor(); + const same = + cur && + (String(cur.id) === String(rowId) || + String(cur.uuid) === String(rowId)); + // If cursor is already on this row, changeCursor may not fire; navigate immediately + // so Cypress and slow CI do not hang waiting for a one-time listener. + if (same) { + this.view?.changePage(pageId); + return; + } + dc.once("changeCursor", () => { this.view?.changePage(pageId); }); - this.datacollection.setCursor(rowId); + dc.setCursor(rowId); } else { this.view?.changePage(pageId); } diff --git a/AppBuilder/platform/plugins/included/view_dataview/FNAbviewdataviewComponent.js b/AppBuilder/platform/plugins/included/view_dataview/FNAbviewdataviewComponent.js index c8fa84f5..20458e5b 100644 --- a/AppBuilder/platform/plugins/included/view_dataview/FNAbviewdataviewComponent.js +++ b/AppBuilder/platform/plugins/included/view_dataview/FNAbviewdataviewComponent.js @@ -160,6 +160,12 @@ export default function FNAbviewdataviewComponent({ height: this.settings.height, template: (item) => this.itemTemplate(item), on: { + // Tab/multiview can show the dataview after init; re-apply cy + handlers. + onViewShow: () => { + this.addCyAttribute(); + this.applyClickEvent(); + this.resize(); + }, onAfterRender: () => { this.applyClickEvent(); this.addCyAttribute(); @@ -174,7 +180,24 @@ export default function FNAbviewdataviewComponent({ async init(AB) { await super.init(AB); + const $dataView = $$(this.ids.dataview); + // data-cy on the container must not wait for DC bind or onAfterRender; + // slow CI can otherwise race Cypress before the first dataview paint. + if ($dataView) { + this.addCyAttribute(); + AB.Webix.extend($dataView, AB.Webix.ProgressBar); + } + const dc = this.datacollection; + if (!$dataView) return; + + // Card field components need AB + full async init before the first bind-driven + // template pass (init(null, 2) from ui() is too early). Single init here, after AB exists. + const detailInit = this.detailComponent.init(AB, 2); + if (detailInit && typeof detailInit.then === "function") { + await detailInit; + } + if (!dc) return; this.linkPage = this.linkPageHelper.component(); @@ -183,8 +206,6 @@ export default function FNAbviewdataviewComponent({ datacollection: dc, }); - const $dataView = $$(this.ids.dataview); - AB.Webix.extend($dataView, AB.Webix.ProgressBar); dc.bind($dataView); this.initRefreshWarning(); @@ -192,7 +213,8 @@ export default function FNAbviewdataviewComponent({ window.addEventListener("resize", () => { clearTimeout(this._resizeEvent); this._resizeEvent = setTimeout(() => { - this.resize($dataView.getParentView()); + const $dv = $$(this.ids.dataview); + if ($dv) this.resize($dv.getParentView()); delete this._resizeEvent; }, 20); }); @@ -257,8 +279,9 @@ export default function FNAbviewdataviewComponent({ } initDetailComponent() { + // Build the detached card UI only; field inits run in init(AB) with a real AB + // and complete before datacollection.bind so itemTemplate sees ready sub-widgets. this._detail_ui = this.AB.Webix.ui(this.getDetailUI()); - this.detailComponent.init(null, 2); } getDetailUI() { @@ -283,7 +306,11 @@ export default function FNAbviewdataviewComponent({ // Mock data ensures card template has dimensions before data exists. if (!item || !Object.keys(item).length) { - item = item ?? {}; + item = { + id: "__ab_dataview_sizing__", + uuid: "__ab_dataview_sizing__", + ...(item ?? {}), + }; this.datacollection?.datasource?.fields().forEach((f) => { switch (f.key) { case "string": @@ -322,7 +349,8 @@ export default function FNAbviewdataviewComponent({ $detailItem.adjust(); this.addCyItemAttributes(tmpDom, item); - return tmpDom.innerHTML.replace(/#itemId#/g, item.id); + const rowId = item?.id ?? item?.uuid ?? ""; + return tmpDom.innerHTML.replace(/#itemId#/g, rowId); } getItemWidth(baseElement) { @@ -417,6 +445,7 @@ export default function FNAbviewdataviewComponent({ addCyAttribute() { const baseView = this.view; const $dataview = $$(this.ids.dataview); + if (!$dataview?.$view) return; const name = (baseView.name ?? "").replace(".dataview", ""); $dataview.$view.setAttribute( "data-cy", @@ -426,7 +455,7 @@ export default function FNAbviewdataviewComponent({ addCyItemAttributes(dom, item) { const baseView = this.view; - const uuid = item.uuid; + const uuid = item?.uuid ?? item?.id ?? ""; const name = (baseView.name ?? "").replace(".dataview", ""); dom.querySelector(".webix_accordionitem_body")?.setAttribute( "data-cy", From 74741be4006918927231939deac0c2b7729e166c Mon Sep 17 00:00:00 2001 From: joshua_cm Date: Thu, 9 Apr 2026 22:10:15 +0700 Subject: [PATCH 3/6] fix(tab): await init before onShow to stabilize E2E --- .../included/view_tab/FNAbviewtabComponent.js | 27 ++++++++++++------- .../views/viewComponent/ABViewTabComponent.js | 27 ++++++++++++------- 2 files changed, 34 insertions(+), 20 deletions(-) diff --git a/AppBuilder/platform/plugins/included/view_tab/FNAbviewtabComponent.js b/AppBuilder/platform/plugins/included/view_tab/FNAbviewtabComponent.js index 9016c5f9..6fab6b85 100644 --- a/AppBuilder/platform/plugins/included/view_tab/FNAbviewtabComponent.js +++ b/AppBuilder/platform/plugins/included/view_tab/FNAbviewtabComponent.js @@ -202,7 +202,7 @@ export default function FNAbviewtabComponent({ }), on: { onViewChange: (prevId, nextId) => { - this.onShow(nextId); + void this.onShow(nextId); }, }, }; @@ -344,7 +344,7 @@ export default function FNAbviewtabComponent({ multiview: { on: { onViewChange: (prevId, nextId) => { - this.onShow(nextId); + void this.onShow(nextId); }, }, }, @@ -478,7 +478,7 @@ export default function FNAbviewtabComponent({ } } - onShow(viewId) { + async onShow(viewId) { const ids = this.ids; let defaultViewIsSet = false; @@ -491,8 +491,9 @@ export default function FNAbviewtabComponent({ const baseView = this.view; const viewComponents = this.viewComponents; + const settings = this.settings; - viewComponents.forEach((vc) => { + for (const vc of viewComponents) { // set default view id const currView = baseView.views((view) => { return view.id === vc.view.id; @@ -511,7 +512,6 @@ export default function FNAbviewtabComponent({ // create view's component once const $tab = $$(ids.tab); - const settings = this.settings; if (!vc?.component && vc?.view?.id === viewId) { // show loading cursor @@ -552,7 +552,10 @@ export default function FNAbviewtabComponent({ // for tabs we need to look at the view's accessLevels accessLevel = vc.view.getUserAccess(); - vc.component.init(ab, accessLevel); + const initP = vc.component.init(ab, accessLevel); + if (initP && typeof initP.then === "function") { + await initP; + } // done setTimeout(() => { @@ -568,15 +571,19 @@ export default function FNAbviewtabComponent({ }, 10); } - // show UI - if (vc?.view?.id === viewId && vc?.component?.onShow) - vc.component.onShow(); + // show UI only after init has completed (above) for newly created components + if (vc?.view?.id === viewId && vc?.component?.onShow) { + const showP = vc.component.onShow(); + if (showP && typeof showP.then === "function") { + await showP; + } + } if (settings.stackTabs && vc?.view?.id === viewId) { $$(viewId)?.show(false, false); $sidebar?.select(`${viewId}_menu`); } - }); + } } }; } diff --git a/AppBuilder/platform/views/viewComponent/ABViewTabComponent.js b/AppBuilder/platform/views/viewComponent/ABViewTabComponent.js index 5d7d6f2e..fb73d470 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewTabComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewTabComponent.js @@ -198,7 +198,7 @@ module.exports = class ABViewTabComponent extends ABViewComponent { }), on: { onViewChange: (prevId, nextId) => { - this.onShow(nextId); + void this.onShow(nextId); }, }, }; @@ -340,7 +340,7 @@ module.exports = class ABViewTabComponent extends ABViewComponent { multiview: { on: { onViewChange: (prevId, nextId) => { - this.onShow(nextId); + void this.onShow(nextId); }, }, }, @@ -474,7 +474,7 @@ module.exports = class ABViewTabComponent extends ABViewComponent { } } - onShow(viewId) { + async onShow(viewId) { const ids = this.ids; let defaultViewIsSet = false; @@ -487,8 +487,9 @@ module.exports = class ABViewTabComponent extends ABViewComponent { const baseView = this.view; const viewComponents = this.viewComponents; + const settings = this.settings; - viewComponents.forEach((vc) => { + for (const vc of viewComponents) { // set default view id const currView = baseView.views((view) => { return view.id === vc.view.id; @@ -507,7 +508,6 @@ module.exports = class ABViewTabComponent extends ABViewComponent { // create view's component once const $tab = $$(ids.tab); - const settings = this.settings; if (!vc?.component && vc?.view?.id === viewId) { // show loading cursor @@ -548,7 +548,10 @@ module.exports = class ABViewTabComponent extends ABViewComponent { // for tabs we need to look at the view's accessLevels accessLevel = vc.view.getUserAccess(); - vc.component.init(ab, accessLevel); + const initP = vc.component.init(ab, accessLevel); + if (initP && typeof initP.then === "function") { + await initP; + } // done setTimeout(() => { @@ -564,14 +567,18 @@ module.exports = class ABViewTabComponent extends ABViewComponent { }, 10); } - // show UI - if (vc?.view?.id === viewId && vc?.component?.onShow) - vc.component.onShow(); + // show UI only after init has completed (above) for newly created components + if (vc?.view?.id === viewId && vc?.component?.onShow) { + const showP = vc.component.onShow(); + if (showP && typeof showP.then === "function") { + await showP; + } + } if (settings.stackTabs && vc?.view?.id === viewId) { $$(viewId)?.show(false, false); $sidebar?.select(`${viewId}_menu`); } - }); + } } }; From 41e9596d58e9df8da58a9579f5851753accf55ee Mon Sep 17 00:00:00 2001 From: joshua_cm Date: Sun, 12 Apr 2026 23:11:25 +0700 Subject: [PATCH 4/6] fix(dataview): align detail component with original implementation without container inheritance --- .../view_dataview/FNAbviewdataview.js | 2 - .../FNAbviewdataviewComponent.js | 254 +++++++++++++++++- 2 files changed, 240 insertions(+), 16 deletions(-) diff --git a/AppBuilder/platform/plugins/included/view_dataview/FNAbviewdataview.js b/AppBuilder/platform/plugins/included/view_dataview/FNAbviewdataview.js index ffe65d05..c5d1d52c 100644 --- a/AppBuilder/platform/plugins/included/view_dataview/FNAbviewdataview.js +++ b/AppBuilder/platform/plugins/included/view_dataview/FNAbviewdataview.js @@ -4,11 +4,9 @@ import FNAbviewdataviewComponent from "./FNAbviewdataviewComponent.js"; // All runtime logic is kept in this plugin module. export default function FNAbviewdataview({ ABViewContainer, - ABViewContainerComponent, ABViewComponentPlugin, }) { const ABAbviewdataviewComponent = FNAbviewdataviewComponent({ - ABViewContainerComponent, ABViewComponentPlugin, }); diff --git a/AppBuilder/platform/plugins/included/view_dataview/FNAbviewdataviewComponent.js b/AppBuilder/platform/plugins/included/view_dataview/FNAbviewdataviewComponent.js index 20458e5b..839a75ba 100644 --- a/AppBuilder/platform/plugins/included/view_dataview/FNAbviewdataviewComponent.js +++ b/AppBuilder/platform/plugins/included/view_dataview/FNAbviewdataviewComponent.js @@ -1,14 +1,7 @@ import ABViewPropertyLinkPageLocal from "./ABViewPropertyLinkPageLocal.js"; -function FNAbviewdataviewDetailComponent({ - ABViewContainerComponent, - ABViewComponentPlugin, -}) { - const ContainerComponent = - ABViewContainerComponent?.default ?? ABViewContainerComponent; - const Base = ContainerComponent ?? ABViewComponentPlugin; - - return class ABAbviewdataviewDetailComponent extends Base { +function FNAbviewdataviewDetailComponent({ ABViewComponentPlugin }) { + return class ABAbviewdataviewDetailComponent extends ABViewComponentPlugin { constructor(baseView, idBase, ids) { super( baseView, @@ -16,6 +9,243 @@ function FNAbviewdataviewDetailComponent({ Object.assign({ detail: "" }, ids) ); this.idBase = idBase || `ABViewDetail_${baseView.id}`; + this.options = null; + + this.viewComponents = { + /* view.id : {viewComponent} */ + }; + // {hash} + // a reference of all our child views that we manage + + this.viewComponentIDs = { + /* view.id : {viewComponent} */ + }; + // {hash} + // a reference of all our child.ui().ids of the views we manage + + this._handlerChangePage = (pageId) => { + baseView.changePage(pageId); + }; + } + + ui() { + const views = this.view.viewsSortByPosition(); + const rowViews = this.getElements(views); + const _ui = super.ui(rowViews); + + delete _ui.type; + + // this wrapper allows the detail view to have a + // card appearance as well as enables the edit and + // details functions to work when clicked + return { + type: "form", + id: this.ids.component, + borderless: true, + rows: [ + { + body: _ui, + }, + ], + }; + } + + getElements(views) { + const rows = []; + const componentMap = {}; + + let curRowIndex; + let curColIndex; + + const settings = this.settings; + const defaultSettings = this.view.constructor.defaultValues(); + + views.forEach((v) => { + // let component = v.component(/* App, idPrefix */); + // NOTE: PONG - Just temporary to be compatible old & new versions + let component; + + try { + component = v.component(this.idBase); + // make sure any existing handlers for changePage are removed. + v.removeAllListeners("changePage"); + } catch (err) { + component = v.component(this.idBase); + + const ui = component.ui; + + component.ui = (() => ui).bind(component); + } + + this.viewComponents[v.id] = component; + + //// + //// TODO: figure out the embedded Callbacks => emit() + //// + // if key == "form" or "button" register the callbacks to the parent + // NOTE this will only work on the last form of a page! + // if (v.key == "form" && v._logic.callbacks) { + // _logic.callbacks = v._logic.callbacks; + // } + + // Create a new row + if (v.position.y == null || v.position.y !== curRowIndex) { + curRowIndex = v.position.y || rows.length; + curColIndex = 0; + + const rowNew = { + cols: [], + }; + + // Create columns following setting value + const colNumber = settings.columns || defaultSettings.columns; + + for (let i = 0; i < colNumber; i++) + rowNew.cols.push({ + gravity: settings.gravity?.[i] + ? parseInt(settings.gravity[i]) + : defaultSettings.gravity, + }); + + rows.push(rowNew); + } + + // Get the last row + const rowIndx = rows.length - 1; + const curRow = rows[rowIndx]; + const newPos = v.position.x ?? 0; + const mapKey = `${rowIndx}-${newPos}`; + + let getGrav = 1; + + if (componentMap[mapKey]) + console.error( + `Component[${component?.ids?.component}] is overwriting component[${componentMap[mapKey].ids?.component}]. <-- Reorder them to fix.` + ); + + componentMap[mapKey] = component; + + if (curRow.cols[newPos]?.gravity) + getGrav = curRow.cols[newPos].gravity; + + const _ui = component.ui(); + + this.viewComponentIDs[v.id] = _ui.id; + _ui.gravity = getGrav; + + // Add ui of sub-view to column + curRow.cols[newPos] = _ui; + + // Trigger 'changePage' event to parent + this.eventAdd({ + emitter: v, + eventName: "changePage", + listener: this._handlerChangePage, + }); + + curColIndex++; + }); + + return rows; + } + + async init(AB, accessLevel = 0, options = {}) { + await super.init(AB); + + this.options = options; + + const allInits = []; + + // // register our callbacks: + // if (options) { + // for (var c in _logic.callbacks) { + // _logic.callbacks[c] = options[c] || _logic.callbacks[c]; + // } + // } + + // see access by CSS class + $$(this.ids.component)?.define("css", `accessLevel-${accessLevel}`); + + const viewComponents = this.viewComponents; + + // attach all the .UI views: + for (const key in viewComponents) { + // skip when the view is removed. + if ( + !viewComponents[key] ?? + !this.view.views((v) => v.id === key).length + ) + continue; + + // Initial component along with options in case there are callbacks we need to listen for + if (accessLevel) { + allInits.push(viewComponents[key].init(AB, accessLevel, options)); + + continue; + } + + $$(this.viewComponentIDs[key])?.hide(); + } + + await Promise.all(allInits); + } + + onShow() { + const baseView = this.view; + + try { + const dataCy = `Detail ${baseView.name?.split(".")[0]} ${baseView.id}`; + + $$(this.ids.component)?.$view.setAttribute("data-cy", dataCy); + } catch (e) { + console.warn("Problem setting data-cy", e); + } + + // listen DC events + const dv = this.datacollection; + + if (dv) { + const currData = dv.getCursor(); + + if (currData) this.displayData(currData); + + ["changeCursor", "cursorStale", "collectionEmpty"].forEach((key) => { + this.eventAdd({ + emitter: dv, + eventName: key, + listener: (...p) => this.displayData(...p), + }); + }); + + this.eventAdd({ + emitter: dv, + eventName: "create", + listener: (createdRow) => { + const currCursor = dv.getCursor(); + + if (currCursor?.id === createdRow.id) + this.displayData(createdRow); + }, + }); + + this.eventAdd({ + emitter: dv, + eventName: "update", + listener: (updatedRow) => { + const currCursor = dv.getCursor(); + + if (currCursor?.id === updatedRow.id) + this.displayData(updatedRow); + }, + }); + } + + super.onShow(); + + // calll .onShow in child components + Object.values(this.viewComponents).forEach((val) => { + val.onShow?.(); + }); } displayData(rowData = {}) { @@ -103,12 +333,8 @@ function FNAbviewdataviewDetailComponent({ }; } -export default function FNAbviewdataviewComponent({ - ABViewContainerComponent, - ABViewComponentPlugin, -}) { +export default function FNAbviewdataviewComponent({ ABViewComponentPlugin }) { const DetailComponent = FNAbviewdataviewDetailComponent({ - ABViewContainerComponent, ABViewComponentPlugin, }); From e856d0c93b252fd58c051cf4958c67646243cf3a Mon Sep 17 00:00:00 2001 From: joshua_cm Date: Mon, 13 Apr 2026 22:34:21 +0700 Subject: [PATCH 5/6] fix(dataview): remove duplicate init and resolve CodeQL missing await --- .../included/view_dataview/FNAbviewdataviewComponent.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/AppBuilder/platform/plugins/included/view_dataview/FNAbviewdataviewComponent.js b/AppBuilder/platform/plugins/included/view_dataview/FNAbviewdataviewComponent.js index 839a75ba..09ce742b 100644 --- a/AppBuilder/platform/plugins/included/view_dataview/FNAbviewdataviewComponent.js +++ b/AppBuilder/platform/plugins/included/view_dataview/FNAbviewdataviewComponent.js @@ -419,10 +419,7 @@ export default function FNAbviewdataviewComponent({ ABViewComponentPlugin }) { // Card field components need AB + full async init before the first bind-driven // template pass (init(null, 2) from ui() is too early). Single init here, after AB exists. - const detailInit = this.detailComponent.init(AB, 2); - if (detailInit && typeof detailInit.then === "function") { - await detailInit; - } + await this.detailComponent.init(AB, 2); if (!dc) return; From 17053604977666dcc2097415d3a800b6cdde14f5 Mon Sep 17 00:00:00 2001 From: joshua_cm Date: Mon, 13 Apr 2026 23:42:03 +0700 Subject: [PATCH 6/6] fix(dataview): align component satisfy reviewer feedback - extend ABViewComponentPlugin only - remove container-based inheritance and duplicated logic --- .../FNAbviewdataviewComponent.js | 323 +----------------- 1 file changed, 14 insertions(+), 309 deletions(-) diff --git a/AppBuilder/platform/plugins/included/view_dataview/FNAbviewdataviewComponent.js b/AppBuilder/platform/plugins/included/view_dataview/FNAbviewdataviewComponent.js index 09ce742b..fcd8d24f 100644 --- a/AppBuilder/platform/plugins/included/view_dataview/FNAbviewdataviewComponent.js +++ b/AppBuilder/platform/plugins/included/view_dataview/FNAbviewdataviewComponent.js @@ -1,4 +1,8 @@ import ABViewPropertyLinkPageLocal from "./ABViewPropertyLinkPageLocal.js"; +import ABViewDetailComponentImport from "../../../views/viewComponent/ABViewDetailComponent.js"; + +const ABViewDetailComponent = + ABViewDetailComponentImport.default ?? ABViewDetailComponentImport; function FNAbviewdataviewDetailComponent({ ABViewComponentPlugin }) { return class ABAbviewdataviewDetailComponent extends ABViewComponentPlugin { @@ -9,326 +13,27 @@ function FNAbviewdataviewDetailComponent({ ABViewComponentPlugin }) { Object.assign({ detail: "" }, ids) ); this.idBase = idBase || `ABViewDetail_${baseView.id}`; - this.options = null; - - this.viewComponents = { - /* view.id : {viewComponent} */ - }; - // {hash} - // a reference of all our child views that we manage - - this.viewComponentIDs = { - /* view.id : {viewComponent} */ - }; - // {hash} - // a reference of all our child.ui().ids of the views we manage - - this._handlerChangePage = (pageId) => { - baseView.changePage(pageId); - }; + this._detail = new ABViewDetailComponent( + baseView, + this.idBase, + Object.assign({ detail: "" }, ids) + ); } ui() { - const views = this.view.viewsSortByPosition(); - const rowViews = this.getElements(views); - const _ui = super.ui(rowViews); - - delete _ui.type; - - // this wrapper allows the detail view to have a - // card appearance as well as enables the edit and - // details functions to work when clicked - return { - type: "form", - id: this.ids.component, - borderless: true, - rows: [ - { - body: _ui, - }, - ], - }; + return this._detail.ui(); } - getElements(views) { - const rows = []; - const componentMap = {}; - - let curRowIndex; - let curColIndex; - - const settings = this.settings; - const defaultSettings = this.view.constructor.defaultValues(); - - views.forEach((v) => { - // let component = v.component(/* App, idPrefix */); - // NOTE: PONG - Just temporary to be compatible old & new versions - let component; - - try { - component = v.component(this.idBase); - // make sure any existing handlers for changePage are removed. - v.removeAllListeners("changePage"); - } catch (err) { - component = v.component(this.idBase); - - const ui = component.ui; - - component.ui = (() => ui).bind(component); - } - - this.viewComponents[v.id] = component; - - //// - //// TODO: figure out the embedded Callbacks => emit() - //// - // if key == "form" or "button" register the callbacks to the parent - // NOTE this will only work on the last form of a page! - // if (v.key == "form" && v._logic.callbacks) { - // _logic.callbacks = v._logic.callbacks; - // } - - // Create a new row - if (v.position.y == null || v.position.y !== curRowIndex) { - curRowIndex = v.position.y || rows.length; - curColIndex = 0; - - const rowNew = { - cols: [], - }; - - // Create columns following setting value - const colNumber = settings.columns || defaultSettings.columns; - - for (let i = 0; i < colNumber; i++) - rowNew.cols.push({ - gravity: settings.gravity?.[i] - ? parseInt(settings.gravity[i]) - : defaultSettings.gravity, - }); - - rows.push(rowNew); - } - - // Get the last row - const rowIndx = rows.length - 1; - const curRow = rows[rowIndx]; - const newPos = v.position.x ?? 0; - const mapKey = `${rowIndx}-${newPos}`; - - let getGrav = 1; - - if (componentMap[mapKey]) - console.error( - `Component[${component?.ids?.component}] is overwriting component[${componentMap[mapKey].ids?.component}]. <-- Reorder them to fix.` - ); - - componentMap[mapKey] = component; - - if (curRow.cols[newPos]?.gravity) - getGrav = curRow.cols[newPos].gravity; - - const _ui = component.ui(); - - this.viewComponentIDs[v.id] = _ui.id; - _ui.gravity = getGrav; - - // Add ui of sub-view to column - curRow.cols[newPos] = _ui; - - // Trigger 'changePage' event to parent - this.eventAdd({ - emitter: v, - eventName: "changePage", - listener: this._handlerChangePage, - }); - - curColIndex++; - }); - - return rows; - } - - async init(AB, accessLevel = 0, options = {}) { - await super.init(AB); - - this.options = options; - - const allInits = []; - - // // register our callbacks: - // if (options) { - // for (var c in _logic.callbacks) { - // _logic.callbacks[c] = options[c] || _logic.callbacks[c]; - // } - // } - - // see access by CSS class - $$(this.ids.component)?.define("css", `accessLevel-${accessLevel}`); - - const viewComponents = this.viewComponents; - - // attach all the .UI views: - for (const key in viewComponents) { - // skip when the view is removed. - if ( - !viewComponents[key] ?? - !this.view.views((v) => v.id === key).length - ) - continue; - - // Initial component along with options in case there are callbacks we need to listen for - if (accessLevel) { - allInits.push(viewComponents[key].init(AB, accessLevel, options)); - - continue; - } - - $$(this.viewComponentIDs[key])?.hide(); - } - - await Promise.all(allInits); + init(AB, accessLevel = 0, options = {}) { + return this._detail.init(AB, accessLevel, options); } onShow() { - const baseView = this.view; - - try { - const dataCy = `Detail ${baseView.name?.split(".")[0]} ${baseView.id}`; - - $$(this.ids.component)?.$view.setAttribute("data-cy", dataCy); - } catch (e) { - console.warn("Problem setting data-cy", e); - } - - // listen DC events - const dv = this.datacollection; - - if (dv) { - const currData = dv.getCursor(); - - if (currData) this.displayData(currData); - - ["changeCursor", "cursorStale", "collectionEmpty"].forEach((key) => { - this.eventAdd({ - emitter: dv, - eventName: key, - listener: (...p) => this.displayData(...p), - }); - }); - - this.eventAdd({ - emitter: dv, - eventName: "create", - listener: (createdRow) => { - const currCursor = dv.getCursor(); - - if (currCursor?.id === createdRow.id) - this.displayData(createdRow); - }, - }); - - this.eventAdd({ - emitter: dv, - eventName: "update", - listener: (updatedRow) => { - const currCursor = dv.getCursor(); - - if (currCursor?.id === updatedRow.id) - this.displayData(updatedRow); - }, - }); - } - - super.onShow(); - - // calll .onShow in child components - Object.values(this.viewComponents).forEach((val) => { - val.onShow?.(); - }); + return this._detail.onShow(); } displayData(rowData = {}) { - const views = (this.view.views() || []).sort((a, b) => { - if (!a?.field?.() || !b?.field?.()) return 0; - if (a.field().key === "formula" && b.field().key === "calculate") - return -1; - if (a.field().key === "calculate" && b.field().key === "formula") - return 1; - return 0; - }); - - views.forEach((f) => { - let val; - - if (f.field) { - const field = f.field(); - if (!field) return; - - switch (field.key) { - case "connectObject": - val = field.pullRelationValues(rowData); - break; - case "list": - val = rowData?.[field.columnName]; - if (!val || (Array.isArray(val) && val.length === 0)) { - val = ""; - break; - } - if (field.settings.isMultiple === 0) { - let myVal = ""; - (field.settings.options || []).forEach((opt) => { - if (opt.id === val) myVal = opt.text; - }); - if (field.settings.hasColors) { - let hasCustomColor = ""; - (field.settings.options || []).forEach((h) => { - if (h.text === myVal) hasCustomColor = "hascustomcolor"; - }); - const hex = (field.settings.options || []).find( - (o) => o.text === myVal - )?.hex; - myVal = `${myVal}`; - } - val = myVal; - } else { - const items = val.map((value) => { - let myVal = ""; - (field.settings.options || []).forEach((opt) => { - if (opt.id === value.id) myVal = opt.text; - }); - const optionHex = - field.settings.hasColors && value.hex - ? `background: ${value.hex};` - : ""; - const hasCustomColor = - field.settings.hasColors && value.hex - ? "hascustomcolor" - : ""; - return `${myVal}`; - }); - val = items.join(""); - } - break; - case "user": - val = field.pullRelationValues(rowData); - break; - case "file": - val = rowData?.[field.columnName] ?? ""; - break; - case "formula": - val = rowData ? field.format(rowData, false) : ""; - break; - default: - val = field.format(rowData); - } - } - - const vComponent = - this.viewComponents?.[f.id] ?? f.component(this.idBase); - vComponent?.setValue?.(val); - vComponent?.displayText?.(rowData); - }); + return this._detail.displayData(rowData); } }; }