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..7e651f39 --- /dev/null +++ b/AppBuilder/platform/plugins/included/view_dataview/ABViewPropertyLinkPageLocal.js @@ -0,0 +1,54 @@ +// 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) { + 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); + }); + dc.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..c5d1d52c --- /dev/null +++ b/AppBuilder/platform/plugins/included/view_dataview/FNAbviewdataview.js @@ -0,0 +1,156 @@ +import FNAbviewdataviewComponent from "./FNAbviewdataviewComponent.js"; + +// Dataview plugin: replaces ABViewDataviewCore + ABViewDataview. +// All runtime logic is kept in this plugin module. +export default function FNAbviewdataview({ + ABViewContainer, + ABViewComponentPlugin, +}) { + const ABAbviewdataviewComponent = FNAbviewdataviewComponent({ + 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..fcd8d24f --- /dev/null +++ b/AppBuilder/platform/plugins/included/view_dataview/FNAbviewdataviewComponent.js @@ -0,0 +1,409 @@ +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 { + constructor(baseView, idBase, ids) { + super( + baseView, + idBase || `ABViewDetail_${baseView.id}`, + Object.assign({ detail: "" }, ids) + ); + this.idBase = idBase || `ABViewDetail_${baseView.id}`; + this._detail = new ABViewDetailComponent( + baseView, + this.idBase, + Object.assign({ detail: "" }, ids) + ); + } + + ui() { + return this._detail.ui(); + } + + init(AB, accessLevel = 0, options = {}) { + return this._detail.init(AB, accessLevel, options); + } + + onShow() { + return this._detail.onShow(); + } + + displayData(rowData = {}) { + return this._detail.displayData(rowData); + } + }; +} + +export default function FNAbviewdataviewComponent({ ABViewComponentPlugin }) { + const DetailComponent = FNAbviewdataviewDetailComponent({ + 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: { + // Tab/multiview can show the dataview after init; re-apply cy + handlers. + onViewShow: () => { + this.addCyAttribute(); + this.applyClickEvent(); + this.resize(); + }, + onAfterRender: () => { + this.applyClickEvent(); + this.addCyAttribute(); + }, + }, + }, + ], + }, + ]); + } + + 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. + await this.detailComponent.init(AB, 2); + + if (!dc) return; + + this.linkPage = this.linkPageHelper.component(); + this.linkPage.init({ + view: this.view, + datacollection: dc, + }); + + dc.bind($dataView); + + this.initRefreshWarning(); + + window.addEventListener("resize", () => { + clearTimeout(this._resizeEvent); + this._resizeEvent = setTimeout(() => { + const $dv = $$(this.ids.dataview); + if ($dv) this.resize($dv.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() { + // 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()); + } + + 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 = { + id: "__ab_dataview_sizing__", + uuid: "__ab_dataview_sizing__", + ...(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); + const rowId = item?.id ?? item?.uuid ?? ""; + return tmpDom.innerHTML.replace(/#itemId#/g, rowId); + } + + 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); + if (!$dataview?.$view) return; + 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 ?? item?.id ?? ""; + 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()); + } + }; +} 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`); } - }); + } } };