diff --git a/AppBuilder/ABFactory.js b/AppBuilder/ABFactory.js index 72349e2f..05b0fd20 100644 --- a/AppBuilder/ABFactory.js +++ b/AppBuilder/ABFactory.js @@ -32,7 +32,7 @@ import Network from "../resources/Network.js"; import Storage from "../resources/Storage.js"; // Storage: manages our interface for local storage -import ABViewManager from "./core/ABViewManagerCore"; +import ABViewManager from "./platform/ABViewManager"; 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..692e247a 160000 --- a/AppBuilder/core +++ b/AppBuilder/core @@ -1 +1 @@ -Subproject commit a7b2f392277e743ea89d4494094533d290d34729 +Subproject commit 692e247a484ef0ae1275b6889a2ab5b417747b59 diff --git a/AppBuilder/platform/plugins/included/index.js b/AppBuilder/platform/plugins/included/index.js index 2ad265f0..bd2fbc35 100644 --- a/AppBuilder/platform/plugins/included/index.js +++ b/AppBuilder/platform/plugins/included/index.js @@ -1,3 +1,4 @@ +import viewDataview from "./view_dataview/FNAbviewdataview.js"; import viewCarousel from "./view_carousel/FNAbviewcarousel.js"; import viewComment from "./view_comment/FNAbviewcomment.js"; import viewCsvExporter from "./view_csvExporter/FNAbviewcsvexporter.js"; @@ -27,7 +28,7 @@ const AllPlugins = [ viewPdfImporter, viewTab, viewText, -]; +, viewDataview]; export default { load: (AB) => { 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..830dc982 --- /dev/null +++ b/AppBuilder/platform/plugins/included/view_dataview/FNAbviewdataview.js @@ -0,0 +1,151 @@ +import FNAbviewdataviewComponent from "./FNAbviewdataviewComponent.js"; +import FNABViewDetail from "../view_detail/FNAbviewdetail.js"; + +// FNAbviewdataview Web +// A web side import for an ABView. +// +export default function FNAbviewdataview({ + /*AB,*/ + ABViewComponentPlugin, + ABViewContainer, + ABViewContainerComponent, + ABViewPropertyLinkPage, +}) { + const ABAbviewdataviewComponent = FNAbviewdataviewComponent({ + ABViewComponentPlugin, + ABViewContainerComponent, + ABViewPropertyLinkPage, + }); + + const ABViewDataviewPropertyComponentDefaults = { + xCount: 1, // {int} the number of columns per row (need at least one) + detailsPage: "", + detailsTab: "", + editPage: "", + editTab: "", + }; + + const ABViewDataviewDefaults = { + key: "dataview", // {string} unique key for this view + icon: "th", // {string} fa-[icon] reference for this view + labelKey: "Data view(plugin)", // {string} the multilingual label key for the class label + }; + + const ABViewDetail = FNABViewDetail({ + ABViewContainer, + ABViewContainerComponent, + }); + + class ABViewDataviewCore extends ABViewDetail { + /** + * @param {obj} values key=>value hash of ABView values + * @param {ABApplication} application the application object this view is under + * @param {ABView} parent the ABView this view is a child of. (can be null) + */ + constructor(values, application, parent, defaultValues) { + super( + values, + application, + parent, + defaultValues || ABViewDataviewDefaults + ); + } + + static common() { + return ABViewDataviewDefaults; + } + + static defaultValues() { + return ABViewDataviewPropertyComponentDefaults; + } + + /// + /// Instance Methods + /// + + /** + * @method fromValues() + * + * initialze this object with the given set of values. + * @param {obj} values + */ + 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; + } + + 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; + } + } + + return class ABViewDataview extends ABViewDataviewCore { + /** + * @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 ABAbviewdataviewComponent(this, parentId); + } + + // constructor(values, application, parent, defaultValues) { + // super(values, application, parent, defaultValues); + // } + + /** + * @method fromValues() + * + * initialze this object with the given set of values. + * @param {obj} values + */ + fromValues(values) { + super.fromValues(values); + + this.settings.detailsPage = + this.settings.detailsPage ?? ABViewDataviewDefaults.detailsPage; + this.settings.editPage = + this.settings.editPage ?? ABViewDataviewDefaults.editPage; + this.settings.detailsTab = + this.settings.detailsTab ?? ABViewDataviewDefaults.detailsTab; + this.settings.editTab = + this.settings.editTab ?? ABViewDataviewDefaults.editTab; + } + }; +} 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..50f725dd --- /dev/null +++ b/AppBuilder/platform/plugins/included/view_dataview/FNAbviewdataviewComponent.js @@ -0,0 +1,404 @@ +import FNABViewDetailComponent from "../view_detail/FNAbviewdetailComponent.js"; + +export default function FNAbviewdataviewComponent({ + /*AB,*/ + ABViewComponentPlugin, + ABViewContainerComponent, + ABViewPropertyLinkPage, +}) { + const ABViewDetailComponent = FNABViewDetailComponent({ + ABViewContainerComponent, + }); + + return class ABAbviewdataviewComponent extends ABViewComponentPlugin { + constructor(baseView, idBase, ids) { + super( + baseView, + idBase || `ABViewDataview_${baseView.id}`, + Object.assign( + { + dataview: "", + reload: "", + }, + ids + ) + ); + + this.linkPage = null; + } + + ui() { + // NOTE: need to initial the detail component here + // because its dom width & height values are used .template function + this.initDetailComponent(); + + const ids = this.ids; + const L = (...params) => (this.AB ?? AB).Multilingual.label(...params); + const _ui = 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: (/* id, event */) => { + 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(); + }, + }, + }, + ], + }, + ]); + + return _ui; + } + + async init(AB) { + await super.init(AB); + + const dc = this.datacollection; + if (!dc) return; + + // Initial the link page helper + this.linkPage = this.linkPageHelper.component(); + this.linkPage.init({ + view: this.view, + datacollection: dc, + }); + + const ids = this.ids; + const $dataView = $$(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); + }); + } + + /** + * @method initRefreshWarning + * + */ + 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(data); + }); + }); + } + + showRefreshWarning() { + if (this.__throttleRefreshWarning) + clearTimeout(this.__throttleRefreshWarning); + + this.__throttleRefreshWarning = setTimeout(() => { + $$(this.ids.reload)?.show(); + }, 200); + } + + reloadData() { + const dc = this.datacollection; + dc?.reloadData(); + + $$(this.ids.reload)?.hide(); + } + + onShow() { + super.onShow(); + + this.resize(); + } + + resize(base_element) { + const $dataview = $$(this.ids.dataview); + if (!$dataview) { + // Not sure if its a problem so notify + this.AB.notify.developer( + new Error("Resize called on missing dataview component"), + { context: "ABViewDataviewComponent.resize()", ids: this.ids } + ); + return; + } + $dataview.resize(); + + const item_width = this.getItemWidth(base_element); + $dataview.customize({ width: item_width }); + $dataview.getTopParentView?.().resize?.(); + } + + initDetailComponent() { + const detailUI = this.getDetailUI(); + this._detail_ui = this.AB.Webix.ui(detailUI); + + // 2 - Always allow access to components inside data view + this.detailComponent.init(null, 2); + } + + getDetailUI() { + const detailCom = this.detailComponent; + const editPage = this.settings.editPage; + const detailsPage = this.settings.detailsPage; + + const _ui = detailCom.ui(); + // adjust the UI to make sure it will look like a "card" + _ui.type = "clean"; + _ui.css = "ab-detail-view"; + + if (detailsPage || editPage) { + _ui.css += ` ab-detail-hover ab-record-#itemId#`; + + if (detailsPage) _ui.css += " ab-detail-page"; + if (editPage) _ui.css += " ab-edit-page"; + } + + return _ui; + } + + itemTemplate(item) { + const detailCom = this.detailComponent; + const $dataview = $$(this.ids.dataview); + const $detail_item = this._detail_ui; + + // Mock up data to initialize height of item + 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; + } + }); + } + detailCom.displayData(item); + + const itemWidth = + $dataview.data.count() > 0 + ? $dataview.type.width + : ($detail_item.$width - 20) / this.settings.xCount; + + const itemHeight = + $dataview.data.count() > 0 + ? $dataview.type.height + : $detail_item.getChildViews()?.[0]?.$height; + + const tmp_dom = document.createElement("div"); + tmp_dom.appendChild($detail_item.$view); + + $detail_item.define("width", itemWidth - 24); + $detail_item.define("height", itemHeight + 15); + $detail_item.adjust(); + + // Add cy attributes + this.addCyItemAttributes(tmp_dom, item); + + return tmp_dom.innerHTML.replace(/#itemId#/g, item.id); + } + + getItemWidth(base_element) { + const $dataview = $$(this.ids.dataview); + + let currElem = base_element ?? $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; + + // check if the browser window minus webix default padding is the same as the parent window + // if so we need to check to see if there is a sidebar and reduce the usable space by the + // width of the sidebar + if (window.innerWidth - 19 <= parentWidth) { + const $sidebar = this.getTabSidebar(); + if ($sidebar) { + parentWidth -= $sidebar.$width; + } + } + + const recordWidth = Math.floor(parentWidth / this.settings.xCount); + + return recordWidth; + } + + 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 ?? []; + + // NOTE: Some web browser clients do not support .path + if (!divs.length) { + divs.push(e.target); + divs.push(e.target.parentNode); + } + + if (editPage) { + for (let 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) { + // var record = parseInt(c.replace("ab-record-", "")); + const record = c.replace("ab-record-", ""); + this.linkPage.changePage(editPage, record); + // com.logic.toggleTab(detailsTab, ids.component); + } + }); + break; + } + } + } + + if (detailsPage && !clicked) { + for (let p of divs) { + if ( + p.className && + p.className.indexOf("webix_accordionitem") > -1 + ) { + p.parentNode.parentNode.classList.forEach((c) => { + if (c.indexOf("ab-record-") > -1) { + // var record = parseInt(c.replace("ab-record-", "")); + const record = c.replace("ab-record-", ""); + this.linkPage.changePage(detailsPage, record); + // com.logic.toggleTab(detailsTab, ids.component); + } + }); + + 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 ABViewDetailComponent( + this.view, + `${this.ids.component}_detail_view` + )); + } + + get linkPageHelper() { + return (this.__linkPageHelper = + this.__linkPageHelper || new ABViewPropertyLinkPage()); + } + }; +} diff --git a/AppBuilder/platform/plugins/included/view_detail/FNAbviewdetailComponent.js b/AppBuilder/platform/plugins/included/view_detail/FNAbviewdetailComponent.js index eb05f1a1..97df2d60 100644 --- a/AppBuilder/platform/plugins/included/view_detail/FNAbviewdetailComponent.js +++ b/AppBuilder/platform/plugins/included/view_detail/FNAbviewdetailComponent.js @@ -42,13 +42,15 @@ export default function FNAbviewdetailComponent({ ABViewContainerComponent }) { 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), - }); - }); + ["changeCursor", "cursorStale", "collectionEmpty"].forEach( + (key) => { + this.eventAdd({ + emitter: dv, + eventName: key, + listener: (...p) => this.displayData(...p), + }); + } + ); this.eventAdd({ emitter: dv, eventName: "create", @@ -171,7 +173,7 @@ export default function FNAbviewdetailComponent({ ABViewContainerComponent }) { break; default: val = field.format(rowData); - // break; + // break; } }