From 7c42e0e35e39cf8464a63246777a4d285583f12c Mon Sep 17 00:00:00 2001 From: Johnny Hausman Date: Thu, 29 Jan 2026 14:35:56 -0600 Subject: [PATCH 1/2] [wip] initial implementation of Bundled Plugins --- AppBuilder/ABFactory.js | 9 + AppBuilder/core | 2 +- AppBuilder/platform/ABClassManager.js | 6 + AppBuilder/platform/ABViewManager.js | 21 +- .../platform/plugins/ABViewWidgetPlugin.js | 31 + AppBuilder/platform/plugins/included/index.js | 12 + .../included/view_list/FNAbviewlist.js | 93 +++ .../view_list/FNAbviewlistComponent.js | 55 ++ .../plugins/included/view_tab/FNAbviewtab.js | 129 ++++ .../included/view_tab/FNAbviewtabComponent.js | 582 ++++++++++++++++++ init/Bootstrap.js | 7 +- 11 files changed, 934 insertions(+), 13 deletions(-) create mode 100644 AppBuilder/platform/plugins/ABViewWidgetPlugin.js create mode 100644 AppBuilder/platform/plugins/included/index.js create mode 100644 AppBuilder/platform/plugins/included/view_list/FNAbviewlist.js create mode 100644 AppBuilder/platform/plugins/included/view_list/FNAbviewlistComponent.js create mode 100644 AppBuilder/platform/plugins/included/view_tab/FNAbviewtab.js create mode 100644 AppBuilder/platform/plugins/included/view_tab/FNAbviewtabComponent.js diff --git a/AppBuilder/ABFactory.js b/AppBuilder/ABFactory.js index 8b24ff70..f243be34 100644 --- a/AppBuilder/ABFactory.js +++ b/AppBuilder/ABFactory.js @@ -29,6 +29,8 @@ import Multilingual from "../resources/Multilingual.js"; import Network from "../resources/Network.js"; // Network: our interface for communicating to our server +import LocalPlugins from "./platform/plugins/included/index.js"; + import Storage from "../resources/Storage.js"; // Storage: manages our interface for local storage @@ -875,6 +877,13 @@ class ABFactory extends ABFactoryCore { this._plugins.push(p); } + pluginLocalLoad() { + // This is a placeholder for a local plugin load. + // The platform version of this method will load the plugins from + // /platform/plugins/local/ + return LocalPlugins.load(this); + } + // // Utilities // diff --git a/AppBuilder/core b/AppBuilder/core index 6792fd38..8311121a 160000 --- a/AppBuilder/core +++ b/AppBuilder/core @@ -1 +1 @@ -Subproject commit 6792fd38566509cbb35c5f322fd3475d0ed746c8 +Subproject commit 8311121af8b7abc465268c5a08f4ae9f9dc4872a diff --git a/AppBuilder/platform/ABClassManager.js b/AppBuilder/platform/ABClassManager.js index d7a3e752..bc572a33 100644 --- a/AppBuilder/platform/ABClassManager.js +++ b/AppBuilder/platform/ABClassManager.js @@ -3,10 +3,14 @@ import ABPropertiesObjectPlugin from "./plugins/ABPropertiesObjectPlugin"; import ABObjectPlugin from "./plugins/ABObjectPlugin.js"; import ABModelPlugin from "./plugins/ABModelPlugin.js"; import ABViewPlugin from "./plugins/ABViewPlugin.js"; +import ABViewWidgetPlugin from "./plugins/ABViewWidgetPlugin.js"; import ABViewComponentPlugin from "./plugins/ABViewComponentPlugin.js"; import ABViewPropertiesPlugin from "./plugins/ABViewPropertiesPlugin.js"; import ABViewEditorPlugin from "./plugins/ABViewEditorPlugin.js"; +// some views need to reference ABViewContainer, +import ABViewContainer from "./views/ABViewContainer.js"; + const classRegistry = { ObjectTypes: new Map(), ObjectPropertiesTypes: new Map(), @@ -50,9 +54,11 @@ export function getPluginAPI() { ABObjectPlugin, ABModelPlugin, ABViewPlugin, + ABViewWidgetPlugin, ABViewComponentPlugin, ABViewPropertiesPlugin, ABViewEditorPlugin, + ABViewContainer, // ABFieldPlugin, // ABViewPlugin, }; diff --git a/AppBuilder/platform/ABViewManager.js b/AppBuilder/platform/ABViewManager.js index d81fc71e..9df52f44 100644 --- a/AppBuilder/platform/ABViewManager.js +++ b/AppBuilder/platform/ABViewManager.js @@ -10,17 +10,16 @@ module.exports = class ABViewManager extends ABViewManagerCore { static newView(values, application, parent) { parent = parent || null; - // check to see if this is a plugin view - if (values.plugin_key) { - // If this is from a plugin, create it from ClassManager - return ClassManager.viewCreate( - values.plugin_key, - values, - application, - parent - ); - } + // Moving to ClassManager for our default views: + let key = values.plugin_key || values.key; + let view = null; - return super.newView(values, application, parent); + try { + view = ClassManager.viewCreate(key, values, application, parent); + } catch (error) { + // console.error(`Error creating view ${key}:`, error); + view = super.newView(values, application, parent); + } + return view; } }; diff --git a/AppBuilder/platform/plugins/ABViewWidgetPlugin.js b/AppBuilder/platform/plugins/ABViewWidgetPlugin.js new file mode 100644 index 00000000..b97ff898 --- /dev/null +++ b/AppBuilder/platform/plugins/ABViewWidgetPlugin.js @@ -0,0 +1,31 @@ +import ABViewWidget from "../views/ABViewWidget.js"; + +export default class ABViewWidgetPlugin extends ABViewWidget { + constructor(...params) { + super(...params); + } + + static getPluginKey() { + return "ab-view-plugin"; + } + + static getPluginType() { + return "view"; + } + + toObj() { + const result = super.toObj(); + result.plugin_key = this.constructor.getPluginKey(); + // plugin_key : is what tells our ABFactory.objectNew() to create this object from the plugin class. + return result; + } + + static newInstance(application, parent) { + // return a new instance from ABViewManager: + return application.viewNew( + { key: this.common().key, plugin_key: this.getPluginKey() }, + application, + parent + ); + } +} diff --git a/AppBuilder/platform/plugins/included/index.js b/AppBuilder/platform/plugins/included/index.js new file mode 100644 index 00000000..1d707c93 --- /dev/null +++ b/AppBuilder/platform/plugins/included/index.js @@ -0,0 +1,12 @@ +import viewList from "./view_list/FNAbviewlist.js"; +import viewTab from "./view_tab/FNAbviewtab.js"; + +const AllPlugins = [viewTab, viewList]; + +export default { + load: (AB) => { + AllPlugins.forEach((plugin) => { + AB.pluginRegister(plugin); + }); + }, +}; diff --git a/AppBuilder/platform/plugins/included/view_list/FNAbviewlist.js b/AppBuilder/platform/plugins/included/view_list/FNAbviewlist.js new file mode 100644 index 00000000..70b4e183 --- /dev/null +++ b/AppBuilder/platform/plugins/included/view_list/FNAbviewlist.js @@ -0,0 +1,93 @@ +import FNAbviewlistComponent from "./FNAbviewlistComponent.js"; + +// FNAbviewlist Web +// A web side import for an ABView. +// +export default function FNAbviewlist({ + /*AB,*/ + ABViewWidgetPlugin, + ABViewComponentPlugin, + ABViewContainer, +}) { + const ABAbviewlistComponent = FNAbviewlistComponent({ + ABViewComponentPlugin, + }); + + const ABViewListPropertyComponentDefaults = { + dataviewID: null, + field: null, + height: 0, + }; + + const ABViewDefaults = { + key: "list", // {string} unique key for this view + icon: "list-ul", // {string} fa-[icon] reference for this view + labelKey: "List(plugin)", // {string} the multilingual label key for the class label + }; + + class ABViewListCore extends ABViewWidgetPlugin { + constructor(values, application, parent, defaultValues) { + super(values, application, parent, defaultValues || ABViewDefaults); + } + + static common() { + return ABViewDefaults; + } + + static defaultValues() { + return ABViewListPropertyComponentDefaults; + } + + /** + * @method componentList + * return the list of components available on this view to display in the editor. + */ + componentList() { + return []; + } + + field() { + var dv = this.datacollection; + if (!dv) return null; + + var object = dv.datasource; + if (!object) return null; + + return object.fieldByID(this.settings.field); + } + } + + return class ABViewList extends ABViewListCore { + /** + * @method getPluginKey + * return the plugin key for this view. + * @return {string} plugin key + */ + static getPluginKey() { + return this.common().key; + } + + /** + * @method component() + * return a UI component based upon this view. + * @return {obj} UI component + */ + component(parentId) { + return new ABAbviewlistComponent(this, parentId); + } + + // constructor(values, application, parent, defaultValues) { + // super(values, application, parent, defaultValues); + // } + + warningsEval() { + super.warningsEval(); + let DC = this.datacollection; + if (!DC) { + this.warningsMessage( + `can't resolve it's datacollection[${this.settings.dataviewID}]` + ); + } + } + }; +} diff --git a/AppBuilder/platform/plugins/included/view_list/FNAbviewlistComponent.js b/AppBuilder/platform/plugins/included/view_list/FNAbviewlistComponent.js new file mode 100644 index 00000000..7cac3ffb --- /dev/null +++ b/AppBuilder/platform/plugins/included/view_list/FNAbviewlistComponent.js @@ -0,0 +1,55 @@ +export default function FNAbviewlistComponent({ + /*AB,*/ + ABViewComponentPlugin, +}) { + return class ABAbviewlistComponent extends ABViewComponentPlugin { + constructor(baseView, idBase, ids) { + super( + baseView, + idBase || `ABViewList_${baseView.id}`, + Object.assign({ list: "" }, ids) + ); + } + + ui() { + const settings = this.settings; + const _uiList = { + id: this.ids.list, + view: "dataview", + type: { + width: 1000, + height: 30, + }, + template: (item) => { + const field = this.view.field(); + + if (!field) return ""; + + return field.format(item); + }, + }; + + // set height or autoHeight + if (settings.height !== 0) _uiList.height = settings.height; + else _uiList.autoHeight = true; + + const _ui = super.ui([_uiList]); + + delete _ui.type; + + return _ui; + } + + async init(AB) { + await super.init(AB); + + const dc = this.datacollection; + + if (!dc) return; + + // bind dc to component + dc.bind($$(this.ids.list)); + // $$(ids.list).sync(dv); + } + }; +} diff --git a/AppBuilder/platform/plugins/included/view_tab/FNAbviewtab.js b/AppBuilder/platform/plugins/included/view_tab/FNAbviewtab.js new file mode 100644 index 00000000..ef85a761 --- /dev/null +++ b/AppBuilder/platform/plugins/included/view_tab/FNAbviewtab.js @@ -0,0 +1,129 @@ +import FNAbviewtabComponent from "./FNAbviewtabComponent.js"; + +// FNAbviewtab Web +// A web side import for an ABView. +// +export default function FNAbviewtab({ + /*AB,*/ + ABViewWidgetPlugin, + ABViewComponentPlugin, + ABViewContainer, +}) { + const ABAbviewtabComponent = FNAbviewtabComponent({ ABViewComponentPlugin }); + + const ABViewTabPropertyComponentDefaults = { + height: 0, + minWidth: 0, + stackTabs: 0, // use sidebar view instead of tabview + darkTheme: 0, // set dark theme css or not + sidebarWidth: 200, // width of sidebar menu when stacking tabs + sidebarPos: "left", // the default position of sidebar + iconOnTop: 0, // do you want to put the icon above the text label? + hintID: null, // store the ID of a webix hint tutorial for this view + }; + + const ABViewTabDefaults = { + key: "tab", // {string} unique key for this view + icon: "window-maximize", // {string} fa-[icon] reference for this view + labelKey: "Tab(plugin)", // {string} the multilingual label key for the class label + }; + + class ABViewTabCore extends ABViewWidgetPlugin { + /** + * @param {obj} values key=>value hash of ABView values + * @param {ABApplication} application the application object this view is under + * @param {ABViewWidget} parent the ABViewWidget this view is a child of. (can be null) + */ + constructor(values, application, parent, defaultValues) { + super(values, application, parent, defaultValues || ABViewTabDefaults); + } + + static common() { + return ABViewTabDefaults; + } + + static defaultValues() { + return ABViewTabPropertyComponentDefaults; + } + + /// + /// Instance Methods + /// + + /** + * @method fromValues() + * + * initialze this object with the given set of values. + * @param {obj} values + */ + fromValues(values) { + super.fromValues(values); + + // convert from "0" => 0 + this.settings.height = parseInt(this.settings.height); + this.settings.minWidth = parseInt(this.settings.minWidth || 0); + this.settings.stackTabs = parseInt(this.settings.stackTabs); + this.settings.darkTheme = parseInt(this.settings.darkTheme); + this.settings.sidebarWidth = parseInt(this.settings.sidebarWidth); + // this.settings.sidebarPos = this.settings.sidebarPos; + this.settings.iconOnTop = parseInt(this.settings.iconOnTop); + } + + addTab(tabName, tabIcon) { + return this.application + .viewNew( + { + key: ABViewContainer.common().key, + label: tabName, + tabicon: tabIcon, + }, + this.application, + this + ) + .save(); + } + + /** + * @method componentList + * return the list of components available on this view to display in the editor. + */ + componentList() { + return []; + } + } + + return class ABViewTab extends ABViewTabCore { + /** + * @method getPluginKey + * return the plugin key for this view. + * @return {string} plugin key + */ + static getPluginKey() { + return this.common().key; + } + + /** + * @method component() + * return a UI component based upon this view. + * @return {obj} UI component + */ + component(parentId) { + return new ABAbviewtabComponent(this, parentId); + } + + warningsEval() { + super.warningsEval(); + + let allViews = this.views(); + + if (allViews.length == 0) { + this.warningsMessage("has no tabs set"); + } + + // NOTE: this is done in ABView: + // (this.views() || []).forEach((v) => { + // v.warningsEval(); + // }); + } + }; +} diff --git a/AppBuilder/platform/plugins/included/view_tab/FNAbviewtabComponent.js b/AppBuilder/platform/plugins/included/view_tab/FNAbviewtabComponent.js new file mode 100644 index 00000000..9016c5f9 --- /dev/null +++ b/AppBuilder/platform/plugins/included/view_tab/FNAbviewtabComponent.js @@ -0,0 +1,582 @@ +export default function FNAbviewtabComponent({ + /*AB,*/ + ABViewComponentPlugin, +}) { + return class ABAbviewtabComponent extends ABViewComponentPlugin { + constructor(baseView, idBase, ids) { + super( + baseView, + idBase || `ABViewTab_${baseView.id}`, + Object.assign( + { + tab: "", + + sidebar: "", + expandMenu: "", + collapseMenu: "", + + popupTabManager: "", + popupTabManagerForm: "", + popupTabManagerSaveButton: "", + }, + ids + ) + ); + + this.viewComponents = + this.viewComponents || + baseView + .views((v) => v.getUserAccess()) + .map((v) => { + return { + view: v, + // component: v.component(App) + }; + }); + } + + ui() { + const ids = this.ids; + const baseView = this.view; + const ab = this.AB; + const abWebix = ab.Webix; + + let _ui = null; + + // We are going to make a custom icon using the first letter of a menu item for menu items that don't have an icon + // to do this we need to modify the default template with the method webix recommended form this snippet https://snippet.webix.com/b566d9f8 + abWebix.type(abWebix.ui.tree, { + baseType: "sideBar", // inherit everything else from sidebar type + name: "customIcons", + icon: (obj, common) => { + if (obj.icon.length) + return [ + "", + ].join(""); + + return [ + "", + obj.value.charAt(0).toUpperCase(), + "", + ].join(""); + }, + }); + + const viewComponents = this.viewComponents; + const settings = this.settings; + + if (viewComponents.length > 0) { + if (settings.stackTabs) { + // define your menu items from the view components + const menuItems = viewComponents.map((vc) => { + const view = vc.view; + + return { + id: `${view.id}_menu`, + value: view.label, + icon: view.tabicon ? view.tabicon : "", + }; + }); + + if (menuItems.length) { + // create a menu item for the collapse option to use later + const collapseMenu = { + id: ids.collapseMenu, + value: this.label("Collapse Menu"), + icon: "chevron-circle-left", + }; + + // create a menu item from the expand option to use later + const expandMenu = { + id: ids.expandMenu, + value: this.label("Expand Menu"), + icon: "chevron-circle-right", + hidden: true, + }; + + // find out what the first option is so we can set it later + let selectedItem = `${viewComponents[0].view.id}_menu`; + + const abStorage = ab.Storage; + const sidebar = { + view: "sidebar", + type: "customIcons", // define the sidebar type with the new template created above + id: ids.sidebar, + height: settings.height, + width: settings.sidebarWidth ? settings.sidebarWidth : 0, + scroll: true, + position: settings.sidebarPos + ? settings.sidebarPos + : "left", + css: settings.darkTheme ? "webix_dark" : "", + data: menuItems.concat(collapseMenu), // add you menu items along with the collapse option to start + on: { + onItemClick: (id) => { + // when a menu item is clicked + if (id === ids.collapseMenu) { + // if it was the collapse menu item + setTimeout(() => { + const $sidebar = $$(ids.sidebar); + + // remove the collapse option from the menu + $sidebar.remove(ids.collapseMenu); + // add the expand option to the menu + $sidebar.add(expandMenu); + // toggle the sidebar state + $sidebar.toggle(); + // we just clicked the collapse...but we don't wanted highlighted + // so highlight the previously selected menu item + $sidebar.select(selectedItem); + // store this state in local storage the user preference is + // remembered next time they see this sidebar + abStorage.set( + `${ids.tab}-state`, + $sidebar.getState() + ); + }, 0); + } else if (id === ids.expandMenu) { + setTimeout(() => { + const $sidebar = $$(ids.sidebar); + + // remove the expand option from the menu + $sidebar.remove(ids.expandMenu); + // add the collapse option to the menu + $sidebar.add(collapseMenu); + // toggle the sidebar state + $sidebar.toggle(); + // we just clicked the collapse...but we don't wanted highlighted + // so highlight the previously selected menu item + $sidebar.select(selectedItem); + // store this state in local storage the user preference is + // remembered next time they see this sidebar + abStorage.set( + `${ids.tab}-state`, + $sidebar.getState() + ); + }, 0); + } else { + // store the selecte menu item just in case someone toggles the menu later + selectedItem = id; + // if the menu item is a regular menu item + // call the onShow with the view id to load the view + + id = id.replace("_menu", ""); + let node = $$(id); + if (node) { + node.show(false, false); + } else { + // How often does this occure? + let msg = `ABViewTabComponent[${this.name}][${this.id}] could not resolve UI panel for provided menu [${selectedItem}].`; + this.AB.notify("developer", msg, {}); + } + // $$(id).show(false, false); + + // onShow(id); + } + }, + onSelectChange: () => { + addDataCy(); + }, + onAfterRender: () => { + addDataCy(); + }, + }, + }; + + const multiview = { + view: "multiview", + id: ids.tab, + keepViews: true, + minWidth: settings.minWidth, + cells: viewComponents.map((view) => { + const tabUi = { + id: view.view.id, + // ui will be loaded when its tab is opened + view: "layout", + rows: [], + }; + + return tabUi; + }), + on: { + onViewChange: (prevId, nextId) => { + this.onShow(nextId); + }, + }, + }; + + const addDataCy = function () { + const $sidebar = $$(ids.sidebar); + + // set ids of controller buttons + const collapseNode = $sidebar?.$view.querySelector( + `[webix_tm_id="${ids.collapseMenu}"]` + ); + + if (collapseNode) + collapseNode.setAttribute( + "data-cy", + `tab-collapseMenu-${ids.collapseMenu}` + ); + + const expandNode = $sidebar?.$view.querySelector( + `[webix_tm_id="${ids.expandMenu}"]` + ); + + if (expandNode) + expandNode.setAttribute( + "data-cy", + `tab-expandMenu-${ids.expandMenu}` + ); + + baseView.views((view) => { + const node = $sidebar?.$view?.querySelector( + `[webix_tm_id="${view.id}_menu"]` + ); + + if (!node) { + return; + } + + node.setAttribute( + "data-cy", + `tab-${view.name.replace(" ", "")}-${view.id}-${ + baseView.id + }` + ); + }); + }; + + let columns = [sidebar, multiview]; + + if (settings.sidebarPos === "right") { + columns = [multiview, sidebar]; + } + + _ui = { + cols: columns, + }; + } else + _ui = { + view: "spacer", + }; + } else { + const cells = baseView + .views((view) => { + const accessLevel = view.getUserAccess(); + + if (accessLevel > 0) { + return view; + } + }) + .map((view) => { + const tabUi = { + id: view.id, + // ui will be loaded when its tab is opened + view: "layout", + rows: [], + }; + + let tabTemplate = ""; + + // tab icon + if (view.tabicon) { + if (settings.iconOnTop) + tabTemplate = [ + "

", + view.label, + "
", + ].join(""); + else + tabTemplate = [ + " ", + view.label, + ].join(""); + } + + // no icon + else tabTemplate = view.label; + + return { + header: tabTemplate, + body: tabUi, + }; + }); + + // if there are cells to display then return a tabview + if (cells.length) { + _ui = { + rows: [ + { + view: "tabview", + id: ids.tab, + minWidth: settings.minWidth, + height: settings.height, + tabbar: { + height: 60, + type: "bottom", + css: settings.darkTheme ? "webix_dark" : "", + on: { + onAfterRender: () => { + baseView.views((view) => { + const node = $$( + ids.tab + )?.$view?.querySelector( + `[button_id="${view.id}"]` + ); + + if (!node) return; + + node.setAttribute( + "data-cy", + `tab ${view.name} ${view.id} ${baseView.id}` + ); + }); + }, + }, + }, + multiview: { + on: { + onViewChange: (prevId, nextId) => { + this.onShow(nextId); + }, + }, + }, + cells: cells, + }, + ], + }; + } + // else we return a spacer + else + _ui = { + view: "spacer", + }; + } + } else + _ui = { + view: "spacer", + }; + + _ui = super.ui([_ui]); + + delete _ui.type; + + return _ui; + } + + async init(AB) { + await super.init(AB); + + const ids = this.ids; + const $tab = $$(ids.tab); + const ab = this.AB; + const abWebix = ab.Webix; + + if ($tab) abWebix.extend($tab, abWebix.ProgressBar); + + const baseView = this.view; + const viewComponents = this.viewComponents; + + viewComponents.forEach((vc) => { + // vc.component.init(AB); + + // Trigger 'changePage' event to parent + this.eventAdd({ + emitter: vc.view, + eventName: "changePage", + listener: (...p) => this.changePage(...p), + }); + }); + + // Trigger 'changeTab' event to parent + this.eventAdd({ + emitter: baseView, + eventName: "changeTab", + listener: (...p) => this.changeTab(...p), + }); + + // initialize the sidebar and figure out if it should be collased or not + const $sidebar = $$(ids.sidebar); + + if (!$sidebar) return; + + const state = await ab.Storage.get(`${ids.tab}-state`); + + if (!state) return; + + // create a menu item for the collapse option to use later + const collapseMenu = { + id: ids.collapseMenu, + value: this.label("Collapse Menu"), + icon: "chevron-circle-left", + }; + + // create a menu item from the expand option to use later + const expandMenu = { + id: ids.expandMenu, + value: this.label("Expand Menu"), + icon: "chevron-circle-right", + hidden: true, + }; + + // this will collapse or expand the sidebar + $sidebar.setState(state); + + const checkCollapseMenu = $sidebar.getItem(ids.collapseMenu) ?? null; + const checkExpandMenu = $sidebar.getItem(ids.expandMenu) ?? null; + + // if the state is collapsed we need to make sure the expand option is available + if (state.collapsed) { + if (checkCollapseMenu && checkExpandMenu) + // $sidebar.remove(ids.collapseMenu); + $sidebar.add(expandMenu); + } else if (checkCollapseMenu && checkExpandMenu) + // $sidebar.remove(ids.collapseMenu); + $sidebar.add(collapseMenu); + } + + changePage(pageId) { + const $tab = $$(this.ids.tab); + + $tab?.blockEvent(); + this.view.changePage(pageId); + $tab?.unblockEvent(); + } + + changeTab(tabViewId) { + const baseView = this.view; + const $tabViewId = $$(tabViewId); + + // switch tab view + this.toggleParent(baseView.parent); + + if (this.settings.stackTabs) + if (!$tabViewId.isVisible()) { + const showIt = setInterval(() => { + if ($tabViewId.isVisible()) clearInterval(showIt); + + $tabViewId.show(false, false); + }, 200); + } else $$(this.ids.tab).setValue(tabViewId); + } + + toggleParent(view) { + const $viewID = $$(view.id); + + if (view.key === "tab" || view.key === "viewcontainer") { + $viewID?.show(false, false); + } + if (view.parent) { + this.toggleParent(view.parent); + } + } + + onShow(viewId) { + const ids = this.ids; + + let defaultViewIsSet = false; + + const $sidebar = $$(ids.sidebar); + + // if no viewId is given, then try to get the currently selected ID + if (!viewId && $sidebar) + viewId = $sidebar.getSelectedId().replace("_menu", ""); + + const baseView = this.view; + const viewComponents = this.viewComponents; + + viewComponents.forEach((vc) => { + // set default view id + const currView = baseView.views((view) => { + return view.id === vc.view.id; + }); + + let accessLevel = 0; + + if (currView.length) accessLevel = currView[0].getUserAccess(); + + // choose the 1st View if we don't have one we are looking for. + if (!viewId && !defaultViewIsSet && accessLevel > 0) { + viewId = vc.view.id; + + defaultViewIsSet = true; + } + + // create view's component once + const $tab = $$(ids.tab); + const settings = this.settings; + + if (!vc?.component && vc?.view?.id === viewId) { + // show loading cursor + if ($tab?.showProgress) $tab.showProgress({ type: "icon" }); + + vc.component = vc.view.component(); + + const $viewID = $$(vc.view.id); + const ab = this.AB; + const abWebix = ab.Webix; + + if (settings.stackTabs) { + // update multiview UI + abWebix.ui( + { + // able to 'scroll' in tab view + id: vc.view.id, + view: "scrollview", + css: "ab-multiview-scrollview", + body: vc.component.ui(), + }, + $viewID + ); + } else { + // update tab UI + abWebix.ui( + { + // able to 'scroll' in tab view + id: vc.view.id, + view: "scrollview", + css: "ab-tabview-scrollview", + body: vc.component.ui(), + }, + $viewID + ); + } + + // for tabs we need to look at the view's accessLevels + accessLevel = vc.view.getUserAccess(); + + vc.component.init(ab, accessLevel); + + // done + setTimeout(() => { + // $$(v.view.id).adjust(); + + $tab?.hideProgress?.(); + // check if tab has a hint + // if (vc?.view?.settings?.hintID) { + // // fetch the steps for the hint + // let hint = ab.hintID(vc.view.settings.hintID); + // hint.createHintUI(); + // } + }, 10); + } + + // show UI + if (vc?.view?.id === viewId && vc?.component?.onShow) + vc.component.onShow(); + + if (settings.stackTabs && vc?.view?.id === viewId) { + $$(viewId)?.show(false, false); + $sidebar?.select(`${viewId}_menu`); + } + }); + } + }; +} diff --git a/init/Bootstrap.js b/init/Bootstrap.js index 6a09bdfd..ae883ba2 100644 --- a/init/Bootstrap.js +++ b/init/Bootstrap.js @@ -367,6 +367,7 @@ class Bootstrap extends EventEmitter { // load our installed plugins here: await loadPlugins(window.__AB_plugins_v1); + await this.AB.pluginLocalLoad(); await this.AB.init(); await webixLoading; // NOTE: special case: User has no Roles defined. @@ -446,7 +447,11 @@ class Bootstrap extends EventEmitter { } alert(options) { - webix.alert(options); + if (webix?.alert) { + webix.alert(options); + } else { + console.error(options); + } } div(el) { From 29ca2c02547e910209283c875ebdbe71a13600c2 Mon Sep 17 00:00:00 2001 From: Johnny Hausman Date: Thu, 29 Jan 2026 15:07:29 -0600 Subject: [PATCH 2/2] [wip] refactor pluginLocalLoad() to be in ABFactory.init() --- AppBuilder/core | 2 +- init/Bootstrap.js | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/AppBuilder/core b/AppBuilder/core index 8311121a..389fa778 160000 --- a/AppBuilder/core +++ b/AppBuilder/core @@ -1 +1 @@ -Subproject commit 8311121af8b7abc465268c5a08f4ae9f9dc4872a +Subproject commit 389fa77818d08f4feef03bbf81762588398f583f diff --git a/init/Bootstrap.js b/init/Bootstrap.js index ae883ba2..9a730a71 100644 --- a/init/Bootstrap.js +++ b/init/Bootstrap.js @@ -367,7 +367,6 @@ class Bootstrap extends EventEmitter { // load our installed plugins here: await loadPlugins(window.__AB_plugins_v1); - await this.AB.pluginLocalLoad(); await this.AB.init(); await webixLoading; // NOTE: special case: User has no Roles defined.