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..422b8903 160000 --- a/AppBuilder/core +++ b/AppBuilder/core @@ -1 +1 @@ -Subproject commit a7b2f392277e743ea89d4494094533d290d34729 +Subproject commit 422b890317b0730a0ecb62ad5036dd85cbf9c1c5 diff --git a/AppBuilder/platform/ABClassManager.js b/AppBuilder/platform/ABClassManager.js index c9fa8e0a..2ba90f08 100644 --- a/AppBuilder/platform/ABClassManager.js +++ b/AppBuilder/platform/ABClassManager.js @@ -17,6 +17,10 @@ import ABViewPropertyFilterData from "./views/viewProperties/ABViewPropertyFilte import ABViewPropertyLinkPage from "./views/viewProperties/ABViewPropertyLinkPage"; import ABViewRuleListFormRecordRules from "../rules/ABViewRuleListFormRecordRules"; import ABViewRuleListFormSubmitRules from "../rules/ABViewRuleListFormSubmitRules"; +import ABViewPropertyAddPage from "./views/viewProperties/ABViewPropertyAddPage"; +import ABViewPropertyEditPage from "./views/viewProperties/ABViewPropertyEditPage"; +import ABFieldImage from "./dataFields/ABFieldImage"; +import FocusableTemplate from "../../webix_custom_components/focusableTemplate"; // MIGRATION: ABViewManager is depreciated. Use ABClassManager instead. import ABViewManager from "./ABViewManager.js"; @@ -74,6 +78,10 @@ export function getPluginAPI() { ABViewPropertyLinkPage, ABViewRuleListFormRecordRules, ABViewRuleListFormSubmitRules, + ABViewPropertyAddPage, + ABViewPropertyEditPage, + ABFieldImage, + FocusableTemplate, // ABFieldPlugin, // ABViewPlugin, }; diff --git a/AppBuilder/platform/RowUpdater.js b/AppBuilder/platform/RowUpdater.js index 57ac54e7..e8e22f43 100644 --- a/AppBuilder/platform/RowUpdater.js +++ b/AppBuilder/platform/RowUpdater.js @@ -1,6 +1,5 @@ // const ABComponent = require("./ABComponent"); import ClassUI from "../../ui/ClassUI"; -const ABViewForm = require("../platform/views/ABViewForm"); let L = null; @@ -313,15 +312,13 @@ class RowUpdater extends ClassUI { this._Object = object; this._mockApp = this.AB.applicationNew({}); - this._mockFormWidget = new ABViewForm( - { - settings: { - showLabel: false, - labelWidth: 0, - }, + this._mockFormWidget = this.AB.viewNewDetatched({ + key: "form", + settings: { + showLabel: false, + labelWidth: 0, }, - this._mockApp // just need any ABApplication here - ); + }); this._mockFormWidget.objectLoad(object); this.setValue(null); // clear diff --git a/AppBuilder/platform/dataFields/ABFieldJson.js b/AppBuilder/platform/dataFields/ABFieldJson.js index 70bf902f..a6cdba4c 100644 --- a/AppBuilder/platform/dataFields/ABFieldJson.js +++ b/AppBuilder/platform/dataFields/ABFieldJson.js @@ -13,13 +13,47 @@ module.exports = class ABFieldJson extends ABFieldJsonCore { columnHeader(options) { const config = super.columnHeader(options); - // config.editor = null; // read only for now config.editor = "text"; config.css = "textCell"; // when called by ABViewFormCustom, will need a .template() fn. - // currently we don't need to return anything so ... - config.template = () => ""; + config.template = (obj) => { + const val = obj[this.columnName]; + + if (val && typeof val == "object") { + try { + return JSON.stringify(val); + } catch (e) { + return val.toString(); + } + } + + return val || ""; + }; + + config.editFormat = (val) => { + if (val && typeof val == "object") { + try { + return JSON.stringify(val); + } catch (e) { + return val.toString(); + } + } + + return val || ""; + }; + + config.editParse = (val) => { + if (val && typeof val == "string") { + try { + return JSON.parse(val); + } catch (e) { + /* ignore */ + } + } + + return val; + }; return config; } @@ -67,9 +101,22 @@ module.exports = class ABFieldJson extends ABFieldJsonCore { } setValue(item, rowData) { - super.setValue(item, rowData, ""); + let val = rowData[this.columnName]; + + if (val && typeof val == "object") { + try { + val = JSON.stringify(val); + } catch (e) { + /* ignore */ + } + } + + const cloneRow = Object.assign({}, rowData, { [this.columnName]: val }); + + super.setValue(item, cloneRow); + if (item) { - item.config.value = rowData[this.columnName]; + item.config.value = val; } } diff --git a/AppBuilder/platform/dataFields/ABFieldList.js b/AppBuilder/platform/dataFields/ABFieldList.js index 7a716c12..5dd76841 100644 --- a/AppBuilder/platform/dataFields/ABFieldList.js +++ b/AppBuilder/platform/dataFields/ABFieldList.js @@ -421,8 +421,14 @@ module.exports = class ABFieldList extends ABFieldListCore { result.push(rowData); } } - if (result.length) { - if (typeof result == "string") result = JSON.parse(result); + if (result && result.length) { + if (typeof result == "string") { + try { + result = JSON.parse(result); + } catch (e) { + console.error(`Error JSON.parsing result [${result}]: `, e); + } + } // Pull text with current language if (this.settings) { @@ -456,9 +462,10 @@ function _getSelectedOptions(field, rowData = {}) { result = rowData[field.columnName]; try { - if (typeof result == "string") result = JSON.parse(result); + if (typeof result == "string" && result != "") + result = JSON.parse(result); } catch (e) { - console.error(`Error JSON.pars()ing result [${result}]: `, e); + console.error(`Error JSON.parsing result [${result}]: `, e); // just go with what is there result = rowData[field.columnName]; } diff --git a/AppBuilder/platform/dataFields/ABFieldTree.js b/AppBuilder/platform/dataFields/ABFieldTree.js index f8c6b716..d5c752e9 100644 --- a/AppBuilder/platform/dataFields/ABFieldTree.js +++ b/AppBuilder/platform/dataFields/ABFieldTree.js @@ -230,8 +230,16 @@ module.exports = class ABFieldTree extends ABFieldTreeCore { typeof row[field.columnName] != "undefined" ) { values = row[field.columnName]; + if (typeof values == "string" && values !== "") { + try { + values = JSON.parse(values); + } catch (e) { + // If it's a comma-separated string? + values = values.split(",").filter((v) => v !== ""); + } + } } - return values; + return Array.isArray(values) ? values : []; } function populateTree(field, vals) { @@ -247,7 +255,7 @@ module.exports = class ABFieldTree extends ABFieldTreeCore { $Tree.uncheckAll(); $Tree.openAll(); - if (values != null && values.length) { + if (Array.isArray(values)) { values.forEach(function (id) { if ($Tree.exists(id)) { $Tree.checkItem(id); @@ -351,7 +359,10 @@ module.exports = class ABFieldTree extends ABFieldTreeCore { const rowData = {}; rowData[field.columnName] = $$(idTree).getChecked(); - field.setValue($$(parentComponent.ui.id), rowData); + field.setValue( + $$(parentComponent.ids.formItem), + rowData + ); } }, }, @@ -426,9 +437,15 @@ module.exports = class ABFieldTree extends ABFieldTreeCore { return detailComponentSetting; } - getValue(item, rowData) { + getValue(item) { + if (!item) return {}; + // selectivity let values = {}; - values = item.getValues(); + if (typeof item.getValues == "function") { + values = item.getValues(); + } else if (typeof item.getValue == "function") { + values = item.getValue(); + } return values; } @@ -437,7 +454,11 @@ module.exports = class ABFieldTree extends ABFieldTreeCore { const val = rowData[this.columnName] || []; - item.setValues(val); + if (typeof item.setValues == "function") { + item.setValues(val); + } else if (typeof item.setValue == "function") { + item.setValue(val); + } // get dom const dom = item.$view.querySelector(".list-data-values"); diff --git a/AppBuilder/platform/plugins/included/index.js b/AppBuilder/platform/plugins/included/index.js index 2ad265f0..bfb1eeea 100644 --- a/AppBuilder/platform/plugins/included/index.js +++ b/AppBuilder/platform/plugins/included/index.js @@ -1,3 +1,5 @@ +import viewDataview from "./view_dataview/FNAbviewdataview.js"; +import viewForm from "./view_form/FNAbviewform.js"; import viewCarousel from "./view_carousel/FNAbviewcarousel.js"; import viewComment from "./view_comment/FNAbviewcomment.js"; import viewCsvExporter from "./view_csvExporter/FNAbviewcsvexporter.js"; @@ -17,7 +19,6 @@ const AllPlugins = [ viewComment, viewCsvExporter, viewCsvImporter, - viewCsvImporter, viewDataSelect, viewDetail, viewImage, @@ -27,6 +28,8 @@ const AllPlugins = [ viewPdfImporter, viewTab, viewText, + viewDataview, + viewForm, ]; export default { 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; } } diff --git a/AppBuilder/platform/plugins/included/view_form/FNAbviewform.js b/AppBuilder/platform/plugins/included/view_form/FNAbviewform.js new file mode 100644 index 00000000..4aaf0be4 --- /dev/null +++ b/AppBuilder/platform/plugins/included/view_form/FNAbviewform.js @@ -0,0 +1,539 @@ +import FNAbviewformComponent from "./FNAbviewformComponent.js"; +import * as fComponents from "./FormComponents.js"; + +// Internalized Core Factories +import FNAbviewformCore from "./core/ABViewFormCore.js"; +import FNAbviewformItemCore from "./core/ABViewFormItemCore.js"; +import FNAbviewformCustomCore from "./core/ABViewFormCustomCore.js"; + +/** + * FNAbviewform + * A web side import for an ABViewForm. + */ +export default function FNAbviewform(API) { + const { + ABViewComponentPlugin, + ABViewPlugin, + ABViewContainer, + ABViewRuleListFormRecordRules, + ABViewRuleListFormSubmitRules, + ABViewPropertyAddPage, + ABViewPropertyEditPage, + ABFieldImage, + FocusableTemplate, + AB, + } = API; + + let FormAPI = { + AB, + ABViewComponentPlugin, + ABViewPlugin, + ABViewPropertyAddPage, + ABViewPropertyEditPage, + ABFieldImage, + FocusableTemplate, + }; + + // 1. Initialize Base Item + const { + FNAbviewformItem, + FNAbviewformCustom, + FNAbviewformURL, + ...otherFComponents + } = fComponents; + + FormAPI.ABViewFormItem = FNAbviewformItem(FormAPI); + + // Store ABViewFormItem for 'instanceof' checks in other plugins + if (AB && AB.Class) { + AB.Class.ABViewFormItem = FormAPI.ABViewFormItem; + } + + FormAPI.ABViewFormItemComponent = + FormAPI.ABViewFormItem.ABViewFormItemComponent; + FormAPI.ABViewFormItemCore = FNAbviewformItemCore(ABViewPlugin); + + // 2. Initialize Custom (base for others) + FormAPI.ABViewFormCustom = FNAbviewformCustom(FormAPI); + FormAPI.ABViewFormCustomCore = FNAbviewformCustomCore( + FormAPI.ABViewFormItemCore + ); + + // 3. Initialize common views + const views = Object.values(otherFComponents).map((FNv) => FNv(FormAPI)); + views.push(FormAPI.ABViewFormItem); + views.push(FormAPI.ABViewFormCustom); + + const ABViewFormButton = views.find((v) => v.common().key === "button"); + const ABViewFormCheckbox = views.find((v) => v.common().key === "checkbox"); + const ABViewFormConnect = views.find((v) => v.common().key === "connect"); + const ABViewFormCustom = FormAPI.ABViewFormCustom; + const ABViewFormDatepicker = views.find( + (v) => v.common().key === "datepicker" + ); + const ABViewFormItem = FormAPI.ABViewFormItem; + const ABViewFormJson = views.find((v) => v.common().key === "json"); + const ABViewFormNumber = views.find((v) => v.common().key === "numberbox"); + const ABViewFormReadonly = views.find( + (v) => v.common().key === "fieldreadonly" + ); + const ABViewFormSelectMultiple = views.find( + (v) => v.common().key === "selectmultiple" + ); + const ABViewFormSelectSingle = views.find( + (v) => v.common().key === "selectsingle" + ); + const ABViewFormTree = views.find((v) => v.common().key === "tree"); + const ABViewFormTextbox = views.find((v) => v.common().key === "textbox"); + + // 4. Initialize Form Base and URL view + const ABRecordRule = ABViewRuleListFormRecordRules; + const ABSubmitRule = ABViewRuleListFormSubmitRules; + + const ABViewFormBase = FNAbviewformCore( + ABViewContainer, + ABViewFormItem, + ABRecordRule, + ABSubmitRule + ); + + ABViewFormBase.prototype.superComponent = function () { + if (this._superComponent == null) { + this._superComponent = ABViewContainer.prototype.component.call(this); + } + return this._superComponent; + }; + + FormAPI.ABViewFormBase = ABViewFormBase; + + const ABAbviewformComponent = FNAbviewformComponent({ + ABViewComponentPlugin, + ABViewFormButton, + ABViewFormCheckbox, + ABViewFormConnect, + ABViewFormCustom, + ABViewFormDatepicker, + ABViewFormItem, + ABViewFormJson, + ABViewFormNumber, + ABViewFormReadonly, + ABViewFormSelectMultiple, + ABViewFormSelectSingle, + ABViewFormTree, + ABViewFormTextbox, + }); + + views.forEach((v) => { + v.getPluginKey = () => v.common().key; + v.getPluginType = () => "view"; + }); + + class ABViewForm extends ABViewFormBase { + constructor(values, application, parent, defaultValues) { + super(values, application, parent, defaultValues); + this._callbacks = { + onBeforeSaveData: () => true, + }; + } + + static getPluginKey() { + return this.common().key; + } + + static getPluginType() { + return "view"; + } + + static newInstance(application, parent) { + return application.viewNew( + { key: this.common().key, plugin_key: this.getPluginKey() }, + parent + ); + } + + toObj() { + const result = super.toObj(); + result.plugin_key = this.constructor.getPluginKey(); + return result; + } + + component(parentId) { + return new ABAbviewformComponent(this, parentId); + } + + refreshDefaultButton(ids) { + let defaultButton = this.views( + (v) => v instanceof ABViewFormButton && v.settings.isDefault + )[0]; + if (defaultButton == null) { + defaultButton = ABViewFormButton.newInstance( + this.application, + this + ); + defaultButton.settings.isDefault = true; + } else { + this._views = this.views((v) => v.id != defaultButton.id); + } + let yList = this.views().map((v) => (v.position.y || 0) + 1); + yList.push(this._views.length || 0); + yList.push($$(ids.fields).length || 0); + let posY = Math.max(...yList); + defaultButton.position.y = posY; + this._views.push(defaultButton); + return defaultButton; + } + + getFormValues(formView, obj, dc, dcLink) { + const visibleFields = ["id"]; + formView.getValues(function (obj) { + visibleFields.push(obj.config.name); + }); + const allVals = formView.getValues(); + const formVals = {}; + visibleFields.forEach((val) => { + formVals[val] = allVals[val]; + }); + this.fieldComponents( + (comp) => + comp instanceof ABViewFormCustom || + comp instanceof ABViewFormConnect || + comp instanceof ABViewFormDatepicker || + comp instanceof ABViewFormSelectMultiple || + (comp instanceof ABViewFormJson && + comp.settings.type == "filter") + ).forEach((f) => { + const vComponent = this.viewComponents[f.id]; + if (vComponent == null) return; + const field = f.field(); + if (field) { + const getValue = + vComponent.getValue ?? vComponent.logic.getValue; + if (getValue) + formVals[field.columnName] = getValue.call( + vComponent, + formVals + ); + } + }); + obj.connectFields().forEach((f) => { + if ( + visibleFields.indexOf(f.columnName) == -1 && + formVals[f.columnName] + ) { + delete formVals[f.columnName]; + delete formVals[f.relationName()]; + } + }); + for (const prop in formVals) { + if (formVals[prop] == null || formVals[prop].length == 0) + formVals[prop] = ""; + } + let linkValues; + if (dcLink) { + linkValues = dcLink.getCursor(); + } + if (linkValues) { + const objectLink = dcLink.datasource; + const connectFields = obj.connectFields(); + connectFields.forEach((f) => { + const formFieldCom = this.fieldComponents( + (fComp) => fComp?.field?.()?.id === f?.id + ); + if ( + objectLink.id == f.settings.linkObject && + formFieldCom.length < 1 && + formVals[f.columnName] === undefined + ) { + const linkColName = f.indexField + ? f.indexField.columnName + : objectLink.PK(); + formVals[f.columnName] = {}; + formVals[f.columnName][linkColName] = + linkValues[linkColName] ?? linkValues.id; + } + }); + } + const cursorFormVals = Object.assign(dc.getCursor() ?? {}, formVals); + obj.fields((f) => f.key == "calculate" || f.key == "formula").forEach( + (f) => { + if (formVals[f.columnName] == null) { + let reCalculate = true; + if ( + f.key == "formula" && + f.settings?.where?.rules?.length > 0 + ) { + reCalculate = false; + } + formVals[f.columnName] = f.format( + cursorFormVals, + reCalculate + ); + } + } + ); + if (allVals.translations?.length > 0) + formVals.translations = allVals.translations; + obj.formCleanValues(formVals); + return formVals; + } + + validateData($formView, object, formVals) { + let list = ""; + const requiredFields = this.fieldComponents( + (fComp) => + fComp?.field?.().settings?.required == true || + fComp?.settings?.required == true + ).map((fComp) => fComp.field()); + const validator = object.isValidData(formVals); + let isValid = validator.pass(); + $formView.validate(); + const fixInvalidMessageUI = (col) => { + const $forminput = $formView.elements[col]; + if (!$forminput) return; + const height = $forminput.$height; + if (height < 56) { + $forminput.define("height", 60); + $forminput.resize(); + } + const domInvalidMessage = $forminput.$view.getElementsByClassName( + "webix_inp_bottom_label" + )[0]; + if (!domInvalidMessage?.style["margin-left"]) { + domInvalidMessage.style.marginLeft = `${ + this.settings.labelWidth ?? + ABViewFormBase.defaultValues().labelWidth + }px`; + } + }; + requiredFields.forEach((f) => { + if (!f) return; + const fieldVal = formVals[f.columnName]; + if (fieldVal == "" || fieldVal == null || fieldVal.length < 1) { + $formView.markInvalid( + f.columnName, + this.AB.Label()("This is a required field.") + ); + list += `
  • ${this.AB.Label()("Missing Required Field")} ${ + f.columnName + }
  • `; + isValid = false; + fixInvalidMessageUI(f.columnName); + } + }); + if (!isValid) { + const saveButton = $formView.queryView({ + view: "button", + type: "form", + }); + if (validator?.errors?.length) { + validator.errors.forEach((err) => { + $formView.markInvalid(err.name, err.message); + list += `
  • ${err.name}: ${err.message}
  • `; + fixInvalidMessageUI(err.name); + }); + saveButton?.disable(); + } else { + saveButton?.enable(); + } + } + if (list) { + this.AB.Webix.alert({ + type: "alert-error", + title: this.AB.Label()("Problems Saving"), + width: 400, + text: ``, + }); + } + return isValid; + } + + async recordRulesReady() { + return this.RecordRule.rulesReady(); + } + + async saveData($formView) { + if (!this._callbacks?.onBeforeSaveData?.()) return; + $formView.clearValidation(); + const dv = this.datacollection; + if (dv == null) return; + const obj = dv.datasource; + if (obj == null) return; + $formView.showProgress?.({ type: "icon" }); + const formVals = this.getFormValues( + $formView, + obj, + dv, + dv.datacollectionLink + ); + const formReady = (newFormVals) => { + if (dv) { + if (this.settings.clearOnSave) { + dv.setCursor(null); + $formView.clear(); + } else { + if (newFormVals && newFormVals.id) + dv.setCursor(newFormVals.id); + } + } + $formView.hideProgress?.(); + if (newFormVals) this.emit("saved", newFormVals); + }; + const formError = (err) => { + const $saveButton = $formView.queryView({ + view: "button", + type: "form", + }); + if (err) { + if (err.invalidAttributes) { + for (const attr in err.invalidAttributes) { + let invalidAttrs = err.invalidAttributes[attr]; + if (invalidAttrs && invalidAttrs[0]) + invalidAttrs = invalidAttrs[0]; + $formView.markInvalid(attr, invalidAttrs.message); + } + } else if (err.sqlMessage) { + this.AB.Webix.message({ + text: err.sqlMessage, + type: "error", + }); + } else { + this.AB.Webix.message({ + text: this.AB.Label()("System could not save your data"), + type: "error", + }); + this.AB.notify.developer(err, { + message: "Could not save your data", + view: this.toObj(), + }); + } + } + $saveButton?.enable(); + $formView?.hideProgress?.(); + }; + await this.loadDcDataOfRecordRules(); + await this.recordRulesReady(); + this.doRecordRulesPre(formVals); + if (!this.validateData($formView, obj, formVals)) { + $formView.hideProgress?.(); + return; + } + let newFormVals; + try { + newFormVals = await this.submitValues(formVals); + } catch (err) { + formError(err.data); + return; + } + try { + await this.doRecordRules(newFormVals); + } catch (err) { + this.AB.notify.developer(err, { + message: "Error processing Record Rules.", + view: this.toObj(), + newFormVals: newFormVals, + }); + } + try { + this.doSubmitRules(newFormVals); + } catch (errs) { + this.AB.notify.developer(errs, { + message: "Error processing Submit Rules.", + view: this.toObj(), + newFormVals: newFormVals, + }); + } + formReady(newFormVals); + return newFormVals; + } + + focusOnFirst() { + let topPosition = 0; + let topPositionId = ""; + this.views().forEach((item) => { + if (item.key == "textbox" || item.key == "numberbox") { + if (item.position.y == topPosition) { + topPositionId = item.id; + } + } + }); + let childComponent = this.viewComponents[topPositionId]; + if (childComponent && $$(childComponent.ui.id)) { + $$(childComponent.ui.id).focus(); + } + } + + async loadDcDataOfRecordRules() { + const tasks = []; + (this.settings?.recordRules ?? []).forEach((rule) => { + (rule?.actionSettings?.valueRules?.fieldOperations ?? []).forEach( + (op) => { + if (op.valueType !== "exist") return; + const pullDataDC = this.AB.datacollectionByID(op.value); + if ( + pullDataDC?.dataStatus === + pullDataDC.dataStatusFlag.notInitial + ) + tasks.push(pullDataDC.loadData()); + } + ); + }); + await Promise.all(tasks); + return true; + } + + get viewComponents() { + const superComponent = this.superComponent(); + return superComponent.viewComponents; + } + + warningsEval() { + super.warningsEval(); + let DC = this.datacollection; + if (!DC) { + this.warningsMessage( + `can't resolve it's datacollection[${this.settings.dataviewID}]` + ); + } + } + + async submitValues(formVals) { + const model = this.datacollection?.model; + if (model == null) return; + if (formVals.id) { + return await model.update(formVals.id, formVals); + } else { + return await model.create(formVals); + } + } + + async deleteData($formView) { + const dc = this.datacollection; + if (dc == null) return; + const model = dc.model; + if (model == null) return; + const formVals = $formView.getValues(); + if (formVals?.id) { + const result = await model.delete(formVals.id); + if (result) { + dc.setCursor(null); + $formView.clear(); + } + return result; + } + } + } + + if (AB && AB.Class) { + AB.Class.ABViewForm = ABViewForm; + } + + const ABViewFormURL = FNAbviewformURL({ + ABAbviewformComponent, + ABViewForm, + }); + // Ensure ABViewFormURL has the necessary plugin methods + ABViewFormURL.getPluginKey = () => ABViewFormURL.common().key; + ABViewFormURL.getPluginType = () => "view"; + + return [ABViewForm, ABViewFormURL, ...views]; +} diff --git a/AppBuilder/platform/plugins/included/view_form/FNAbviewformButton.js b/AppBuilder/platform/plugins/included/view_form/FNAbviewformButton.js new file mode 100644 index 00000000..6d056eab --- /dev/null +++ b/AppBuilder/platform/plugins/included/view_form/FNAbviewformButton.js @@ -0,0 +1,33 @@ +import FNAbviewformButtonComponent from "./viewComponent/FNAbviewformButtonComponent.js"; +import FNAbviewformButtonCoreFactory from "./core/ABViewFormButtonCore.js"; + +export default function FNAbviewformButton({ + ABViewComponentPlugin, + ABViewPlugin, + ABViewFormItemComponent, +}) { + const ABViewFormButtonCore = FNAbviewformButtonCoreFactory(ABViewPlugin); + + if (!ABViewFormItemComponent) { + const error = new Error( + "ABViewFormButton: ABViewFormItemComponent is undefined" + ); + console.error(error); + return null; + } + + const ABAbviewformButtonComponent = FNAbviewformButtonComponent({ + ABViewFormItemComponent, + }); + + return class ABViewFormButton extends ABViewFormButtonCore { + /** + * @method component() + * return a UI component based upon this view. + * @return {obj} UI component + */ + component() { + return new ABAbviewformButtonComponent(this); + } + }; +} diff --git a/AppBuilder/platform/plugins/included/view_form/FNAbviewformCheckbox.js b/AppBuilder/platform/plugins/included/view_form/FNAbviewformCheckbox.js new file mode 100644 index 00000000..7086ccc6 --- /dev/null +++ b/AppBuilder/platform/plugins/included/view_form/FNAbviewformCheckbox.js @@ -0,0 +1,25 @@ +import FNAbviewformCheckboxComponent from "./viewComponent/FNAbviewformCheckboxComponent.js"; +import FNAbviewformCheckboxCoreFactory from "./core/ABViewFormCheckboxCore.js"; + +export default function FNAbviewformCheckbox({ + ABViewComponentPlugin, + ABViewFormItemComponent, + ABViewFormItem, +}) { + const ABViewFormCheckboxCore = + FNAbviewformCheckboxCoreFactory(ABViewFormItem); + const ABAbviewformCheckboxComponent = FNAbviewformCheckboxComponent({ + ABViewFormItemComponent, + }); + + return class ABViewFormCheckbox extends ABViewFormCheckboxCore { + /** + * @method component() + * return a UI component based upon this view. + * @return {obj} UI component + */ + component() { + return new ABAbviewformCheckboxComponent(this); + } + }; +} diff --git a/AppBuilder/platform/plugins/included/view_form/FNAbviewformComponent.js b/AppBuilder/platform/plugins/included/view_form/FNAbviewformComponent.js new file mode 100644 index 00000000..9285fb0e --- /dev/null +++ b/AppBuilder/platform/plugins/included/view_form/FNAbviewformComponent.js @@ -0,0 +1,605 @@ +export default function FNAbviewformComponent({ + /*AB,*/ + ABViewComponentPlugin, + ABViewFormButton, + ABViewFormCheckbox, + ABViewFormConnect, + ABViewFormCustom, + ABViewFormDatepicker, + ABViewFormItem, + ABViewFormJson, + ABViewFormNumber, + ABViewFormReadonly, + ABViewFormSelectMultiple, + ABViewFormSelectSingle, + ABViewFormTextbox, + ABViewFormTree, +}) { + async function timeout(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); + } + + const fieldValidations = []; + + return class ABAbviewformComponent extends ABViewComponentPlugin { + constructor(baseView, idBase, ids) { + super( + baseView, + idBase || `ABViewForm_${baseView.id}`, + Object.assign( + { + form: "", + + layout: "", + filterComplex: "", + }, + ids + ) + ); + + this.timerId = null; + this._showed = false; + } + + ui() { + console.log("ABAbviewformComponent.ui()", this.view.key, this.view.id); + const baseView = this.view; + const superComponent = baseView.superComponent(); + const rows = superComponent.ui().rows ?? []; + const fieldValidationsHolder = this.uiValidationHolder(); + const _ui = super.ui([ + { + id: this.ids.form, + view: "form", + abid: baseView.id, + rows: rows.concat(fieldValidationsHolder), + }, + ]); + + delete _ui.type; + + return _ui; + } + + uiValidationHolder() { + const result = [ + { + hidden: true, + rows: [], + }, + ]; + + // NOTE: this._currentObject can be set in the KanBan Side Panel + const baseView = this.view; + const object = + baseView.datacollection?.datasource ?? baseView._currentObject; + + if (!object) return result; + + const validationUI = []; + const existsFields = baseView.fieldComponents(); + + object + // Pull fields that have validation rules + .fields((f) => f?.settings?.validationRules) + .forEach((f) => { + const view = existsFields.find( + (com) => f.id === com.settings.fieldId + ); + if (!view) return; + + // parse the rules because they were stored as a string + // check if rules are still a string...if so lets parse them + if (typeof f.settings.validationRules === "string") { + f.settings.validationRules = JSON.parse( + f.settings.validationRules + ); + } + + // there could be more than one so lets loop through and build the UI + f.settings.validationRules.forEach((rule, indx) => { + const Filter = this.AB.filterComplexNew( + `${f.columnName}_${indx}` + ); + // add the new ui to an array so we can add them all at the same time + if (typeof Filter.ui === "function") { + validationUI.push(Filter.ui()); + } else { + // Legacy v1 method: + validationUI.push(Filter.ui); + } + + // store the filter's info so we can assign values and settings after the ui is rendered + fieldValidations.push({ + filter: Filter, + view: Filter.ids.querybuilder, + columnName: f.columnName, + validationRules: rule.rules, + invalidMessage: rule.invalidMessage, + }); + }); + }); + + result[0].rows = validationUI; + + return result; + } + + async init(AB, accessLevel, options = {}) { + await super.init(AB); + + this.view.superComponent().init(AB, accessLevel, options); + + this.initCallbacks(options); + this.initEvents(); + this.initValidationRules(); + + const abWebix = this.AB.Webix; + const $form = $$(this.ids.form); + + if ($form) { + abWebix.extend($form, abWebix.ProgressBar); + } + + if (accessLevel < 2) $form.disable(); + } + + initCallbacks(options = {}) { + // ? We need to determine from these options whether to clear on load? + if (options?.clearOnLoad) { + // does this need to be a function? + this.view.settings.clearOnLoad = options.clearOnLoad(); + } + // Q: Should we use emit the event instead ? + const baseView = this.view; + + if (options.onBeforeSaveData) + baseView._callbacks.onBeforeSaveData = options.onBeforeSaveData; + else baseView._callbacks.onBeforeSaveData = () => true; + } + + initEvents() { + // bind a data collection to form component + const dc = this.datacollection; + + if (!dc) return; + + // listen DC events + ["changeCursor", "cursorStale"].forEach((key) => { + this.eventAdd({ + emitter: dc, + eventName: key, + listener: (rowData) => { + const baseView = this.view; + const linkViaOneConnection = baseView.fieldComponents( + (comp) => comp instanceof ABViewFormConnect + ); + // clear previous xxx->one selections and add new from + // cursor change + linkViaOneConnection.forEach((f) => { + const field = f.field(); + if ( + field?.settings?.linkViaType == "one" && + field?.linkViaOneValues + ) { + delete field.linkViaOneValues; + const relationVals = + rowData?.[field.relationName()] ?? + rowData?.[field.columnName]; + if (relationVals) { + if (Array.isArray(relationVals)) { + const valArray = []; + relationVals.forEach((v) => { + valArray.push( + field.getRelationValue(v, { + forUpdate: true, + }) + ); + }); + field.linkViaOneValues = valArray.join(","); + } else { + field.linkViaOneValues = field.getRelationValue( + relationVals, + { forUpdate: true } + ); + } + } + } + }); + + this.displayData(rowData); + }, + }); + }); + + const ids = this.ids; + + this.eventAdd({ + emitter: dc, + eventName: "initializingData", + listener: () => { + const $form = $$(ids.form); + + if ($form) { + $form.disable(); + + $form.showProgress?.({ type: "icon" }); + } + }, + }); + + this.eventAdd({ + emitter: dc, + eventName: "initializedData", + listener: () => { + const $form = $$(ids.form); + + if ($form) { + $form.enable(); + + $form.hideProgress?.(); + } + }, + }); + + // I think this case is currently handled by the DC.[changeCursor, cursorStale] + // events: + // this.eventAdd({ + // emitter: dc, + // eventName: "ab.datacollection.update", + // listener: (msg, data) => { + // if (!data?.objectId) return; + + // const object = dc.datasource; + + // if (!object) return; + + // if ( + // object.id === data.objectId || + // object.fields((f) => f.settings.linkObject === data.objectId) + // .length > 0 + // ) { + // const currData = dc.getCursor(); + + // if (currData) this.displayData(currData); + // } + // }, + // }); + + // bind the cursor event of the parent DC + const linkDv = dc.datacollectionLink; + + if (linkDv) + // update the value of link field when data of the parent dc is changed + ["changeCursor", "cursorStale"].forEach((key) => { + this.eventAdd({ + emitter: linkDv, + eventName: key, + listener: (rowData) => { + this.displayParentData(rowData); + }, + }); + }); + } + + initValidationRules() { + const dc = this.datacollection; + + if (!dc) return; + + if (!fieldValidations.length) return; + + // we need to store the rules for use later so lets build a container array + const complexValidations = []; + + fieldValidations.forEach((f) => { + // init each ui to have the properties (app and fields) of the object we are editing + f.filter.applicationLoad?.(dc.datasource.application); // depreciated. + f.filter.fieldsLoad(dc.datasource.fields()); + // now we can set the value because the fields are properly initialized + f.filter.setValue(f.validationRules); + + // if there are validation rules present we need to store them in a lookup hash + // so multiple rules can be stored on a single field + if (!Array.isArray(complexValidations[f.columnName])) + complexValidations[f.columnName] = []; + + // now we can push the rules into the hash + // what happens if $$(f.view) isn't present? + if ($$(f.view)) { + complexValidations[f.columnName].push({ + filters: $$(f.view).getFilterFunction(), + // values: $$(ids.form).getValues(), + invalidMessage: f.invalidMessage, + }); + } + }); + + const ids = this.ids; + + // use the lookup to build the validation rules + Object.keys(complexValidations).forEach((key) => { + // get our field that has validation rules + const formField = $$(ids.form).queryView({ + name: key, + }); + + if (!formField) return; + + // store the rules in a data param to be used later + formField.$view.complexValidations = complexValidations[key]; + // define validation rules + formField.define("validate", function (nval, oval, field) { + // get field now that we are validating + const fieldValidating = $$(ids.form)?.queryView({ + name: field, + }); + if (!fieldValidating) return true; + + // default valid is true + let isValid = true; + + // check each rule that was stored previously on the element + fieldValidating.$view.complexValidations.forEach((filter) => { + const object = dc.datasource; + const data = this.getValues(); + + // convert rowData from { colName : data } to { id : data } + const newData = {}; + + (object.fields() || []).forEach((field) => { + newData[field.id] = data[field.columnName]; + }); + + // for the case of "this_object" conditions: + if (data.uuid) newData["this_object"] = data.uuid; + + // use helper funtion to check if valid + const ruleValid = filter.filters(newData); + + // if invalid we need to tell the field + if (!ruleValid) { + isValid = false; + // we also need to define an error message + fieldValidating.define( + "invalidMessage", + filter.invalidMessage + ); + } + }); + + return isValid; + }); + + formField.refresh(); + }); + } + + async onShow(data) { + this.saveButton?.disable(); + + this._showed = true; + + const baseView = this.view; + + // call .onShow in the base component + const superComponent = baseView.superComponent(); + await superComponent.onShow(); + + const $form = $$(this.ids.form); + const dc = this.datacollection; + + if (dc) { + // clear current cursor on load + // if (this.settings.clearOnLoad || _logic.callbacks.clearOnLoad() ) { + const settings = this.settings; + + if (settings.clearOnLoad) { + dc.setCursor(null); + } + + // pull data of current cursor + // await dc.waitReady(); + const rowData = dc.getCursor(); + + if ($form) dc.bind($form); + + // do this for the initial form display so we can see defaults + await this.displayData(rowData); + } + // show blank data in the form + else await this.displayData(data ?? {}); + + //Focus on first focusable component + this.focusOnFirst(); + + if ($form) $form.adjust(); + + // Load data of DCs that are use in record rules here + // no need to wait until they are done. (Let the save button enable) + // It will be re-check again when saving. + baseView.loadDcDataOfRecordRules(); + + this.saveButton?.enable(); + } + + async displayData(rowData) { + // If setTimeout is already scheduled, no need to do anything + if (this.timerId) return; + + this.timerId = true; + await timeout(80); + this.timerId = null; + + const baseView = this.view; + const customFields = baseView.fieldComponents( + (comp) => + comp instanceof ABViewFormCustom || + // rich text + (comp instanceof ABViewFormTextbox && + comp.settings.type === "rich") || + (comp instanceof ABViewFormJson && + comp.settings.type === "filter") + ); + + const normalFields = baseView.fieldComponents( + (comp) => + comp instanceof ABViewFormItem && + !(comp instanceof ABViewFormCustom) + ); + + // Set default values + if (!rowData) { + customFields.forEach((f) => { + const field = f.field(); + if (!field) return; + + const comp = baseView.viewComponents[f.id]; + if (!comp) return; + + // var colName = field.columnName; + if (this._showed) comp?.onShow?.(); + + // set value to each components + const defaultRowData = {}; + + field.defaultValue(defaultRowData); + field.setValue($$(comp.ids.formItem), defaultRowData); + + comp?.refresh?.(defaultRowData); + }); + + normalFields.forEach((f) => { + if (f.key === "button") return; + + const field = f.field(); + if (!field) return; + + const comp = baseView.viewComponents[f.id]; + if (!comp) return; + + const colName = field.columnName; + + // set value to each components + const values = {}; + + field.defaultValue(values); + $$(comp.ids.formItem)?.setValue(values[colName] ?? ""); + }); + + // select parent data to default value + const dc = this.datacollection; + const linkDv = dc.datacollectionLink; + + if (linkDv) { + const parentData = linkDv.getCursor(); + + this.displayParentData(parentData); + } + } + + // Populate value to custom fields + else { + customFields.forEach((f) => { + const comp = baseView.viewComponents[f.id]; + if (!comp) return; + + if (this._showed) comp?.onShow?.(); + + // set value to each components + f?.field()?.setValue($$(comp.ids.formItem), rowData); + + comp?.refresh?.(rowData); + }); + + normalFields.forEach((f) => { + if (f.key === "button") return; + + const field = f.field(); + if (!field) return; + + const comp = baseView.viewComponents[f.id]; + if (!comp) return; + // + if (f.key === "datepicker") { + // Not sure why, but the local format isn't applied correctly + // without a timeout here + setTimeout(() => { + field.setValue($$(comp.ids.formItem), rowData); + }, 200); + return; + } + + field.setValue($$(comp.ids.formItem), rowData); + }); + } + + this.timerId = null; + } + + displayParentData(rowData) { + const dc = this.datacollection; + + // If the cursor is selected, then it will not update value of the parent field + const currCursor = dc.getCursor(); + if (currCursor) return; + + const relationField = dc.fieldLink; + if (!relationField) return; + + const baseView = this.view; + // Pull a component of relation field + const relationFieldCom = baseView.fieldComponents((comp) => { + if (!(comp instanceof ABViewFormItem)) return false; + + return comp.field()?.id === relationField.id; + })[0]; + if (!relationFieldCom) return; + + const relationFieldView = baseView.viewComponents[relationFieldCom.id]; + if (!relationFieldView) return; + + const $relationFieldView = $$(relationFieldView.ids.formItem), + relationName = relationField.relationName(); + + // pull data of parent's dc + const formData = {}; + + formData[relationName] = rowData; + + // set data of parent to default value + relationField.setValue($relationFieldView, formData); + } + + detatch() { + // TODO: remove any handlers we have attached. + } + + focusOnFirst() { + const baseView = this.view; + + let topPosition = 0; + let topPositionId = ""; + + baseView.views().forEach((item) => { + if (item.key === "textbox" || item.key === "numberbox") + if (item.position.y === topPosition) { + topPosition = item.position.y; + topPositionId = item.id; + } + }); + + const childComponent = baseView.viewComponents[topPositionId]; + + if (childComponent && $$(childComponent.ids.formItem)) + $$(childComponent.ids.formItem).focus(); + } + + get saveButton() { + return $$(this.ids.form)?.queryView({ + view: "button", + type: "form", + }); + } + }; +} diff --git a/AppBuilder/platform/plugins/included/view_form/FNAbviewformConnect.js b/AppBuilder/platform/plugins/included/view_form/FNAbviewformConnect.js new file mode 100644 index 00000000..ca5e202a --- /dev/null +++ b/AppBuilder/platform/plugins/included/view_form/FNAbviewformConnect.js @@ -0,0 +1,90 @@ +import FNAbviewformConnectComponent from "./viewComponent/FNAbviewformConnectComponent.js"; +import FNAbviewformConnectCoreFactory from "./core/ABViewFormConnectCore.js"; + +export default function FNAbviewformConnect({ + ABViewComponentPlugin, + ABViewFormItemComponent, + ABViewFormItem, + ABViewPropertyAddPage, + ABViewPropertyEditPage, +}) { + const ABViewFormConnectCore = FNAbviewformConnectCoreFactory(ABViewFormItem); + const ABAbviewformConnectComponent = FNAbviewformConnectComponent({ + ABViewFormItemComponent, + ABViewPropertyAddPage, + ABViewPropertyEditPage, + }); + + return class ABViewFormConnect extends ABViewFormConnectCore { + /** + * @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); + + // Set filter value + this.__filterComponent = this.AB.filterComplexNew( + `${this.id}__filterComponent` + ); + this.__filterComponent.fieldsLoad( + this.datasource ? this.datasource.fields() : [], + this.datasource ? this.datasource : null + ); + + this.__filterComponent.setValue( + this.settings.filterConditions ?? + this.constructor.defaultValues().filterConditions + ); + } + + /// + /// Instance Methods + /// + + /** + * @method fromValues() + * + * initialze this object with the given set of values. + * @param {obj} values + */ + fromValues(values) { + super.fromValues(values); + + this.addPageTool.fromSettings(this.settings); + this.editPageTool.fromSettings(this.settings); + } + + static get addPageProperty() { + return ABViewPropertyAddPage.propertyComponent(this.App, this.idBase); + } + + static get editPageProperty() { + return ABViewPropertyEditPage.propertyComponent(this.App, this.idBase); + } + + /** + * @method component() + * return a UI component based upon this view. + * @return {obj} UI component + */ + component() { + return new ABAbviewformConnectComponent(this); + } + + get addPageTool() { + if (this.__addPageTool == null) + this.__addPageTool = new ABViewPropertyAddPage(); + + return this.__addPageTool; + } + + get editPageTool() { + if (this.__editPageTool == null) + this.__editPageTool = new ABViewPropertyEditPage(); + + return this.__editPageTool; + } + }; +} diff --git a/AppBuilder/platform/plugins/included/view_form/FNAbviewformCustom.js b/AppBuilder/platform/plugins/included/view_form/FNAbviewformCustom.js new file mode 100644 index 00000000..ecda7c10 --- /dev/null +++ b/AppBuilder/platform/plugins/included/view_form/FNAbviewformCustom.js @@ -0,0 +1,28 @@ +import FNAbviewformCustomComponent from "./viewComponent/FNAbviewformCustomComponent.js"; +import FNAbviewformCustomCoreFactory from "./core/ABViewFormCustomCore.js"; + +export default function FNAbviewformCustom({ + ABViewComponentPlugin, + ABViewFormItemComponent, + ABViewFormItem, + ABFieldImage, + FocusableTemplate, +}) { + const ABViewFormCustomCore = FNAbviewformCustomCoreFactory(ABViewFormItem); + const ABAbviewformCustomComponent = FNAbviewformCustomComponent({ + ABViewFormItemComponent, + ABFieldImage, + FocusableTemplate, + }); + + return class ABViewFormCustom extends ABViewFormCustomCore { + /** + * @method component() + * return a UI component based upon this view. + * @return {obj} UI component + */ + component() { + return new ABAbviewformCustomComponent(this); + } + }; +} diff --git a/AppBuilder/platform/plugins/included/view_form/FNAbviewformDatepicker.js b/AppBuilder/platform/plugins/included/view_form/FNAbviewformDatepicker.js new file mode 100644 index 00000000..baed3b89 --- /dev/null +++ b/AppBuilder/platform/plugins/included/view_form/FNAbviewformDatepicker.js @@ -0,0 +1,25 @@ +import FNAbviewformDatepickerComponent from "./viewComponent/FNAbviewformDatepickerComponent.js"; +import FNAbviewformDatepickerCoreFactory from "./core/ABViewFormDatepickerCore.js"; + +export default function FNAbviewformDatepicker({ + ABViewComponentPlugin, + ABViewFormItemComponent, + ABViewFormItem, +}) { + const ABViewFormDatepickerCore = + FNAbviewformDatepickerCoreFactory(ABViewFormItem); + const ABAbviewformDatepickerComponent = FNAbviewformDatepickerComponent({ + ABViewFormItemComponent, + }); + + return class ABViewFormDatepicker extends ABViewFormDatepickerCore { + /** + * @method component() + * return a UI component based upon this view. + * @return {obj} UI component + */ + component() { + return new ABAbviewformDatepickerComponent(this); + } + }; +} diff --git a/AppBuilder/platform/plugins/included/view_form/FNAbviewformItem.js b/AppBuilder/platform/plugins/included/view_form/FNAbviewformItem.js new file mode 100644 index 00000000..1860cd88 --- /dev/null +++ b/AppBuilder/platform/plugins/included/view_form/FNAbviewformItem.js @@ -0,0 +1,45 @@ +import FNAbviewformItemComponent from "./viewComponent/FNAbviewformItemComponent.js"; +import FNAbviewformItemCoreFactory from "./core/ABViewFormItemCore.js"; + +export default function FNAbviewformItem({ + ABViewComponentPlugin, + ABViewPlugin, +}) { + const ABViewFormItemCore = FNAbviewformItemCoreFactory(ABViewPlugin); + const ABAbviewformItemComponent = FNAbviewformItemComponent({ + ABViewComponentPlugin, + }); + + const ABViewFormItem = class ABViewFormItem extends ABViewFormItemCore { + /** + * @method component() + * return a UI component based upon this view. + * @return {obj} UI component + */ + component() { + return new ABAbviewformItemComponent(this); + } + + /** + * @method parentFormUniqueID + * return a unique ID based upon the closest form object this component is on. + * @param {string} key The basic id string we will try to make unique + * @return {string} + */ + parentFormUniqueID(key) { + var form = this.parentFormComponent(); + var uniqueInstanceID; + if (form) { + uniqueInstanceID = form.uniqueInstanceID; + } else { + uniqueInstanceID = webix.uid(); + } + + return key + uniqueInstanceID; + } + }; + + ABViewFormItem.ABViewFormItemComponent = ABAbviewformItemComponent; + + return ABViewFormItem; +} diff --git a/AppBuilder/platform/plugins/included/view_form/FNAbviewformJson.js b/AppBuilder/platform/plugins/included/view_form/FNAbviewformJson.js new file mode 100644 index 00000000..3ca4c4e2 --- /dev/null +++ b/AppBuilder/platform/plugins/included/view_form/FNAbviewformJson.js @@ -0,0 +1,24 @@ +import FNAbviewformJsonComponent from "./viewComponent/FNAbviewformJsonComponent.js"; +import FNAbviewformJsonCoreFactory from "./core/ABViewFormJsonCore.js"; + +export default function FNAbviewformJson({ + ABViewComponentPlugin, + ABViewFormItemComponent, + ABViewFormItem, +}) { + const ABViewFormJsonCore = FNAbviewformJsonCoreFactory(ABViewFormItem); + const ABAbviewformJsonComponent = FNAbviewformJsonComponent({ + ABViewFormItemComponent, + }); + + return class ABViewFormJson extends ABViewFormJsonCore { + /** + * @method component() + * return a UI component based upon this view. + * @return {obj} UI component + */ + component() { + return new ABAbviewformJsonComponent(this); + } + }; +} diff --git a/AppBuilder/platform/plugins/included/view_form/FNAbviewformNumber.js b/AppBuilder/platform/plugins/included/view_form/FNAbviewformNumber.js new file mode 100644 index 00000000..7113fab5 --- /dev/null +++ b/AppBuilder/platform/plugins/included/view_form/FNAbviewformNumber.js @@ -0,0 +1,24 @@ +import FNAbviewformNumberComponent from "./viewComponent/FNAbviewformNumberComponent.js"; +import FNAbviewformNumberCoreFactory from "./core/ABViewFormNumberCore.js"; + +export default function FNAbviewformNumber({ + ABViewComponentPlugin, + ABViewFormItemComponent, + ABViewFormItem, +}) { + const ABViewFormNumberCore = FNAbviewformNumberCoreFactory(ABViewFormItem); + const ABAbviewformNumberComponent = FNAbviewformNumberComponent({ + ABViewFormItemComponent, + }); + + return class ABViewFormNumber extends ABViewFormNumberCore { + /** + * @method component() + * return a UI component based upon this view. + * @return {obj} UI component + */ + component() { + return new ABAbviewformNumberComponent(this); + } + }; +} diff --git a/AppBuilder/platform/plugins/included/view_form/FNAbviewformReadonly.js b/AppBuilder/platform/plugins/included/view_form/FNAbviewformReadonly.js new file mode 100644 index 00000000..0945242e --- /dev/null +++ b/AppBuilder/platform/plugins/included/view_form/FNAbviewformReadonly.js @@ -0,0 +1,29 @@ +import FNAbviewformReadonlyComponent from "./viewComponent/FNAbviewformReadonlyComponent.js"; +import FNAbviewformReadonlyCoreFactory from "./core/ABViewFormReadonlyCore.js"; + +export default function FNAbviewformReadonly({ + ABViewComponentPlugin, + ABViewFormItemComponent, + ABViewFormCustom, + ABFieldImage, + FocusableTemplate, +}) { + const ABViewFormReadonlyCore = + FNAbviewformReadonlyCoreFactory(ABViewFormCustom); + const ABAbviewformReadonlyComponent = FNAbviewformReadonlyComponent({ + ABViewFormItemComponent, + ABFieldImage, + FocusableTemplate, + }); + + return class ABViewFormReadonly extends ABViewFormReadonlyCore { + /** + * @method component() + * return a UI component based upon this view. + * @return {obj} UI component + */ + component() { + return new ABAbviewformReadonlyComponent(this); + } + }; +} diff --git a/AppBuilder/platform/plugins/included/view_form/FNAbviewformSelectMultiple.js b/AppBuilder/platform/plugins/included/view_form/FNAbviewformSelectMultiple.js new file mode 100644 index 00000000..766b91d3 --- /dev/null +++ b/AppBuilder/platform/plugins/included/view_form/FNAbviewformSelectMultiple.js @@ -0,0 +1,27 @@ +import FNAbviewformSelectMultipleComponent from "./viewComponent/FNAbviewformSelectMultipleComponent.js"; +import FNAbviewformSelectMultipleCoreFactory from "./core/ABViewFormSelectMultipleCore.js"; + +export default function FNAbviewformSelectMultiple({ + ABViewComponentPlugin, + ABViewFormItemComponent, + ABViewFormItem, +}) { + const ABViewFormSelectMultipleCore = + FNAbviewformSelectMultipleCoreFactory(ABViewFormItem); + + const ABAbviewformSelectMultipleComponent = + FNAbviewformSelectMultipleComponent({ + ABViewFormItemComponent, + }); + + return class ABViewFormSelectMultiple extends ABViewFormSelectMultipleCore { + /** + * @method component() + * return a UI component based upon this view. + * @return {obj} UI component + */ + component() { + return new ABAbviewformSelectMultipleComponent(this); + } + }; +} diff --git a/AppBuilder/platform/plugins/included/view_form/FNAbviewformSelectSingle.js b/AppBuilder/platform/plugins/included/view_form/FNAbviewformSelectSingle.js new file mode 100644 index 00000000..36edae18 --- /dev/null +++ b/AppBuilder/platform/plugins/included/view_form/FNAbviewformSelectSingle.js @@ -0,0 +1,25 @@ +import FNAbviewformSelectSingleComponent from "./viewComponent/FNAbviewformSelectSingleComponent.js"; +import FNAbviewformSelectSingleCoreFactory from "./core/ABViewFormSelectSingleCore.js"; + +export default function FNAbviewformSelectSingle({ + ABViewComponentPlugin, + ABViewFormItemComponent, + ABViewFormItem, +}) { + const ABViewFormSelectSingleCore = + FNAbviewformSelectSingleCoreFactory(ABViewFormItem); + const ABAbviewformSelectSingleComponent = FNAbviewformSelectSingleComponent({ + ABViewFormItemComponent, + }); + + return class ABViewFormSelectSingle extends ABViewFormSelectSingleCore { + /** + * @method component() + * return a UI component based upon this view. + * @return {obj} UI component + */ + component() { + return new ABAbviewformSelectSingleComponent(this); + } + }; +} diff --git a/AppBuilder/platform/plugins/included/view_form/FNAbviewformTextbox.js b/AppBuilder/platform/plugins/included/view_form/FNAbviewformTextbox.js new file mode 100644 index 00000000..5c0f3973 --- /dev/null +++ b/AppBuilder/platform/plugins/included/view_form/FNAbviewformTextbox.js @@ -0,0 +1,24 @@ +import FNAbviewformTextboxComponent from "./viewComponent/FNAbviewformTextboxComponent.js"; +import FNAbviewformTextboxCoreFactory from "./core/ABViewFormTextboxCore.js"; + +export default function FNAbviewformTextbox({ + ABViewComponentPlugin, + ABViewFormItemComponent, + ABViewFormItem, +}) { + const ABViewFormTextboxCore = FNAbviewformTextboxCoreFactory(ABViewFormItem); + const ABAbviewformTextboxComponent = FNAbviewformTextboxComponent({ + ABViewFormItemComponent, + }); + + return class ABViewFormTextbox extends ABViewFormTextboxCore { + /** + * @method component() + * return a UI component based upon this view. + * @return {obj} UI component + */ + component() { + return new ABAbviewformTextboxComponent(this); + } + }; +} diff --git a/AppBuilder/platform/plugins/included/view_form/FNAbviewformTree.js b/AppBuilder/platform/plugins/included/view_form/FNAbviewformTree.js new file mode 100644 index 00000000..3ea53a2d --- /dev/null +++ b/AppBuilder/platform/plugins/included/view_form/FNAbviewformTree.js @@ -0,0 +1,28 @@ +import FNAbviewformTreeComponent from "./viewComponent/FNAbviewformTreeComponent.js"; +import FNAbviewformTreeCoreFactory from "./core/ABViewFormTreeCore.js"; + +export default function FNAbviewformTree({ + ABViewComponentPlugin, + ABViewFormItemComponent, + ABViewFormCustom, + ABFieldImage, + FocusableTemplate, +}) { + const ABViewFormTreeCore = FNAbviewformTreeCoreFactory(ABViewFormCustom); + const ABAbviewformTreeComponent = FNAbviewformTreeComponent({ + ABViewFormItemComponent, + ABFieldImage, + FocusableTemplate, + }); + + return class ABViewFormTree extends ABViewFormTreeCore { + /** + * @method component() + * return a UI component based upon this view. + * @return {obj} UI component + */ + component() { + return new ABAbviewformTreeComponent(this); + } + }; +} diff --git a/AppBuilder/platform/plugins/included/view_form/FNAbviewformURL.js b/AppBuilder/platform/plugins/included/view_form/FNAbviewformURL.js new file mode 100644 index 00000000..9768129b --- /dev/null +++ b/AppBuilder/platform/plugins/included/view_form/FNAbviewformURL.js @@ -0,0 +1,16 @@ +import FNAbviewformURLCoreFactory from "./core/ABViewFormURLCore.js"; + +export default function FNAbviewformURL({ ABAbviewformComponent, ABViewForm }) { + const ABViewFormURLCore = FNAbviewformURLCoreFactory(ABViewForm); + + return class ABViewFormURL extends ABViewFormURLCore { + /** + * @method component() + * return a UI component based upon this view. + * @return {obj} UI component + */ + component() { + return new ABAbviewformComponent(this); + } + }; +} diff --git a/AppBuilder/platform/plugins/included/view_form/FormComponents.js b/AppBuilder/platform/plugins/included/view_form/FormComponents.js new file mode 100644 index 00000000..c63f4aa3 --- /dev/null +++ b/AppBuilder/platform/plugins/included/view_form/FormComponents.js @@ -0,0 +1,14 @@ +export { default as FNAbviewformButton } from "./FNAbviewformButton.js"; +export { default as FNAbviewformCheckbox } from "./FNAbviewformCheckbox.js"; +export { default as FNAbviewformConnect } from "./FNAbviewformConnect.js"; +export { default as FNAbviewformCustom } from "./FNAbviewformCustom.js"; +export { default as FNAbviewformDatepicker } from "./FNAbviewformDatepicker.js"; +export { default as FNAbviewformItem } from "./FNAbviewformItem.js"; +export { default as FNAbviewformJson } from "./FNAbviewformJson.js"; +export { default as FNAbviewformNumber } from "./FNAbviewformNumber.js"; +export { default as FNAbviewformReadonly } from "./FNAbviewformReadonly.js"; +export { default as FNAbviewformSelectMultiple } from "./FNAbviewformSelectMultiple.js"; +export { default as FNAbviewformSelectSingle } from "./FNAbviewformSelectSingle.js"; +export { default as FNAbviewformTree } from "./FNAbviewformTree.js"; +export { default as FNAbviewformTextbox } from "./FNAbviewformTextbox.js"; +export { default as FNAbviewformURL } from "./FNAbviewformURL.js"; diff --git a/AppBuilder/platform/plugins/included/view_form/core/ABViewFormButtonCore.js b/AppBuilder/platform/plugins/included/view_form/core/ABViewFormButtonCore.js new file mode 100644 index 00000000..b4f67b35 --- /dev/null +++ b/AppBuilder/platform/plugins/included/view_form/core/ABViewFormButtonCore.js @@ -0,0 +1,125 @@ +export default function (ABView) { + const ABViewFormButtonPropertyComponentDefaults = { + includeSave: true, + saveLabel: "", + includeCancel: false, + cancelLabel: "", + includeReset: false, + resetLabel: "", + includeDelete: false, + deleteLabel: "", + afterCancel: null, + alignment: "right", + isDefault: false, // mark default button of form widget + }; + + const ABViewFormButtonDefaults = { + key: "button", + // {string} unique key for this view + + icon: "square", + // {string} fa-[icon] reference for this view + + labelKey: "ab.components.button", + // {string} the multilingual label key for the class label + }; + + return class ABViewFormButtonCore extends ABView { + constructor(values, application, parent, defaultValues) { + super( + values, + application, + parent, + defaultValues || ABViewFormButtonDefaults + ); + } + + static common() { + return ABViewFormButtonDefaults; + } + + static defaultValues() { + return ABViewFormButtonPropertyComponentDefaults; + } + + /// + /// Instance Methods + /// + + toObj() { + // labels are multilingual values: + let labels = []; + + if (this.settings.saveLabel) labels.push("saveLabel"); + + if (this.settings.cancelLabel) labels.push("cancelLabel"); + + if (this.settings.resetLabel) labels.push("resetLabel"); + + if (this.settings.deleteLabel) labels.push("deleteLabel"); + + this.unTranslate(this.settings, this.settings, labels); + + let result = super.toObj(); + + return result; + } + + /** + * @property datacollection + * return data source + * NOTE: this view doesn't track a DataCollection. + * @return {ABDataCollection} + */ + get datacollection() { + return null; + } + + fromValues(values) { + super.fromValues(values); + + // labels are multilingual values: + let labels = []; + + if (this.settings.saveLabel) labels.push("saveLabel"); + + if (this.settings.cancelLabel) labels.push("cancelLabel"); + + if (this.settings.resetLabel) labels.push("resetLabel"); + + if (this.settings.deleteLabel) labels.push("deleteLabel"); + + this.unTranslate(this.settings, this.settings, labels); + + this.settings.includeSave = JSON.parse( + (this.settings?.includeSave ?? true) && + ABViewFormButtonPropertyComponentDefaults.includeSave + ); + this.settings.includeCancel = JSON.parse( + this.settings.includeCancel || + ABViewFormButtonPropertyComponentDefaults.includeCancel + ); + this.settings.includeReset = JSON.parse( + this.settings.includeReset || + ABViewFormButtonPropertyComponentDefaults.includeReset + ); + this.settings.includeDelete = JSON.parse( + this.settings.includeDelete || + ABViewFormButtonPropertyComponentDefaults.includeDelete + ); + + this.settings.isDefault = JSON.parse( + this.settings.isDefault || + ABViewFormButtonPropertyComponentDefaults.isDefault + ); + } + + /** + * @method componentList + * return the list of components available on this view to display in the editor. + */ + componentList() { + return []; + } + }; +} diff --git a/AppBuilder/platform/plugins/included/view_form/core/ABViewFormCheckboxCore.js b/AppBuilder/platform/plugins/included/view_form/core/ABViewFormCheckboxCore.js new file mode 100644 index 00000000..9b612775 --- /dev/null +++ b/AppBuilder/platform/plugins/included/view_form/core/ABViewFormCheckboxCore.js @@ -0,0 +1,28 @@ +export default function (ABViewFormItem) { + const ABViewFormCheckboxPropertyComponentDefaults = {}; + + const ABViewFormCheckboxDefaults = { + key: "checkbox", // {string} unique key for this view + icon: "check-square-o", // {string} fa-[icon] reference for this view + labelKey: "ab.components.checkbox", // {string} the multilingual label key for the class label + }; + + return class ABViewFormCheckboxCore extends ABViewFormItem { + constructor(values, application, parent, defaultValues) { + super( + values, + application, + parent, + defaultValues || ABViewFormCheckboxDefaults + ); + } + + static common() { + return ABViewFormCheckboxDefaults; + } + + static defaultValues() { + return ABViewFormCheckboxPropertyComponentDefaults; + } + }; +} diff --git a/AppBuilder/platform/plugins/included/view_form/core/ABViewFormConnectCore.js b/AppBuilder/platform/plugins/included/view_form/core/ABViewFormConnectCore.js new file mode 100644 index 00000000..0ef808db --- /dev/null +++ b/AppBuilder/platform/plugins/included/view_form/core/ABViewFormConnectCore.js @@ -0,0 +1,63 @@ +export default function (ABViewFormItem) { + const ABViewFormConnectPropertyComponentDefaults = { + formView: "", // id of form to add new data + filterConditions: { + // array of filters to apply to the data table + glue: "and", + rules: [], + }, + sortFields: [], + // objectWorkspace: { + // filterConditions: { + // // array of filters to apply to the data table + // glue: "and", + // rules: [], + // }, + // }, + popupWidth: 700, + popupHeight: 450, + }; + + const ABViewFormConnectDefaults = { + key: "connect", // {string} unique key for this view + icon: "list-ul", // {string} fa-[icon] reference for this view + labelKey: "Connect", // {string} the multilingual label key for the class label + }; + + return class ABViewFormConnectCore extends ABViewFormItem { + constructor(values, application, parent, defaultValues) { + super( + values, + application, + parent, + defaultValues || ABViewFormConnectDefaults + ); + } + + static common() { + return ABViewFormConnectDefaults; + } + + static defaultValues() { + return ABViewFormConnectPropertyComponentDefaults; + } + + /// + /// Instance Methods + /// + + /** + * @method fromValues() + * + * initialze this object with the given set of values. + * @param {obj} values + */ + fromValues(values) { + super.fromValues(values); + + this.settings.filterConditions = + this.settings.filterConditions || + ABViewFormConnectPropertyComponentDefaults.filterConditions; + } + }; +} diff --git a/AppBuilder/platform/plugins/included/view_form/core/ABViewFormCore.js b/AppBuilder/platform/plugins/included/view_form/core/ABViewFormCore.js new file mode 100644 index 00000000..f98e14eb --- /dev/null +++ b/AppBuilder/platform/plugins/included/view_form/core/ABViewFormCore.js @@ -0,0 +1,245 @@ +export default function ( + ABViewContainer, + ABViewFormItem, + ABRecordRule, + ABSubmitRule +) { + const ABViewFormDefaults = { + key: "form", // unique key identifier for this ABViewForm + icon: "list-alt", // icon reference: (without 'fa-' ) + labelKey: "Form", // {string} the multilingual label key for the class label + }; + + const ABViewFormPropertyComponentDefaults = { + dataviewID: null, + showLabel: true, + labelPosition: "left", + labelWidth: 120, + height: 200, + clearOnLoad: false, + clearOnSave: false, + displayRules: [], + editForm: "none", // The url pointer of ABViewForm + + // [{ + // action: {string}, + // when: [ + // { + // fieldId: {UUID}, + // comparer: {string}, + // value: {string} + // } + // ], + // values: [ + // { + // fieldId: {UUID}, + // value: {object} + // } + // ] + // }] + recordRules: [], + + // [{ + // action: {string}, + // when: [ + // { + // fieldId: {UUID}, + // comparer: {string}, + // value: {string} + // } + // ], + // value: {string} + // }] + submitRules: [], + }; + + return class ABViewFormCore extends ABViewContainer { + constructor(values, application, parent, defaultValues) { + super( + values, + application, + parent, + defaultValues || ABViewFormDefaults + ); + this.isForm = true; + } + + static common() { + return ABViewFormDefaults; + } + + static defaultValues() { + return ABViewFormPropertyComponentDefaults; + } + + /// + /// Instance Methods + /// + + /** + * @method fromValues() + * + * initialze this object with the given set of values. + * @param {obj} values + */ + fromValues(values) { + super.fromValues(values); + + this.settings.labelPosition = + this.settings.labelPosition || + ABViewFormPropertyComponentDefaults.labelPosition; + + // convert from "0" => true/false + this.settings.showLabel = JSON.parse( + this.settings.showLabel != null + ? this.settings.showLabel + : ABViewFormPropertyComponentDefaults.showLabel + ); + this.settings.clearOnLoad = JSON.parse( + this.settings.clearOnLoad != null + ? this.settings.clearOnLoad + : ABViewFormPropertyComponentDefaults.clearOnLoad + ); + this.settings.clearOnSave = JSON.parse( + this.settings.clearOnSave != null + ? this.settings.clearOnSave + : ABViewFormPropertyComponentDefaults.clearOnSave + ); + + // convert from "0" => 0 + this.settings.labelWidth = parseInt( + this.settings.labelWidth == null + ? ABViewFormPropertyComponentDefaults.labelWidth + : this.settings.labelWidth + ); + this.settings.height = parseInt( + this.settings.height == null + ? ABViewFormPropertyComponentDefaults.height + : this.settings.height + ); + } + + // Use this function in kanban + objectLoad(object) { + this._currentObject = object; + } + + /** + * @method componentList + * return the list of components available on this view to display in the editor. + */ + componentList() { + var viewsToAllow = ["label", "layout", "button", "text"], + allComponents = this.application.viewAll(); + + return allComponents.filter((c) => { + return viewsToAllow.indexOf(c.common().key) > -1; + }); + } + + /** + * @method fieldComponents() + * + * return an array of all the ABViewFormField children + * + * @param {fn} filter a filter fn to return a set of ABViewFormField that this fn + * returns true for. + * @return {array} array of ABViewFormField + */ + fieldComponents(filter) { + const flattenComponents = (views) => { + let components = []; + + views.forEach((v) => { + if (v == null) return; + + components.push(v); + + if (v._views?.length) { + components = components.concat(flattenComponents(v._views)); + } + }); + + return components; + }; + + if (this._views?.length) { + const allComponents = flattenComponents(this._views); + + if (filter == null) { + filter = (comp) => comp.isFormField; + } + + return allComponents.filter(filter); + } else { + return []; + } + } + + addFieldToForm(field, yPosition) { + if (field == null) return; + + var fieldComponent = field.formComponent(); + if (fieldComponent == null) return; + + var newView = fieldComponent.newInstance(this.application, this); + if (newView == null) return; + + // set settings to component + newView.settings = newView.settings || {}; + newView.settings.fieldId = field.id; + // TODO : Default settings + + if (yPosition != null) newView.position.y = yPosition; + + // add a new component + this._views.push(newView); + + return newView; + } + + get RecordRule() { + let object = this.datacollection?.datasource; + + if (this._recordRule == null) { + this._recordRule = new ABRecordRule(); + } + + this._recordRule.formLoad(this); + this._recordRule.fromSettings(this.settings.recordRules); + this._recordRule.objectLoad(object); + + return this._recordRule; + } + + doRecordRulesPre(rowData) { + return this.RecordRule.processPre({ data: rowData, form: this }); + } + + doRecordRules(rowData) { + // validate for record rules + if (rowData) { + let object = this.datacollection.datasource; + let ruleValidator = object.isValidData(rowData); + let isUpdatedDataValid = ruleValidator.pass(); + if (!isUpdatedDataValid) { + console.error("Updated data is invalid.", { rowData: rowData }); + return Promise.reject(new Error("Updated data is invalid.")); + } + } + + return this.RecordRule.process({ data: rowData, form: this }); + } + + doSubmitRules(rowData) { + var object = this.datacollection.datasource; + + var SubmitRules = new ABSubmitRule(); + SubmitRules.formLoad(this); + SubmitRules.fromSettings(this.settings.submitRules); + SubmitRules.objectLoad(object); + + return SubmitRules.process({ data: rowData, form: this }); + } + }; +} diff --git a/AppBuilder/platform/plugins/included/view_form/core/ABViewFormCustomCore.js b/AppBuilder/platform/plugins/included/view_form/core/ABViewFormCustomCore.js new file mode 100644 index 00000000..abb3321f --- /dev/null +++ b/AppBuilder/platform/plugins/included/view_form/core/ABViewFormCustomCore.js @@ -0,0 +1,31 @@ +export default function (ABViewFormItem) { + const ABViewFormCustomPropertyComponentDefaults = {}; + + const ABViewFormCustomDefaults = { + key: "fieldcustom", + // {string} unique key for this view + icon: "object-group", + // {string} fa-[icon] reference for this view + labelKey: "ab.components.custom", + // {string} the multilingual label key for the class label + }; + + return class ABViewFormCustom extends ABViewFormItem { + constructor(values, application, parent, defaultValues) { + super( + values, + application, + parent, + defaultValues || ABViewFormCustomDefaults + ); + } + + static common() { + return ABViewFormCustomDefaults; + } + + static defaultValues() { + return ABViewFormCustomPropertyComponentDefaults; + } + }; +} diff --git a/AppBuilder/platform/plugins/included/view_form/core/ABViewFormDatepickerCore.js b/AppBuilder/platform/plugins/included/view_form/core/ABViewFormDatepickerCore.js new file mode 100644 index 00000000..3e3a53c0 --- /dev/null +++ b/AppBuilder/platform/plugins/included/view_form/core/ABViewFormDatepickerCore.js @@ -0,0 +1,30 @@ +export default function (ABViewFormItem) { + const ABViewFormDatepickerPropertyComponentDefaults = { + timepicker: false, + }; + + const ABViewFormDatepickerDefaults = { + key: "datepicker", // {string} unique key for this view + icon: "calendar", // {string} fa-[icon] reference for this view + labelKey: "ab.components.datepicker", // {string} the multilingual label key for the class label + }; + + return class ABViewFormDatepickerCore extends ABViewFormItem { + constructor(values, application, parent, defaultValues) { + super( + values, + application, + parent, + defaultValues || ABViewFormDatepickerDefaults + ); + } + + static common() { + return ABViewFormDatepickerDefaults; + } + + static defaultValues() { + return ABViewFormDatepickerPropertyComponentDefaults; + } + }; +} diff --git a/AppBuilder/platform/plugins/included/view_form/core/ABViewFormItemCore.js b/AppBuilder/platform/plugins/included/view_form/core/ABViewFormItemCore.js new file mode 100644 index 00000000..24126e29 --- /dev/null +++ b/AppBuilder/platform/plugins/included/view_form/core/ABViewFormItemCore.js @@ -0,0 +1,68 @@ +export default function (ABView) { + const ABViewFormFieldPropertyComponentDefaults = { + required: 0, + disable: 0, + }; + + return class ABViewFormItemCore extends ABView { + constructor(values, application, parent, defaultValues) { + super(values, application, parent, defaultValues); + this.isFormField = true; + } + + static defaultValues() { + return ABViewFormFieldPropertyComponentDefaults; + } + + /** + * @property datacollection + * return data source + * NOTE: this view doesn't track a DataCollection. + * @return {ABDataCollection} + */ + get datacollection() { + let form = this.parentFormComponent(); + if (form == null) return null; + + let datacollection = form.datacollection; + if (datacollection == null) return null; + + return datacollection; + } + + field() { + if (this.settings.objectId) { + let object = this.AB.objectByID(this.settings.objectId); + if (!object) return null; + + return object.fieldByID(this.settings.fieldId); + } else { + let form = this.parentFormComponent(); + if (form == null) return null; + + let object; + if (form._currentObject) { + object = form._currentObject; + } else { + let datacollection = form.datacollection; + if (datacollection == null) return null; + + object = datacollection.datasource; + } + + if (object == null) return null; + + let field = object.fieldByID(this.settings.fieldId); + return field; + } + } + + /** + * @method componentList + * return the list of components available on this view to display in the editor. + */ + componentList() { + return []; + } + }; +} diff --git a/AppBuilder/platform/plugins/included/view_form/core/ABViewFormJsonCore.js b/AppBuilder/platform/plugins/included/view_form/core/ABViewFormJsonCore.js new file mode 100644 index 00000000..33ec9968 --- /dev/null +++ b/AppBuilder/platform/plugins/included/view_form/core/ABViewFormJsonCore.js @@ -0,0 +1,30 @@ +export default function (ABViewFormItem) { + const ABViewFormJsonPropertyComponentDefaults = { + type: "string", // 'string', 'systemObject' or 'filter' + }; + + const ABViewFormJsonDefaults = { + key: "json", // {string} unique key for this view + icon: "brackets-curly", // {string} fa-[icon] reference for this view + labelKey: "ab.components.json", // {string} the multilingual label key for the class label + }; + + return class ABViewFormJsonCore extends ABViewFormItem { + constructor(values, application, parent, defaultValues) { + super( + values, + application, + parent, + defaultValues || ABViewFormJsonDefaults + ); + } + + static common() { + return ABViewFormJsonDefaults; + } + + static defaultValues() { + return ABViewFormJsonPropertyComponentDefaults; + } + }; +} diff --git a/AppBuilder/platform/plugins/included/view_form/core/ABViewFormNumberCore.js b/AppBuilder/platform/plugins/included/view_form/core/ABViewFormNumberCore.js new file mode 100644 index 00000000..779155c5 --- /dev/null +++ b/AppBuilder/platform/plugins/included/view_form/core/ABViewFormNumberCore.js @@ -0,0 +1,68 @@ +export default function (ABViewFormItem) { + const ABViewFormNumberPropertyComponentDefaults = { + isStepper: 0, + }; + + const ABViewFormNumberDefaults = { + key: "numberbox", // {string} unique key for this view + icon: "hashtag", // {string} fa-[icon] reference for this view + labelKey: "ab.components.number", // {string} the multilingual label key for the class label + }; + + return class ABViewFormNumberCore extends ABViewFormItem { + constructor(values, application, parent, defaultValues) { + super( + values, + application, + parent, + defaultValues || ABViewFormNumberDefaults + ); + } + + static common() { + return ABViewFormNumberDefaults; + } + + static defaultValues() { + return ABViewFormNumberPropertyComponentDefaults; + } + + /// + /// Instance Methods + /// + + /** + * @method toObj() + * + * properly compile the current state of this ABViewFormText instance + * into the values needed for saving. + * + * @return {json} + */ + toObj() { + this.unTranslate(this, this, ["label", "formLabel"]); + + var obj = super.toObj(); + obj.views = []; // no subviews + return obj; + } + + /** + * @method fromValues() + * + * initialze this object with the given set of values. + * @param {obj} values + */ + fromValues(values) { + super.fromValues(values); + + // if this is being instantiated on a read from the Property UI, + this.settings.isStepper = + this.settings.isStepper || + ABViewFormNumberPropertyComponentDefaults.isStepper; + + // convert from "0" => 0 + this.settings.isStepper = parseInt(this.settings.isStepper); + } + }; +} diff --git a/AppBuilder/platform/plugins/included/view_form/core/ABViewFormReadonlyCore.js b/AppBuilder/platform/plugins/included/view_form/core/ABViewFormReadonlyCore.js new file mode 100644 index 00000000..52c7aa67 --- /dev/null +++ b/AppBuilder/platform/plugins/included/view_form/core/ABViewFormReadonlyCore.js @@ -0,0 +1,28 @@ +export default function (ABViewFormCustom) { + const ABViewFormReadonlyPropertyComponentDefaults = {}; + + const ABViewFormReadonlyDefaults = { + key: "fieldreadonly", // {string} unique key for this view + icon: "eye", // {string} fa-[icon] reference for this view + labelKey: "ab.components.readonly", // {string} the multilingual label key for the class label + }; + + return class ABViewFormReadonly extends ABViewFormCustom { + constructor(values, application, parent, defaultValues) { + super( + values, + application, + parent, + defaultValues || ABViewFormReadonlyDefaults + ); + } + + static common() { + return ABViewFormReadonlyDefaults; + } + + static defaultValues() { + return ABViewFormReadonlyPropertyComponentDefaults; + } + }; +} diff --git a/AppBuilder/platform/plugins/included/view_form/core/ABViewFormSelectMultipleCore.js b/AppBuilder/platform/plugins/included/view_form/core/ABViewFormSelectMultipleCore.js new file mode 100644 index 00000000..032907e9 --- /dev/null +++ b/AppBuilder/platform/plugins/included/view_form/core/ABViewFormSelectMultipleCore.js @@ -0,0 +1,30 @@ +export default function (ABViewFormItem) { + const ABViewFormSelectMultiplePropertyComponentDefaults = { + type: "multicombo", // 'richselect' or 'radio' + }; + + const ABSelectMultipleDefaults = { + key: "selectmultiple", // {string} unique key for this view + icon: "list-ul", // {string} fa-[icon] reference for this view + labelKey: "ab.components.selectmultiple", // {string} the multilingual label key for the class label + }; + + return class ABViewFormSelectMultipleCore extends ABViewFormItem { + constructor(values, application, parent, defaultValues) { + super( + values, + application, + parent, + defaultValues || ABSelectMultipleDefaults + ); + } + + static common() { + return ABSelectMultipleDefaults; + } + + static defaultValues() { + return ABViewFormSelectMultiplePropertyComponentDefaults; + } + }; +} diff --git a/AppBuilder/platform/plugins/included/view_form/core/ABViewFormSelectSingleCore.js b/AppBuilder/platform/plugins/included/view_form/core/ABViewFormSelectSingleCore.js new file mode 100644 index 00000000..c6c26617 --- /dev/null +++ b/AppBuilder/platform/plugins/included/view_form/core/ABViewFormSelectSingleCore.js @@ -0,0 +1,30 @@ +export default function (ABViewFormItem) { + const ABViewFormSelectSinglePropertyComponentDefaults = { + type: "richselect", // 'richselect' or 'radio' + }; + + const ABSelectSingleDefaults = { + key: "selectsingle", // {string} unique key for this view + icon: "list-ul", // {string} fa-[icon] reference for this view + labelKey: "ab.components.selectsingle", // {string} the multilingual label key for the class label + }; + + return class ABViewFormSelectSingleCore extends ABViewFormItem { + constructor(values, application, parent, defaultValues) { + super( + values, + application, + parent, + defaultValues || ABSelectSingleDefaults + ); + } + + static common() { + return ABSelectSingleDefaults; + } + + static defaultValues() { + return ABViewFormSelectSinglePropertyComponentDefaults; + } + }; +} diff --git a/AppBuilder/platform/plugins/included/view_form/core/ABViewFormTextboxCore.js b/AppBuilder/platform/plugins/included/view_form/core/ABViewFormTextboxCore.js new file mode 100644 index 00000000..49f4fc3d --- /dev/null +++ b/AppBuilder/platform/plugins/included/view_form/core/ABViewFormTextboxCore.js @@ -0,0 +1,30 @@ +export default function (ABViewFormItem) { + const ABViewFormTextboxPropertyComponentDefaults = { + type: "single", // 'single', 'multiple' or 'rich' + }; + + const ABViewFormTextboxDefaults = { + key: "textbox", // {string} unique key for this view + icon: "i-cursor", // {string} fa-[icon] reference for this view + labelKey: "ab.components.textbox", // {string} the multilingual label key for the class label + }; + + return class ABViewFormTextboxCore extends ABViewFormItem { + constructor(values, application, parent, defaultValues) { + super( + values, + application, + parent, + defaultValues || ABViewFormTextboxDefaults + ); + } + + static common() { + return ABViewFormTextboxDefaults; + } + + static defaultValues() { + return ABViewFormTextboxPropertyComponentDefaults; + } + }; +} diff --git a/AppBuilder/platform/plugins/included/view_form/core/ABViewFormTreeCore.js b/AppBuilder/platform/plugins/included/view_form/core/ABViewFormTreeCore.js new file mode 100644 index 00000000..b7831468 --- /dev/null +++ b/AppBuilder/platform/plugins/included/view_form/core/ABViewFormTreeCore.js @@ -0,0 +1,23 @@ +export default function (ABViewFormCustom) { + const ABViewFormTreePropertyComponentDefaults = {}; + + const ABTreeDefaults = { + key: "formtree", // {string} unique key for this view + icon: "sitemap", // {string} fa-[icon] reference for this view + labelKey: "ab.components.tree", // {string} the multilingual label key for the class label + }; + + return class ABViewFormTreeCore extends ABViewFormCustom { + constructor(values, application, parent, defaultValues) { + super(values, application, parent, defaultValues || ABTreeDefaults); + } + + static common() { + return ABTreeDefaults; + } + + static defaultValues() { + return ABViewFormTreePropertyComponentDefaults; + } + }; +} diff --git a/AppBuilder/platform/plugins/included/view_form/core/ABViewFormURLCore.js b/AppBuilder/platform/plugins/included/view_form/core/ABViewFormURLCore.js new file mode 100644 index 00000000..9b3e591b --- /dev/null +++ b/AppBuilder/platform/plugins/included/view_form/core/ABViewFormURLCore.js @@ -0,0 +1,40 @@ +export default function (ABViewForm) { + const ABViewFormURLDefaults = { + key: "form-url", // unique key identifier for this ABViewForm + icon: "list-alt", // icon reference: (without 'fa-' ) + labelKey: "FormUrl", // {string} the multilingual label key for the class label + }; + + return class ABViewFormURLCore extends ABViewForm { + static common() { + return ABViewFormURLDefaults; + } + + async submitValues(formVals) { + let url = this.settings.url; + let method = this.settings.method || "get"; + method = method.toLowerCase(); + if (!["get", "post", "put", "delete"].includes(method)) { + throw new Error( + `Invalid method "${method}" specified for ABViewFormURL` + ); + } + + // remove empty id from formVals + if (formVals.id === "") { + delete formVals.id; + } + + let params = { + data: formVals, + url, + }; + + if (this.settings.headers) { + params.headers = this.settings.headers; + } + + return await this.AB.Network[method](params); + } + }; +} diff --git a/AppBuilder/platform/plugins/included/view_form/viewComponent/FNAbviewformButtonComponent.js b/AppBuilder/platform/plugins/included/view_form/viewComponent/FNAbviewformButtonComponent.js new file mode 100644 index 00000000..2c459dac --- /dev/null +++ b/AppBuilder/platform/plugins/included/view_form/viewComponent/FNAbviewformButtonComponent.js @@ -0,0 +1,244 @@ +export default function FNAbviewformButtonComponent({ + ABViewFormItemComponent, +}) { + return class ABViewFormButton extends ABViewFormItemComponent { + constructor(baseView, idBase, ids) { + super(baseView, idBase || `ABViewFormButton_${baseView.id}`, ids); + } + + ui() { + const self = this; + const baseView = this.view; + const form = baseView.parentFormComponent(); + const settings = baseView.settings ?? {}; + + const alignment = + settings.alignment || + baseView.constructor.defaultValues().alignment; + + const _ui = { + cols: [], + }; + + // spacer + if (alignment === "center" || alignment === "right") { + _ui.cols.push({}); + } + + // delete button + if (settings.includeDelete) { + _ui.cols.push( + { + view: "button", + autowidth: true, + value: settings.deleteLabel || this.label("Delete"), + css: "webix_danger", + click: function () { + self.onDelete(this); + }, + on: { + onAfterRender: function () { + this.getInputNode().setAttribute( + "data-cy", + `button delete ${form.id}` + ); + }, + }, + }, + { + width: 10, + } + ); + } + + // cancel button + if (settings.includeCancel) { + _ui.cols.push( + { + view: "button", + autowidth: true, + value: settings.cancelLabel || this.label("Cancel"), + click: function () { + self.onCancel(this); + }, + on: { + onAfterRender: function () { + this.getInputNode().setAttribute( + "data-cy", + `button cancel ${form.id}` + ); + }, + }, + }, + { + width: 10, + } + ); + } + + // reset button + if (settings.includeReset) { + _ui.cols.push( + { + view: "button", + autowidth: true, + value: settings.resetLabel || this.label("Reset"), + click: function () { + self.onClear(this); + }, + on: { + onAfterRender: function () { + this.getInputNode().setAttribute( + "data-cy", + `button reset ${form.id}` + ); + }, + }, + }, + { + width: 10, + } + ); + } + + // save button + if (settings.includeSave) { + _ui.cols.push({ + view: "button", + type: "form", + css: "webix_primary", + autowidth: true, + value: settings.saveLabel || this.label("Save"), + click: function () { + self.onSave(this); + }, + on: { + onAfterRender: function () { + this.getInputNode().setAttribute( + "data-cy", + `button save ${form.id}` + ); + }, + }, + }); + } + + // spacer + if (alignment === "center" || alignment === "left") _ui.cols.push({}); + + return super.ui(_ui); + } + + onCancel(cancelButton) { + const baseView = this.view; + const settings = baseView.settings ?? {}; + + // get form component + const form = baseView.parentFormComponent(); + + // get ABDatacollection + const dc = form.datacollection; + + // clear cursor of DC if not set to follow another + if (!dc?.isCursorFollow) { + dc?.setCursor(null); + } + // dc?.setStaticCursor(); // unless it should be static + + cancelButton?.getFormView?.().clear(); + + if (settings.afterCancel) form.changePage(settings.afterCancel); + // If the redirect page is not defined, then redirect to parent page + else { + const noPopupFilter = (p) => + p.settings && p.settings.type != "popup"; + + const pageCurr = this.view.pageParent(); + if (pageCurr) { + const pageParent = + pageCurr.pageParent(noPopupFilter) ?? pageCurr; + + if (pageParent) form.changePage(pageParent.id); + } + } + } + + onClear(resetButton) { + // get form component + const form = this.view.parentFormComponent(); + + // get ABDatacollection + const dc = form.datacollection; + + // clear cursor of DC + if (dc) { + dc.setCursor(null); + } + + resetButton?.getFormView?.().clear(); + } + + onSave(saveButton) { + if (!saveButton) { + console.error("Require the button element"); + return; + } + // get form component + const form = this.view.parentFormComponent(); + const formView = saveButton.getFormView(); + + // disable the save button + saveButton.disable?.(); + + // save data + form + .saveData(formView) + .then(() => { + saveButton.enable?.(); + + //Focus on first focusable component + form.focusOnFirst(); + }) + .catch((err) => { + console.error(err); + // Catch uncaught error reported in Sentry and add context + // APPBUILDER-WEB-1A3(https://appdev-designs.sentry.io/issues/4631880265/) + try { + saveButton.enable?.(); + } catch (e) { + this.AB.notify.developer(e, { + context: + "formButton.onSave > catch err > saveButton.enable()", + buttonID: this?.view?.id, + formID: this?.view?.parent?.id, + }); + } + }); + } + + onDelete(deleteButton) { + this.AB.Webix.confirm({ + title: this.label("Delete data"), + text: this.label("Do you want to delete this data?"), + callback: async (confirm) => { + if (!confirm) return; + + deleteButton.disable?.(); + + try { + // get form component + const form = this.view.parentFormComponent(); + const $formView = deleteButton.getFormView(); + + // delete a record row + await form.deleteData($formView); + } catch (err) { + console.error(err); + } finally { + deleteButton.enable?.(); + } + }, + }); + } + }; +} diff --git a/AppBuilder/platform/plugins/included/view_form/viewComponent/FNAbviewformCheckboxComponent.js b/AppBuilder/platform/plugins/included/view_form/viewComponent/FNAbviewformCheckboxComponent.js new file mode 100644 index 00000000..48d1b67b --- /dev/null +++ b/AppBuilder/platform/plugins/included/view_form/viewComponent/FNAbviewformCheckboxComponent.js @@ -0,0 +1,15 @@ +export default function FNAbviewformCheckboxComponent({ + ABViewFormItemComponent, +}) { + return class ABViewFormCheckboxComponent extends ABViewFormItemComponent { + constructor(baseView, idBase, ids) { + super(baseView, idBase || `ABViewFormCheckbox_${baseView.id}`, ids); + } + + ui() { + return super.ui({ + view: "checkbox", + }); + } + }; +} diff --git a/AppBuilder/platform/plugins/included/view_form/viewComponent/FNAbviewformConnectComponent.js b/AppBuilder/platform/plugins/included/view_form/viewComponent/FNAbviewformConnectComponent.js new file mode 100644 index 00000000..7360444d --- /dev/null +++ b/AppBuilder/platform/plugins/included/view_form/viewComponent/FNAbviewformConnectComponent.js @@ -0,0 +1,642 @@ +export default function FNAbviewformConnectComponent({ + ABViewFormItemComponent, +}) { + return class ABViewFormConnectComponent extends ABViewFormItemComponent { + constructor(baseView, idBase, ids) { + super( + baseView, + idBase || `ABViewFormConnect_${baseView.id}`, + Object.assign( + { + popup: "", + editpopup: "", + }, + ids + ) + ); + + this.addPageComponent = null; + this.editPageComponent = null; + } + + get field() { + return this.view.field(); + } + + get multiselect() { + return this.field?.settings.linkType == "many"; + } + + ui() { + const field = this.field; + const baseView = this.view; + const form = baseView.parentFormComponent(); + const settings = this.settings; + + if (!field) { + console.error(`This field could not found : ${settings.fieldId}`); + + return super.ui({ + view: "label", + label: "", + }); + } + + const multiselect = this.multiselect; // field.settings.linkType == "many"; + const formSettings = form?.settings || {}; + const ids = this.ids; + + let _ui = { + id: ids.formItem, + view: multiselect ? "multicombo" : "combo", + name: field.columnName, + required: + field?.settings?.required || + parseInt(settings?.required) || + false, + // label: field.label, + // labelWidth: settings.labelWidth, + dataFieldId: field.id, + on: { + onItemClick: (id, e) => { + if ( + e.target.classList.contains("editConnectedPage") && + e.target.dataset.itemId + ) { + const rowId = e.target.dataset.itemId; + if (!rowId) return; + this.goToEditPage(rowId); + } + }, + onChange: (data) => { + this._onChange(data); + }, + }, + }; + + if (formSettings.showLabel) { + _ui.label = field.label; + _ui.labelWidth = formSettings.labelWidth; + _ui.labelPosition = formSettings.labelPosition; + } + + this.initAddEditTool(); + + _ui.suggest = { + button: true, + selectAll: multiselect ? true : false, + body: { + data: [], + template: `${ + baseView?.settings?.editForm + ? '' + : "" + }#value#`, + }, + on: { + onShow: () => { + field.populateOptionsDataCy($$(ids.formItem), field, form); + }, + }, + // Support partial matches + filter: ({ value }, search) => + value.toLowerCase().includes(search.toLowerCase()), + }; + + _ui.onClick = { + customField: (id, e, trg) => { + if (settings.disable === 1) return; + + const rowData = {}; + const $formItem = $$(ids.formItem); + + if ($formItem) { + const node = $formItem.$view; + + field.customEdit(rowData, /* App,*/ node); + } + }, + }; + + let apcUI = this.addPageComponent?.ui; + if (apcUI) { + // reset some component vals to make room for button + _ui.label = ""; + _ui.labelWidth = 0; + + // add click event to add new button + apcUI.on = { + onItemClick: (/*id, evt*/) => { + // let $form = $$(id).getFormView(); + this.addPageComponent?.onClick(form.datacollection); + + return false; + }, + }; + + if (_ui.labelPosition == "top") { + _ui.labelPosition = "left"; + _ui = { + inputId: ids.formItem, + rows: [ + { + view: "label", + label: field.label, + // height: 22, + align: "left", + }, + { + cols: [apcUI, _ui], + }, + ], + }; + } else { + _ui = { + inputId: ids.formItem, + rows: [ + { + cols: [ + { + view: "label", + label: field.label, + width: formSettings.labelWidth, + align: "left", + }, + apcUI, + _ui, + ], + }, + ], + }; + } + + _ui = super.ui(_ui); + } else { + _ui = { + inputId: ids.formItem, + rows: [_ui], + }; + + _ui = super.ui(_ui); + + delete _ui.rows[0].id; + } + + return _ui; + } + + async _onChange(data) { + const ids = this.ids; + const field = this.field; + const baseView = this.view; + + if (this.multiselect) { + if (typeof data == "string") { + data = data.split(","); + } + } + + let selectedValues; + if (Array.isArray(data)) { + selectedValues = []; + data.forEach((record) => { + selectedValues.push(record.id || record); + }); + } else { + selectedValues = data; + if (typeof data != "object") { + // we need to convert either index or uuid to full data object + selectedValues = field.getItemFromVal(data); + } + if (selectedValues?.id) { + selectedValues = selectedValues.id; + } else { + selectedValues = data; + } + } + + // We can now set the new value but we need to block event listening + // so it doesn't trigger onChange again + const $formItem = $$(ids.formItem); + + // Q: if we don't have a $formItem, is any of the rest valid? + if ($formItem) { + // for xxx->one connections we need to populate again before setting + // values because we need to use the selected values to add options + // to the UI + if (this?.field?.settings?.linkViaType == "one") { + this.busy(); + await field.getAndPopulateOptions( + $formItem, + baseView.options, + field, + baseView.parentFormComponent() + ); + this.ready(); + } + + $formItem.blockEvent(); + + // store the user's selected option in local storage. + field.saveSelect(selectedValues); + + const prepedVals = selectedValues.join + ? selectedValues.join() + : selectedValues; + + $formItem.setValue(prepedVals); + $formItem.unblockEvent(); + } + } + + async init(AB, options) { + await super.init(AB); + + const $formItem = $$(this.ids.formItem); + if ($formItem) webix.extend($formItem, webix.ProgressBar); + + this.initAddEditTool(); + } + + initAddEditTool() { + const baseView = this.view; + + // Initial add/edit page tools + const addFormID = baseView?.settings?.formView; + if (addFormID && baseView && !this.addPageComponent) { + this.addPageComponent = baseView.addPageTool.component( + this.AB, + `${baseView.id}_${addFormID}` + ); + this.addPageComponent.applicationLoad(baseView.application); + this.addPageComponent.init({ + onSaveData: this.callbackSaveData.bind(this), + onCancelClick: this.callbackCancel.bind(this), + clearOnLoad: this.callbackClearOnLoad.bind(this), + }); + } + + const editFormID = baseView?.settings?.editForm; + if (editFormID && baseView && !this.editPageComponent) { + this.editPageComponent = baseView.editPageTool.component( + this.AB, + `${baseView.id}_${editFormID}` + ); + this.editPageComponent.applicationLoad(baseView.application); + this.editPageComponent.init({ + onSaveData: this.callbackSaveData.bind(this), + onCancelClick: this.callbackCancel.bind(this), + clearOnLoad: this.callbackClearOnLoad.bind(this), + }); + } + } + + async callbackSaveData(saveData) { + if (saveData == null) return; + else if (!Array.isArray(saveData)) saveData = [saveData]; + + const ids = this.ids; + const field = this.field; + + // find the select component + const $formItem = $$(ids.formItem); + if (!$formItem) return; + + // Refresh option list + this.busy(); + field.clearStorage(this.view.settings.filterConditions); + const data = await field.getAndPopulateOptions( + $formItem, + this.view.options, + field, + this.view.parentFormComponent() + ); + this.ready(); + + data.forEach((item) => { + item.value = item.text; + }); + + $formItem.getList().clearAll(); + $formItem.getList().define("data", data); + + if (field.settings.linkType === "many") { + let selectedItems = $formItem.getValue(); + saveData.forEach((sData) => { + if (selectedItems.indexOf(sData.id) === -1) + selectedItems = selectedItems + ? `${selectedItems},${sData.id}` + : sData.id; + }); + + $formItem.setValue(selectedItems); + } else { + $formItem.setValue(saveData[0].id); + } + } + + callbackCancel() { + $$(this.ids?.popup)?.close?.(); + + return false; + } + + callbackClearOnLoad() { + return true; + } + + getValue(rowData) { + return this.field.getValue($$(this.ids.formItem), rowData); + } + + busy() { + const $formItem = $$(this.ids.formItem); + + $formItem?.disable(); + $formItem?.showProgress?.({ type: "icon" }); + } + + ready() { + const $formItem = $$(this.ids.formItem); + + $formItem?.enable(); + $formItem?.hideProgress?.(); + } + + goToEditPage(rowId) { + const settings = this.settings; + + if (!settings.editForm) return; + + const editForm = this.view.application.urlResolve(settings.editForm); + + if (!editForm) return; + + // Open the form popup + this.editPageComponent.onClick().then(() => { + const dc = editForm.datacollection; + + if (dc) { + dc.setCursor(rowId); + + this.__editFormDcEvent = + this.__editFormDcEvent || + dc.on("initializedData", () => { + dc.setCursor(rowId); + }); + } + }); + } + + async onShow() { + const ids = this.ids; + const $formItem = $$(ids.formItem); + + if (!$formItem) return; + + const field = this.field; + + if (!field) return; + + const node = $formItem.$view; + + if (!node) return; + + const $node = $$(node); + + if (!$node) return; + + const settings = this.settings; + let filterConditions = { + glue: "and", + rules: [], + }; + + if (settings?.filterConditions?.rules?.length) { + filterConditions = this.AB.cloneDeep( + this.view.settings.filterConditions + ); + } + + // NOTE: compatible with version 1. This code should not be here too long. + if ( + !filterConditions?.rules?.length && + settings?.objectWorkspace?.filterConditions?.rules?.length + ) { + filterConditions = this.AB.cloneDeep( + settings.objectWorkspace.filterConditions + ); + } + + // Add the filter connected value + if ((settings?.filterConnectedValue ?? "").indexOf(":") > -1) { + const values = settings.filterConnectedValue.split(":"), + uiConfigName = values[0], + connectFieldId = values[1]; + + filterConditions.rules.push({ + key: connectFieldId, + rule: "filterByConnectValue", + value: uiConfigName, + }); + } + + const getFilterByConnectValues = (conditions, depth = 0) => { + return [ + ...conditions.rules + .filter((e) => e.rule === "filterByConnectValue") + .map((e) => { + const filterByConnectValue = Object.assign({}, e); + + filterByConnectValue.depth = depth; + + return filterByConnectValue; + }), + ].concat( + ...conditions.rules + .filter((e) => e.glue) + .map((e) => getFilterByConnectValues(e, depth + 1)) + ); + }; + + const baseView = this.view; + const filterByConnectValues = getFilterByConnectValues( + filterConditions + ).map((e) => { + for (const key in baseView.parent.viewComponents) { + if ( + !( + baseView.parent.viewComponents[key] instanceof + this.constructor + ) + ) + continue; + + const $ui = $$( + baseView.parent.viewComponents[key] + .ui() + .rows.find((vc) => vc.inputId)?.inputId + ); + + if ($ui?.config?.name === e.value) { + // we need to use the element id stored in the settings to find out what the + // ui component id is so later we can use it to look up its current value + e.filterValue = $ui; + + break; + } + } + + const ab = this.AB; + const field = ab + .objectByID(settings.objectId) + .fieldByID(settings.fieldId); + const linkedObject = ab.objectByID(field.settings.linkObject); + const linkedField = linkedObject.fieldByID(e.key); + + if (linkedField?.settings?.isCustomFK) { + // finally if this is a custom foreign key we need the stored columnName by + // default uuid is passed for all non CFK + e.filterColumn = ab + .objectByID(linkedField.settings.linkObject) + .fields( + (filter) => + filter.id === linkedField.settings.indexField || + linkedField.settings.indexField2 + )[0].columnName; + } else e.filterColumn = null; + + return e; + }); + + baseView.options = { + formView: settings.formView, + filters: filterConditions, + // NOTE: settings.objectWorkspace.xxx is a depreciated setting. + // We will be phasing this out. + sort: settings.sortFields ?? settings.objectWorkspace?.sortFields, + editable: settings.disable === 1 ? false : true, + editPage: + !settings.editForm || settings.editForm === "none" + ? false + : true, + filterByConnectValues, + }; + + // if this field's options are filtered off another field's value we need + // to make sure the UX helps the user know what to do. + // fetch the options and set placeholder text for this view + if (baseView.options.editable) { + const parentFields = []; + + filterByConnectValues.forEach((fv) => { + if (fv.filterValue && fv.key) { + const $filterValueConfig = $$(fv.filterValue.config.id); + + let parentField = null; + + if (!$filterValueConfig) { + // this happens in the Interface Builder when only the single form UI is displayed + parentField = { + id: "perentElement", + label: this.label("PARENT ELEMENT"), + }; + } else { + const value = field.getValue($filterValueConfig); + + if (!value) { + // if there isn't a value on the parent select element set this one to readonly and change placeholder text + parentField = { + id: fv.filterValue.config.id, + label: $filterValueConfig.config.label, + }; + } + + $filterValueConfig.attachEvent( + "onChange", + async (e) => { + const parentVal = $filterValueConfig.getValue(); + + if (parentVal) { + $node.define("disabled", false); + $node.define( + "placeholder", + this.label("Select items") + ); + this.busy(); + await field.getAndPopulateOptions( + $node, + baseView.options, + field, + baseView.parentFormComponent() + ); + this.ready(); + } else { + $node.define("disabled", true); + $node.define( + "placeholder", + this.label( + "Must select item from '{0}' first.", + [$filterValueConfig.config.label] + ) + ); + } + + $node.refresh(); + }, + false + ); + } + + if ( + parentField && + parentFields.findIndex((e) => e.id === parentField.id) < 0 + ) + parentFields.push(parentField); + } + }); + + if (parentFields.length && !$node.getValue()) { + $node.define("disabled", true); + $node.define( + "placeholder", + this.label(`Must select item from '{0}' first.`, [ + parentFields.map((e) => e.label).join(", "), + ]) + ); + } else { + $node.define("disabled", false); + $node.define("placeholder", this.label("Select items")); + } + } else { + $node.define("placeholder", ""); + $node.define("disabled", true); + } + + $node.refresh(); + + // Add data-cy attributes + const dataCy = `${field.key} ${field.columnName} ${field.id} ${baseView.parent.id}`; + node.setAttribute("data-cy", dataCy); + + this.busy(); + try { + await field.getAndPopulateOptions( + $formItem, + baseView.options, + field, + baseView.parentFormComponent() + ); + } catch (err) { + this.AB.notify.developer(err, { + context: + "ABViewFormConnectComponent > onShow() error calling field.getAndPopulateOptions", + }); + } + this.ready(); + + // Need to refresh selected values when they are custom index + this._onChange($formItem.getValue()); + } + }; +} diff --git a/AppBuilder/platform/plugins/included/view_form/viewComponent/FNAbviewformCustomComponent.js b/AppBuilder/platform/plugins/included/view_form/viewComponent/FNAbviewformCustomComponent.js new file mode 100644 index 00000000..8ff90150 --- /dev/null +++ b/AppBuilder/platform/plugins/included/view_form/viewComponent/FNAbviewformCustomComponent.js @@ -0,0 +1,196 @@ +export default function FNAbviewformCustomComponent({ + ABViewFormItemComponent, + ABFieldImage, + FocusableTemplate, +}) { + const DEFAULT_HEIGHT = 80; + + return class ABViewFormCustomComponent extends ABViewFormItemComponent { + constructor(baseView, idBase, ids) { + super(baseView, idBase || `ABViewFormCustom_${baseView.id}`, ids); + } + + get new_width() { + const baseView = this.view; + const form = baseView.parentFormComponent(); + const formSettings = form?.settings ?? {}; + const settings = baseView.settings ?? {}; + + let newWidth = formSettings.labelWidth; + + if (settings.formView) newWidth += 40; + else if ( + formSettings.showLabel && + formSettings.labelPosition === "top" + ) + newWidth = 0; + + return newWidth; + } + + ui() { + const baseView = this.view; + const field = baseView.field(); + const form = baseView.parentFormComponent(); + const formSettings = form?.settings ?? {}; + const settings = field?.settings ?? baseView.settings ?? {}; + + const requiredClass = + field?.settings?.required || this.settings.required + ? "webix_required" + : ""; + + let templateLabel = ""; + + if (formSettings.showLabel) { + if (formSettings.labelPosition === "top") + templateLabel = ``; + else + templateLabel = ``; + } + + let height = 38; + let width = this.new_width; + + if (typeof field == "undefined") { + console.warn( + `BaseView[${baseView.id}] returned an undefined field()`, + baseView.toObj() + ); + } + + if (field.key === "image" || field.key === "file") { + if (settings.useHeight) { + if (formSettings.labelPosition === "top") { + height = parseInt(settings.imageHeight) || DEFAULT_HEIGHT; + height += 38; + } else { + height = parseInt(settings.imageHeight) || DEFAULT_HEIGHT; + } + } else if (formSettings.labelPosition === "top") { + height = DEFAULT_HEIGHT + 38; + } else { + if (DEFAULT_HEIGHT > 38) { + height = DEFAULT_HEIGHT; + } + } + width = + settings.useWidth && settings.imageWidth + ? settings.imageWidth + : 0; + } else if ( + formSettings.showLabel && + formSettings.labelPosition === "top" + ) + height = DEFAULT_HEIGHT; + + let template = `
    ${ + formSettings.labelPosition == "top" ? "" : templateLabel + }#template#
    ` + .replace(/#width#/g, formSettings.labelWidth) + .replace(/#label#/g, field?.label ?? "") + .replace( + /#template#/g, + field + ?.columnHeader({ + width: width, + height: height, + editable: true, + }) + .template({}) ?? "" + ); + + if (settings.useWidth == 0) { + template = template.replace( + /"ab-image-data-field" style="float: left; width: 100%/g, + '"ab-image-data-field" style="float: left; width: calc(100% - ' + + formSettings.labelWidth + + "px)" + ); + } + + return super.ui({ + view: "forminput", + labelWidth: 0, + paddingY: 0, + paddingX: 0, + css: "ab-custom-field", + body: { + view: new FocusableTemplate(this.AB._App).key, + css: "customFieldCls", + borderless: true, + template: template, + height: height, + onClick: { + customField: (evt, e, trg) => { + if (settings.disable === 1) return; + + let rowData = {}; + + const formView = + this?.parentFormComponent?.() || + this.view?.parentFormComponent?.(); + + if (formView) { + const dv = formView.datacollection; + if (dv) rowData = dv.getCursor() || {}; + } + + let node = $$(trg).getParentView().$view; + field?.customEdit( + rowData, + this.AB._App, + node, + this.ids.formItem, + evt + ); + }, + }, + }, + }); + } + + onShow() { + const ids = this.ids; + const $formItem = $$(ids.formItem); + + if (!$formItem) return; + + const baseView = this.view; + const field = baseView.field(), + rowData = {}, + node = $formItem.$view; + + // Add data-cy attributes + const dataCy = `${baseView.key} ${field?.key ?? ""} ${ + field?.columnName ?? "" + } ${baseView.id} ${baseView.parent?.id ?? ""}`; + node.setAttribute("data-cy", dataCy); + + const options = { + formId: ids.formItem, + editable: baseView.settings.disable === 1 ? false : true, + }; + + if (field.key === "image" || field.key === "file") { + options.height = field.settings.useHeight + ? parseInt(field.settings.imageHeight) || DEFAULT_HEIGHT + : DEFAULT_HEIGHT; + options.width = field.settings.useWidth + ? parseInt(field.settings.imageWidth) || 0 + : 0; + } + + field?.customDisplay(rowData, this.AB._App, node, options); + } + + getValue(rowData) { + const field = this.view.field(); + const $formItem = $$(this.ids.formItem); + + return field.getValue($formItem, rowData); + } + }; +} diff --git a/AppBuilder/platform/plugins/included/view_form/viewComponent/FNAbviewformDatepickerComponent.js b/AppBuilder/platform/plugins/included/view_form/viewComponent/FNAbviewformDatepickerComponent.js new file mode 100644 index 00000000..c4214840 --- /dev/null +++ b/AppBuilder/platform/plugins/included/view_form/viewComponent/FNAbviewformDatepickerComponent.js @@ -0,0 +1,116 @@ +export default function FNAbviewformDatepickerComponent({ + ABViewFormItemComponent, +}) { + return class ABViewFormDatepickerComponent extends ABViewFormItemComponent { + constructor(baseView, idBase, ids) { + super(baseView, idBase || `ABViewFormDatepicker_${baseView.id}`, ids); + } + + ui() { + const self = this; + const field = this.view.field(); + + const _ui = { + view: "datepicker", + suggest: { + body: { + view: + this.AB.Account?._config?.languageCode == "th" + ? "thaicalendar" + : "calendar", + type: field.settings?.dateFormat === 1 ? "time" : "", + timepicker: + field.key === "datetime" && + field.settings?.timeFormat !== 1 + ? true + : false, + editable: true, + on: { + onAfterDateSelect: function (date) { + this.getParentView().setMasterValue({ + value: date, + }); + }, + onTodaySet: function (date) { + this.getParentView().setMasterValue({ + value: date, + }); + }, + onDateClear: function (date) { + this.getParentView().setMasterValue({ + value: date, + }); + }, + }, + }, + on: { + onShow: function () { + const text = this.getMasterValue(); + const field = self.view.field(); + if (!text || !field) return true; + + const vals = {}; + vals[field.columnName] = text; + const date = self.getValue(vals); + + const $calendar = this.getChildViews()[0]; + $calendar.setValue(date); + }, + }, + }, + }; + + if (!field) return _ui; + + // Ignore date - Only time picker + if (field.settings?.dateFormat === 1) _ui.type = "time"; + + // Date & Time picker + if (field.key === "datetime" && field.settings?.timeFormat !== 1) + _ui.timepicker = true; + + // allows entering characters in datepicker input, false by default + _ui.editable = true; + + // default value + if (_ui.value && !(_ui.value instanceof Date)) + _ui.value = new Date(_ui.value); + + // if we have webix locale set, will use the date format form there. + if (!window.webixLocale) _ui.format = field.getFormat(); + + return super.ui(_ui); + } + + getValue(rowData) { + const field = this.view.field(); + const text = rowData[field.columnName]; + if (!field || !text) return null; + + if (!this.AB) { + if (this.view.AB) { + this.AB = this.view.AB; + } else { + let errNoAB = new Error( + "ABViewFormDatePicerComponent:getValue(): AB was not set." + ); + console.log("view:", JSON.stringify(this.view.toObj())); + throw errNoAB; + } + } + let dateVal = this.AB.Webix.Date.strToDate(field.getFormat())(text); + if (this.AB.Account?._config?.languageCode == "th") { + dateVal = this.AB.Webix.Date.strToDate("%j/%m/%Y")(text); + } + const date = dateVal; + + if ( + this.AB.Account?._config?.languageCode == "th" && + field.settings?.dateFormat !== 1 + ) + date.setFullYear(date.getFullYear() - 543); + + return date; + } + }; +} diff --git a/AppBuilder/platform/plugins/included/view_form/viewComponent/FNAbviewformItemComponent.js b/AppBuilder/platform/plugins/included/view_form/viewComponent/FNAbviewformItemComponent.js new file mode 100644 index 00000000..d42876c4 --- /dev/null +++ b/AppBuilder/platform/plugins/included/view_form/viewComponent/FNAbviewformItemComponent.js @@ -0,0 +1,102 @@ +export default function FNAbviewformItemComponent({ ABViewComponentPlugin }) { + return class ABViewFormItemComponent extends ABViewComponentPlugin { + constructor(baseView, idBase, ids) { + super( + baseView, + idBase || `ABViewFormItem_${baseView.id}`, + Object.assign({ formItem: "" }, ids) + ); + } + + ui(uiFormItemComponent = {}) { + // setup 'label' of the element + const baseView = this.view; + const form = baseView.parentFormComponent(), + field = baseView.field?.() || null, + label = ""; + const settings = form?.settings || {}; + const _uiFormItem = { + id: this.ids.formItem, + labelPosition: settings.labelPosition, + labelWidth: settings.labelWidth, + label, + }; + + if (field) { + _uiFormItem.name = field.columnName; + + // default value + const data = {}; + + field.defaultValue(data); + + if (data[field.columnName]) + _uiFormItem.value = data[field.columnName]; + + if (settings.showLabel) _uiFormItem.label = field.label; + + if (field.settings.required || baseView.settings?.required) + _uiFormItem.required = 1; + + if (baseView.settings?.disable === 1) _uiFormItem.disabled = true; + + // add data-cy to form element for better testing code + _uiFormItem.on = { + onAfterRender() { + if (this.getList) { + const popup = this.getPopup(); + + if (!popup) return; + + this.getList().data.each((option) => { + if (!option) return; + + // our option.ids are based on builder input and can include the ' character + const node = popup.$view.querySelector( + `[webix_l_id='${(option?.id ?? "") + .toString() + .replaceAll("'", "\\'")}']` + ); + + if (!node) return; + + node.setAttribute( + "data-cy", + `${field.key} options ${option.id} ${field.id} ${ + form?.id || "nf" + }` + ); + }); + } + + this.getInputNode?.().setAttribute?.( + "data-cy", + `${field.key} ${field.columnName} ${field.id} ${ + form?.id || "nf" + }` + ); + }, + }; + + // this may be needed if we want to format data at this point + // if (field.format) data = field.format(data); + + _uiFormItem.validate = (val, data, colName) => { + const validator = this.AB.Validation.validator(); + + field.isValidData(data, validator); + + return validator.pass(); + }; + } + + const _ui = super.ui([ + Object.assign({}, _uiFormItem, uiFormItemComponent), + ]); + + delete _ui.type; + + return _ui; + } + }; +} diff --git a/AppBuilder/platform/plugins/included/view_form/viewComponent/FNAbviewformJsonComponent.js b/AppBuilder/platform/plugins/included/view_form/viewComponent/FNAbviewformJsonComponent.js new file mode 100644 index 00000000..34762e3a --- /dev/null +++ b/AppBuilder/platform/plugins/included/view_form/viewComponent/FNAbviewformJsonComponent.js @@ -0,0 +1,168 @@ +export default function FNAbviewformJsonComponent({ ABViewFormItemComponent }) { + return class ABViewFormJsonComponent extends ABViewFormItemComponent { + constructor(baseView, idBase, ids) { + super(baseView, idBase || `ABViewFormJson_${baseView.id}`, ids); + if (this.settings.type == "filter") { + this.rowFilter = this.AB.filterComplexNew( + `${baseView.id}_filterComplex`, + { + isSaveHidden: true, + height: 300, + borderless: false, + showObjectName: true, + } + ); + } + } + + getFilterField(instance) { + if ( + instance?.settings?.filterField && + instance?.view?.parent?.viewComponents + ) { + let filterField = ""; + for (const value of Object.values( + instance.view.parent.viewComponents + )) { + if (value.settings.fieldId == instance.settings.filterField) { + filterField = value; + } + } + + if (filterField?.ids?.formItem) { + return filterField.ids.formItem; + } else { + return ""; + } + } else { + return ""; + } + } + + get getSystemObjects() { + // get list of all objects in the app + let objects = this.AB.objects(); + // reformat objects into simple array for Webix multicombo + // if you do not the data causes a maximum stack error + let objectsArray = []; + objects.forEach((obj) => { + objectsArray.push({ id: obj.id, label: obj.label }); + }); + // return the simple array + return objectsArray; + } + + refreshFilter(values) { + if (values) { + let fieldDefs = []; + values.forEach((obj) => { + let object = this.AB.objectByID(obj); + let fields = object.fields(); + if (fields.length) { + fields.forEach((f) => { + fieldDefs.push(f); + }); + } + }); + this.rowFilter.fieldsLoad(fieldDefs); + if ($$(this.ids.formItem).config.value) + this.rowFilter.setValue($$(this.ids.formItem).config.value); + } else { + this.rowFilter.fieldsLoad([]); + if ($$(this.ids.formItem).config.value) + this.rowFilter.setValue($$(this.ids.formItem).config.value); + } + } + + getValue() { + return this.rowFilter.getValue(); + } + + setValue(formVals) { + $$(this.ids.formItem).config.value = formVals; + } + + ui() { + const _ui = {}; + + switch ( + this.settings.type || + this.view.settings.type || + this.view.constructor.defaultValues().type + ) { + case "string": + _ui.view = "textarea"; + _ui.disabled = true; + _ui.height = 200; + _ui.format = { + parse: function (parsed) { + try { + parsed = JSON.parse(parsed); + } catch (err) { + // already parsed + } + return parsed; + }, + edit: function (stringify) { + try { + stringify = JSON.stringify(stringify); + } catch (err) { + // already a string + } + return stringify; + }, + }; + break; + case "systemObject": + _ui.view = "multicombo"; + _ui.placeholder = this.label( + "Select one or more system objects" + ); + _ui.button = false; + _ui.stringResult = false; + _ui.suggest = { + selectAll: true, + body: { + data: this.getSystemObjects, + template: webix.template("#label#"), + }, + }; + break; + case "filter": + _ui.view = "forminput"; + _ui.css = "ab-custom-field"; + _ui.body = this.rowFilter.ui; + break; + } + + return super.ui(_ui); + } + + init() { + // if (this.settings.type == "filter") { + // this.rowFilter.init({ showObjectName: true }); + // } + } + + onShow() { + const _ui = this.ui(); + if (this?.settings?.type == "filter") { + let filterField = this.getFilterField(this); + if (!$$(filterField)) return; + $$(filterField).detachEvent("onChange"); + $$(filterField).attachEvent("onChange", (values) => { + this.refreshFilter(values); + }); + this.rowFilter.init({ showObjectName: true }); + this.rowFilter.on("changed", (val) => { + this.setValue(val); + }); + if ($$(this.ids.formItem).config.value) { + this.rowFilter.setValue($$(this.ids.formItem).config.value); + } else { + this.rowFilter.setValue(""); + } + } + } + }; +} diff --git a/AppBuilder/platform/plugins/included/view_form/viewComponent/FNAbviewformNumberComponent.js b/AppBuilder/platform/plugins/included/view_form/viewComponent/FNAbviewformNumberComponent.js new file mode 100644 index 00000000..65e5c6ab --- /dev/null +++ b/AppBuilder/platform/plugins/included/view_form/viewComponent/FNAbviewformNumberComponent.js @@ -0,0 +1,23 @@ +export default function FNAbviewformNumberComponent({ + ABViewFormItemComponent, +}) { + return class ABViewFormNumberComponent extends ABViewFormItemComponent { + constructor(baseView, idBase, ids) { + super(baseView, idBase || `ABViewFormNumber_${baseView.id}`, ids); + } + + ui() { + const settings = this.view.settings ?? {}; + const _ui = {}; + + if (settings.isStepper) { + _ui.view = "counter"; + } else { + _ui.view = "text"; + _ui.attributes = { type: "number" }; + } + + return super.ui(_ui); + } + }; +} diff --git a/AppBuilder/platform/plugins/included/view_form/viewComponent/FNAbviewformReadonlyComponent.js b/AppBuilder/platform/plugins/included/view_form/viewComponent/FNAbviewformReadonlyComponent.js new file mode 100644 index 00000000..5cb95e26 --- /dev/null +++ b/AppBuilder/platform/plugins/included/view_form/viewComponent/FNAbviewformReadonlyComponent.js @@ -0,0 +1,126 @@ +export default function FNAbviewformReadonlyComponent({ + ABViewFormItemComponent, + ABFieldImage, + FocusableTemplate, +}) { + return class ABViewFormReadonlyComponent extends ABViewFormItemComponent { + constructor(baseView, idBase, ids) { + super( + baseView, + idBase || `ABViewFormReadonly_${baseView.id}`, + Object.assign( + { + template: "", + }, + ids + ) + ); + } + + ui() { + const baseView = this.view; + const field = baseView.field(); + + const _ui = { + view: "forminput", + labelWidth: 0, + paddingY: 0, + paddingX: 0, + readonly: true, + css: "ab-readonly-field", + body: { + id: this.ids.template, + view: "label", + borderless: true, + css: { "background-color": "#fff" }, + label: "", + }, + }; + + const form = baseView.parentFormComponent(); + const settings = form ? form.settings || {} : {}; + + if (settings.showLabel == true && settings.labelPosition == "top") { + _ui.body.height = 80; + } else if (field.settings && field.settings.useHeight) { + _ui.body.height = parseInt(field.settings.imageHeight) || 38; + } else _ui.body.height = 38; + + return super.ui(_ui); + } + + async init(AB) { + await super.init(AB); + + const $formItem = $$(this.ids.formItem); + if (!$formItem) return; + + const $form = $formItem.getFormView(); + const rowData = $form?.getValues() ?? {}; + + this.refresh(rowData); + $form?.attachEvent("onChange", (newv, oldv) => { + const rowData = $form?.getValues() ?? {}; + + this.refresh(rowData); + }); + } + + onShow() { + const $formItem = $$(this.ids.formItem); + if (!$formItem) return; + + const $form = $formItem.getFormView(); + const rowData = $form?.getValues() ?? {}; + + this.refresh(rowData); + } + + getValue(rowData) { + const field = this.view.field(); + if (!field) return null; + + return rowData[field.columnName]; + } + + refresh(rowData) { + const baseView = this.view; + const form = baseView.parentFormComponent(), + field = baseView.field(); + + const formSettings = form ? form.settings || {} : {}; + + let templateLabel = ""; + + if (formSettings.showLabel) { + if (formSettings.labelPosition === "top") + templateLabel = ``; + else + templateLabel = ``; + } + + let newWidth = formSettings.labelWidth; + + if (this.settings && this.settings.formView) newWidth += 40; + else if ( + formSettings.showLabel && + formSettings.labelPosition === "top" + ) + newWidth = 0; + + const template = + `
    ${templateLabel}#template#
    `.replace( + /#template#/g, + field + .columnHeader({ + width: newWidth, + editable: true, + }) + .template(rowData) + ); + + // Re-build template element + $$(this.ids.template)?.setHTML(template); + } + }; +} diff --git a/AppBuilder/platform/plugins/included/view_form/viewComponent/FNAbviewformSelectMultipleComponent.js b/AppBuilder/platform/plugins/included/view_form/viewComponent/FNAbviewformSelectMultipleComponent.js new file mode 100644 index 00000000..97087930 --- /dev/null +++ b/AppBuilder/platform/plugins/included/view_form/viewComponent/FNAbviewformSelectMultipleComponent.js @@ -0,0 +1,96 @@ +export default function FNAbviewformSelectMultipleComponent({ + ABViewFormItemComponent, +}) { + return class ABViewFormSelectMultipleComponent extends ABViewFormItemComponent { + constructor(baseView, idBase, ids) { + super( + baseView, + idBase || `ABViewFormSelectMultiple_${baseView.id}`, + ids + ); + } + + ui() { + const baseView = this.view; + const field = baseView.field(), + settings = this.settings; + const options = []; + + if (field?.key === "user") options.push(...field.getUsers()); + else if (field) + options.push(...(field.settings.options ?? settings.options ?? [])); + + const ids = this.ids; + const _ui = { + id: ids.formItem, + view: settings.type || baseView.constructor.defaultValues().type, + options: options.map((opt) => { + return { + id: opt.id, + value: opt.text, + hex: opt.hex, + }; + }), + }; + + switch (_ui.view) { + case "multicombo": + _ui.tagMode = false; + _ui.css = "hideWebixMulticomboTag"; + _ui.tagTemplate = (values) => { + const selectedOptions = []; + const $formItem = $$(ids.formItem) ?? $$(_ui.id); + + values.forEach((val) => { + selectedOptions.push($formItem.getList().getItem(val)); + }); + + let vals = selectedOptions; + + if (field.getSelectedOptions) + vals = field.getSelectedOptions(field, selectedOptions); + + const items = []; + + vals.forEach((val) => { + let hasCustomColor = ""; + let optionHex = ""; + + if (field.settings.hasColors && val.hex) { + hasCustomColor = "hascustomcolor"; + optionHex = `background: ${val.hex};`; + } + + const text = val.text ? val.text : val.value; + + items.push( + `${text}` + ); + }); + + return items.join(""); + }; + + break; + + case "checkbox": + // radio element could not be empty options + _ui.options.push({ + id: "temp", + value: this.label("Option"), + }); + + break; + } + + return super.ui(_ui); + } + + getValue(rowData) { + const field = this.view.field(), + $formItem = $$(this.ids.formItem); + + return field.getValue($formItem, rowData); + } + }; +} diff --git a/AppBuilder/platform/plugins/included/view_form/viewComponent/FNAbviewformSelectSingleComponent.js b/AppBuilder/platform/plugins/included/view_form/viewComponent/FNAbviewformSelectSingleComponent.js new file mode 100644 index 00000000..50afc2c1 --- /dev/null +++ b/AppBuilder/platform/plugins/included/view_form/viewComponent/FNAbviewformSelectSingleComponent.js @@ -0,0 +1,78 @@ +export default function FNAbviewformSelectSingleComponent({ + ABViewFormItemComponent, +}) { + return class ABViewFormSelectSingleComponent extends ABViewFormItemComponent { + constructor(baseView, idBase, ids) { + super( + baseView, + idBase || `ABViewFormSelectSingle_${baseView.id}`, + ids + ); + } + + ui() { + const baseView = this.view; + const field = baseView.field(), + settings = baseView.settings || this.settings; + const options = []; + + if (field?.key === "user") options.push(...field.getUsers()); + else if (field) + options.push(...(field.settings.options ?? settings.options ?? [])); + else options.push(...(settings.options ?? [])); + + const _ui = { + view: settings.type || baseView.constructor.defaultValues().type, + }; + + if (field?.settings.hasColors) { + _ui.css = "combowithcolors"; + _ui.options = { + view: "suggest", + body: { + view: "list", + data: options.map((opt) => { + return { + id: opt.id, + value: opt.text || opt.value, + hex: field.settings.hasColors ? opt.hex : "", + }; + }), + template: function (value) { + const items = []; + + let hasCustomColor = ""; + let optionHex = ""; + + if (value.hex) { + hasCustomColor = "hascustomcolor"; + optionHex = `background: ${value.hex};`; + } + + items.push( + `${value.value}` + ); + + return items.join(""); + }, + }, + }; + } else + _ui.options = options.map((opt) => { + return { + id: opt.id, + value: opt.text || opt.value, + }; + }); + + // radio element could not be empty options + if (_ui.view === "radio" && _ui.options.length < 1) + _ui.options.push({ + id: "temp", + value: this.AB ? this.AB.Label()("Option") : "Option", // Safe fallback + }); + + return super.ui(_ui); + } + }; +} diff --git a/AppBuilder/platform/plugins/included/view_form/viewComponent/FNAbviewformTextboxComponent.js b/AppBuilder/platform/plugins/included/view_form/viewComponent/FNAbviewformTextboxComponent.js new file mode 100644 index 00000000..fd865b17 --- /dev/null +++ b/AppBuilder/platform/plugins/included/view_form/viewComponent/FNAbviewformTextboxComponent.js @@ -0,0 +1,81 @@ +export default function FNAbviewformTextboxComponent({ + ABViewFormItemComponent, +}) { + return class ABViewFormTextboxComponent extends ABViewFormItemComponent { + constructor(baseView, idBase, ids) { + super(baseView, idBase || `ABViewFormTextbox_${baseView.id}`, ids); + this.type = + this.settings.type || + this.view.settings.type || + this.view.constructor.defaultValues().type; + } + + ui() { + const _ui = {}; + + switch (this.type) { + case "single": + _ui.view = "text"; + break; + case "multiple": + _ui.view = "textarea"; + _ui.height = 200; + break; + case "rich": + _ui.view = "forminput"; + _ui.height = 200; + _ui.css = "ab-rich-text"; + _ui.body = { + view: "tinymce-editor", + value: "", + cdn: "/js/webix/extras/tinymce", + config: { + plugins: "link", + menubar: "format edit", + toolbar: + "undo redo | styles | bold italic | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | fontsize | link", + }, + }; + break; + } + + return super.ui(_ui); + } + + async onShow() { + if (this.type !== "rich") return; + await this.initTinyMCE(); + const _ui = this.ui(); + const _uiFormItem = _ui.rows[0]; + let $formItem = $$(this.ids.formItem); + + // WORKAROUND : to fix breaks TinyMCE when switch pages/tabs + // https://forum.webix.com/discussion/6772/switching-tabs-breaks-tinymce + if ($formItem) { + // recreate rich editor + $formItem = this.AB.Webix.ui(_uiFormItem, $formItem); + + // Add dataCy to TinyMCE text editor + const baseView = this.view; + + $formItem + .getChildViews()[0] + .getEditor(true) + .then((editor) => { + const dataCy = `${baseView.key} rich ${_uiFormItem.name} ${ + baseView.id ?? "" + } ${baseView.parent?.id ?? ""}`; + + editor.contentAreaContainer.setAttribute("data-cy", dataCy); + }); + } + } + + /** + * Ensure TinyMCE has been loaded and initialized. + */ + async initTinyMCE() { + await this.AB.custom["tinymce-editor"].init(); + } + }; +} diff --git a/AppBuilder/platform/plugins/included/view_form/viewComponent/FNAbviewformTreeComponent.js b/AppBuilder/platform/plugins/included/view_form/viewComponent/FNAbviewformTreeComponent.js new file mode 100644 index 00000000..b2f7affa --- /dev/null +++ b/AppBuilder/platform/plugins/included/view_form/viewComponent/FNAbviewformTreeComponent.js @@ -0,0 +1,118 @@ +export default function FNAbviewformTreeComponent({ ABViewFormItemComponent }) { + return class ABViewFormTreeComponent extends ABViewFormItemComponent { + constructor(baseView, idBase, ids) { + super(baseView, idBase || `ABViewFormTree_${baseView.id}`, ids); + } + + ui() { + const self = this; + const baseView = this.view; + const field = baseView.field(); + + const _ui = { + label: "", + labelWidth: 0, + }; + + // this field may be deleted + if (!field) return super.ui(_ui); + + const form = baseView.parentFormComponent(); + const formSettings = form ? form.settings || {} : {}; + + const requiredClass = + field.settings.required === 1 ? "webix_required" : ""; + + let templateLabel = ""; + + if (formSettings.showLabel) { + if (formSettings.labelPosition === "top") + templateLabel = ``; + else + templateLabel = ``; + } + + let newWidth = formSettings.labelWidth; + + if (baseView.settings && baseView.settings.formView) newWidth += 40; + + _ui.view = "template"; + _ui.css = "webix_el_box"; + _ui.height = + field.settings.useHeight === 1 + ? parseInt(field.settings.imageHeight) + : 38; + _ui.borderless = true; + + _ui.template = (obj) => { + let val = self._value || ""; + + if (typeof val == "string" && val.indexOf("[") === 0) { + try { + val = JSON.parse(val); + } catch (e) { + /* ignore */ + } + } + + const rowData = { [field.columnName]: val }; + const template = field + .columnHeader({ + width: newWidth, + }) + .template(rowData); + + return `
    ${templateLabel}${template}
    `; + }; + + _ui.onClick = { + customField: (id, e, trg) => { + const node = $$(this.ids.formItem).$view; + + field.customEdit( + { [field.columnName]: self.getValue() }, + this.AB._App, + node, + this + ); + }, + }; + + _ui.on = { + onAfterRender: function () { + if (this.config.value) { + self._value = this.config.value; + } + + this.setValue = (vals) => { + self._value = vals; + this.refresh(); + }; + this.getValue = () => { + return self._value || ""; + }; + this.setValues = (vals) => { + this.setValue(vals); + }; + this.getValues = () => { + return this.getValue(); + }; + }, + }; + + return super.ui(_ui); + } + + getValue(rowData) { + const $formItem = $$(this.ids.formItem); + if (!$formItem) return ""; + + let vals = $formItem.getValue(); + + // Pass empty string if the returned values is empty array + if (Array.isArray(vals) && vals.length === 0) vals = ""; + + return vals; + } + }; +} diff --git a/AppBuilder/platform/views/ABViewForm.js b/AppBuilder/platform/views/ABViewForm.js deleted file mode 100644 index 6b36da57..00000000 --- a/AppBuilder/platform/views/ABViewForm.js +++ /dev/null @@ -1,584 +0,0 @@ -const ABViewFormCore = require("../../core/views/ABViewFormCore"); -const ABViewFormComponent = require("./viewComponent/ABViewFormComponent"); -const ABViewFormButton = require("./ABViewFormButton"); -const ABViewFormCustom = require("./ABViewFormCustom"); -const ABViewFormConnect = require("./ABViewFormConnect"); -const ABViewFormDatepicker = require("./ABViewFormDatepicker"); -const ABViewFormSelectMultiple = require("./ABViewFormSelectMultiple"); -const ABViewFormTextbox = require("./ABViewFormTextbox"); -const ABViewFormJson = require("./ABViewFormJson"); - -const L = (...params) => AB.Multilingual.label(...params); - -// const ABRecordRule = require("../../rules/ABViewRuleListFormRecordRules"); -// const ABSubmitRule = require("../../rules/ABViewRuleListFormSubmitRules"); - -// let PopupRecordRule = null; -// let PopupSubmitRule = null; - -const ABViewFormPropertyComponentDefaults = ABViewFormCore.defaultValues(); - -module.exports = class ABViewForm extends ABViewFormCore { - constructor(values, application, parent, defaultValues) { - super(values, application, parent, defaultValues); - - this._callbacks = { - onBeforeSaveData: () => true, - }; - } - - superComponent() { - if (this._superComponent == null) - this._superComponent = super.component(); - - return this._superComponent; - } - - /** - * @method component() - * return a UI component based upon this view. - * @return {obj} UI component - */ - component() { - return new ABViewFormComponent(this); - } - - refreshDefaultButton(ids) { - // If default button is not exists, then skip this - let defaultButton = this.views( - (v) => v instanceof ABViewFormButton && v.settings.isDefault - )[0]; - - // Add a default button - if (defaultButton == null) { - defaultButton = ABViewFormButton.newInstance(this.application, this); - defaultButton.settings.isDefault = true; - } - // Remove default button from array, then we will add it to be the last item later (.push) - else { - this._views = this.views((v) => v.id != defaultButton.id); - } - - // Calculate position Y of the default button - let yList = this.views().map((v) => (v.position.y || 0) + 1); - yList.push(this._views.length || 0); - yList.push($$(ids.fields).length || 0); - let posY = Math.max(...yList); - - // Update to be the last item - defaultButton.position.y = posY; - - // Keep the default button is always the last item of array - this._views.push(defaultButton); - - return defaultButton; - } - - /** - * @method getFormValues - * - * @param {webix form} formView - * @param {ABObject} obj - * @param {ABDatacollection} dc - * @param {ABDatacollection} dcLink [optional] - */ - getFormValues(formView, obj, dc, dcLink) { - // get the fields that are on this form - const visibleFields = ["id"]; // we always want the id so we can udpate records - formView.getValues(function (obj) { - visibleFields.push(obj.config.name); - }); - - // only get data passed from form - const allVals = formView.getValues(); - const formVals = {}; - visibleFields.forEach((val) => { - formVals[val] = allVals[val]; - }); - - // get custom values - this.fieldComponents( - (comp) => - comp instanceof ABViewFormCustom || - comp instanceof ABViewFormConnect || - comp instanceof ABViewFormDatepicker || - comp instanceof ABViewFormSelectMultiple || - (comp instanceof ABViewFormJson && comp.settings.type == "filter") - ).forEach((f) => { - const vComponent = this.viewComponents[f.id]; - if (vComponent == null) return; - - const field = f.field(); - if (field) { - const getValue = vComponent.getValue ?? vComponent.logic.getValue; - if (getValue) - formVals[field.columnName] = getValue.call(vComponent, formVals); - } - }); - - // remove connected fields if they were not on the form and they are present in the formVals because it is a datacollection - obj.connectFields().forEach((f) => { - if ( - visibleFields.indexOf(f.columnName) == -1 && - formVals[f.columnName] - ) { - delete formVals[f.columnName]; - delete formVals[f.relationName()]; - } - }); - - // clear undefined values or empty arrays - for (const prop in formVals) { - if (formVals[prop] == null || formVals[prop].length == 0) - formVals[prop] = ""; - } - - // Add parent's data collection cursor when a connect field does not show - let linkValues; - - if (dcLink) { - linkValues = dcLink.getCursor(); - } - - if (linkValues) { - const objectLink = dcLink.datasource; - - const connectFields = obj.connectFields(); - connectFields.forEach((f) => { - const formFieldCom = this.fieldComponents( - (fComp) => fComp?.field?.()?.id === f?.id - ); - - if ( - objectLink.id == f.settings.linkObject && - formFieldCom.length < 1 && // check field does not show - formVals[f.columnName] === undefined - ) { - const linkColName = f.indexField - ? f.indexField.columnName - : objectLink.PK(); - - formVals[f.columnName] = {}; - formVals[f.columnName][linkColName] = - linkValues[linkColName] ?? linkValues.id; - } - }); - } - - // NOTE: need to pull data of current cursor to calculate Calculate & Formula fields - // .formVals variable does not include data that does not display in the Form widget - const cursorFormVals = Object.assign(dc.getCursor() ?? {}, formVals); - - // Set value of calculate or formula fields to use in record rule - obj.fields((f) => f.key == "calculate" || f.key == "formula").forEach( - (f) => { - if (formVals[f.columnName] == null) { - let reCalculate = true; - - // WORKAROUND: If "Formula" field will have Filter conditions, - // Then it is not able to re-calculate on client side - // because relational data is not full data so FilterComplex will not have data to check - if (f.key == "formula" && f.settings?.where?.rules?.length > 0) { - reCalculate = false; - } - - formVals[f.columnName] = f.format(cursorFormVals, reCalculate); - } - } - ); - - if (allVals.translations?.length > 0) - formVals.translations = allVals.translations; - - // give the Object a final chance to review the data being handled. - obj.formCleanValues(formVals); - - return formVals; - } - - /** - * @method validateData - * - * @param {webix form} formView - * @param {ABObject} object - * @param {object} formVals - * - * @return {boolean} isValid - */ - validateData($formView, object, formVals) { - let list = ""; - - // validate required fields - const requiredFields = this.fieldComponents( - (fComp) => - fComp?.field?.().settings?.required == true || - fComp?.settings?.required == true - ).map((fComp) => fComp.field()); - - // validate data - const validator = object.isValidData(formVals); - let isValid = validator.pass(); - - // $$($formView).validate(); - $formView.validate(); - /** - * helper function to fix the webix ui after adding an validation error - * message. - * @param {string} col - field.columnName - */ - const fixInvalidMessageUI = (col) => { - const $forminput = $formView.elements[col]; - if (!$forminput) return; - // Y position - const height = $forminput.$height; - if (height < 56) { - $forminput.define("height", 60); - $forminput.resize(); - } - - // X position - const domInvalidMessage = $forminput.$view.getElementsByClassName( - "webix_inp_bottom_label" - )[0]; - if (!domInvalidMessage?.style["margin-left"]) { - domInvalidMessage.style.marginLeft = `${ - this.settings.labelWidth ?? - ABViewFormPropertyComponentDefaults.labelWidth - }px`; - } - }; - - // Display required messages - requiredFields.forEach((f) => { - if (!f) return; - - const fieldVal = formVals[f.columnName]; - if (fieldVal == "" || fieldVal == null || fieldVal.length < 1) { - $formView.markInvalid(f.columnName, L("This is a required field.")); - list += `
  • ${L("Missing Required Field")} ${f.columnName}
  • `; - isValid = false; - - // Fix position of invalid message - fixInvalidMessageUI(f.columnName); - } - }); - - // if data is invalid - if (!isValid) { - const saveButton = $formView.queryView({ - view: "button", - type: "form", - }); - - // error message - if (validator?.errors?.length) { - validator.errors.forEach((err) => { - $formView.markInvalid(err.name, err.message); - list += `
  • ${err.name}: ${err.message}
  • `; - fixInvalidMessageUI(err.name); - }); - - saveButton?.disable(); - } else { - saveButton?.enable(); - } - } - if (list) { - webix.alert({ - type: "alert-error", - title: L("Problems Saving"), - width: 400, - text: ``, - }); - } - - return isValid; - } - - /** - * @method recordRulesReady() - * This returns a Promise that gets resolved when all record rules report - * that they are ready. - * @return {Promise} - */ - async recordRulesReady() { - return this.RecordRule.rulesReady(); - } - - /** - * @method saveData - * save data in to database - * @param $formView - webix's form element - * - * @return {Promise} - */ - async saveData($formView) { - // call .onBeforeSaveData event - // if this function returns false, then it will not go on. - if (!this._callbacks?.onBeforeSaveData?.()) return; - - $formView.clearValidation(); - - // get ABDatacollection - const dv = this.datacollection; - if (dv == null) return; - - // get ABObject - const obj = dv.datasource; - if (obj == null) return; - - // show progress icon - $formView.showProgress?.({ type: "icon" }); - - // get update data - const formVals = this.getFormValues( - $formView, - obj, - dv, - dv.datacollectionLink - ); - - // form ready function - const formReady = (newFormVals) => { - // clear cursor after saving. - if (dv) { - if (this.settings.clearOnSave) { - dv.setCursor(null); - $formView.clear(); - } else { - if (newFormVals && newFormVals.id) dv.setCursor(newFormVals.id); - } - } - - $formView.hideProgress?.(); - - // if there was saved data pass it up to the onSaveData callback - // if (newFormVals) this._logic.callbacks.onSaveData(newFormVals); - if (newFormVals) this.emit("saved", newFormVals); // Q? is this the right upgrade? - }; - - const formError = (err) => { - const $saveButton = $formView.queryView({ - view: "button", - type: "form", - }); - - // mark error - if (err) { - if (err.invalidAttributes) { - for (const attr in err.invalidAttributes) { - let invalidAttrs = err.invalidAttributes[attr]; - if (invalidAttrs && invalidAttrs[0]) - invalidAttrs = invalidAttrs[0]; - - $formView.markInvalid(attr, invalidAttrs.message); - } - } else if (err.sqlMessage) { - webix.message({ - text: err.sqlMessage, - type: "error", - }); - } else { - webix.message({ - text: L("System could not save your data"), - type: "error", - }); - this.AB.notify.developer(err, { - message: "Could not save your data", - view: this.toObj(), - }); - } - } - - $saveButton?.enable(); - - $formView?.hideProgress?.(); - }; - - // Load data of DCs that use in record rules - await this.loadDcDataOfRecordRules(); - - // wait for our Record Rules to be ready before we continue. - await this.recordRulesReady(); - - // update value from the record rule (pre-update) - this.doRecordRulesPre(formVals); - - // validate data - if (!this.validateData($formView, obj, formVals)) { - // console.warn("Data is invalid."); - $formView.hideProgress?.(); - return; - } - let newFormVals; - try { - newFormVals = await this.submitValues(formVals); - } catch (err) { - formError(err.data); - return; - } - // {obj} - // The fully populated values returned back from service call - // We use this in our post processing Rules - - /* - // OLD CODE: - try { - await this.doRecordRules(newFormVals); - // make sure any updates from RecordRules get passed along here. - this.doSubmitRules(newFormVals); - formReady(newFormVals); - return newFormVals; - } catch (err) { - this.AB.notify.developer(err, { - message: "Error processing Record Rules.", - view: this.toObj(), - newFormVals: newFormVals, - }); - // Question: how do we respond to an error? - // ?? just keep going ?? - this.doSubmitRules(newFormVals); - formReady(newFormVals); - return; - } - */ - - try { - await this.doRecordRules(newFormVals); - } catch (err) { - this.AB.notify.developer(err, { - message: "Error processing Record Rules.", - view: this.toObj(), - newFormVals: newFormVals, - }); - } - - // make sure any updates from RecordRules get passed along here. - try { - this.doSubmitRules(newFormVals); - } catch (errs) { - this.AB.notify.developer(errs, { - message: "Error processing Submit Rules.", - view: this.toObj(), - newFormVals: newFormVals, - }); - } - - formReady(newFormVals); - return newFormVals; - } - - focusOnFirst() { - let topPosition = 0; - let topPositionId = ""; - this.views().forEach((item) => { - if (item.key == "textbox" || item.key == "numberbox") { - if (item.position.y == topPosition) { - // topPosition = item.position.y; - topPositionId = item.id; - } - } - }); - let childComponent = this.viewComponents[topPositionId]; - if (childComponent && $$(childComponent.ui.id)) { - $$(childComponent.ui.id).focus(); - } - } - - async loadDcDataOfRecordRules() { - const tasks = []; - - (this.settings?.recordRules ?? []).forEach((rule) => { - (rule?.actionSettings?.valueRules?.fieldOperations ?? []).forEach( - (op) => { - if (op.valueType !== "exist") return; - - const pullDataDC = this.AB.datacollectionByID(op.value); - - if ( - pullDataDC?.dataStatus === - pullDataDC.dataStatusFlag.notInitial - ) - tasks.push(pullDataDC.loadData()); - } - ); - }); - - await Promise.all(tasks); - - return true; - } - - get viewComponents() { - const superComponent = this.superComponent(); - return superComponent.viewComponents; - } - - warningsEval() { - super.warningsEval(); - - let DC = this.datacollection; - if (!DC) { - this.warningsMessage( - `can't resolve it's datacollection[${this.settings.dataviewID}]` - ); - } - - if (this.settings.recordRules) { - // TODO: scan recordRules for warnings - } - - if (this.settings.submitRules) { - // TODO: scan submitRules for warnings. - } - } - - async submitValues(formVals) { - // get ABModel - const model = this.datacollection.model; - if (model == null) return; - - // is this an update or create? - if (formVals.id) { - return await model.update(formVals.id, formVals); - } else { - return await model.create(formVals); - } - } - - /** - * @method deleteData - * delete data in to database - * @param $formView - webix's form element - * - * @return {Promise} - */ - async deleteData($formView) { - // get ABDatacollection - const dc = this.datacollection; - if (dc == null) return; - - // get ABObject - const obj = dc.datasource; - if (obj == null) return; - - // get ABModel - const model = dc.model; - if (model == null) return; - - // get update data - const formVals = $formView.getValues(); - - if (formVals?.id) { - const result = await model.delete(formVals.id); - - // clear form - if (result) { - dc.setCursor(null); - $formView.clear(); - } - - return result; - } - } -}; diff --git a/AppBuilder/platform/views/ABViewFormButton.js b/AppBuilder/platform/views/ABViewFormButton.js deleted file mode 100644 index 3a9450de..00000000 --- a/AppBuilder/platform/views/ABViewFormButton.js +++ /dev/null @@ -1,13 +0,0 @@ -const ABViewFormButtonCore = require("../../core/views/ABViewFormButtonCore"); -const ABViewFormButtonComponent = require("./viewComponent/ABViewFormButtonComponent"); - -module.exports = class ABViewFormButton extends ABViewFormButtonCore { - /** - * @method component() - * return a UI component based upon this view. - * @return {obj} UI component - */ - component() { - return new ABViewFormButtonComponent(this); - } -}; diff --git a/AppBuilder/platform/views/ABViewFormConnect.js b/AppBuilder/platform/views/ABViewFormConnect.js deleted file mode 100644 index d9687016..00000000 --- a/AppBuilder/platform/views/ABViewFormConnect.js +++ /dev/null @@ -1,102 +0,0 @@ -const ABViewFormConnectCore = require("../../core/views/ABViewFormConnectCore"); -const ABViewFormConnectComponent = require("./viewComponent/ABViewFormConnectComponent"); -const ABViewPropertyAddPage = - require("./viewProperties/ABViewPropertyAddPage").default; -const ABViewPropertyEditPage = - require("./viewProperties/ABViewPropertyEditPage").default; - -const ABViewFormConnectPropertyComponentDefaults = - ABViewFormConnectCore.defaultValues(); - -module.exports = class ABViewFormConnect extends ABViewFormConnectCore { - /** - * @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); - - // Set filter value - this.__filterComponent = this.AB.filterComplexNew( - `${this.id}__filterComponent` - ); - // this.__filterComponent.applicationLoad(application); - this.__filterComponent.fieldsLoad( - this.datasource ? this.datasource.fields() : [], - this.datasource ? this.datasource : null - ); - - // NOTE: .objectWorkspace is a v1 setting - // if ( - // !this.settings.objectWorkspace || - // !this.settings.objectWorkspace.filterConditions - // ) { - // this.AB.error("Error: filter conditions do not exist", { - // error: "filterConditions do not exist", - // viewLocation: { - // application: this.application.name, - // id: this.id, - // name: this.label, - // }, - // view: this, - // }); - // // manually place an empty filter - // this.settings["objectWorkspace"] = {}; - // this.settings["objectWorkspace"]["filterConditions"] = { glue: "and" }; - // } - - this.__filterComponent.setValue( - this.settings.filterConditions ?? - ABViewFormConnectPropertyComponentDefaults.filterConditions - ); - } - - /// - /// Instance Methods - /// - - /** - * @method fromValues() - * - * initialze this object with the given set of values. - * @param {obj} values - */ - fromValues(values) { - super.fromValues(values); - - this.addPageTool.fromSettings(this.settings); - this.editPageTool.fromSettings(this.settings); - } - - static get addPageProperty() { - return ABViewPropertyAddPage.propertyComponent(this.App, this.idBase); - } - - static get editPageProperty() { - return ABViewPropertyEditPage.propertyComponent(this.App, this.idBase); - } - - /** - * @method component() - * return a UI component based upon this view. - * @return {obj} UI component - */ - component() { - return new ABViewFormConnectComponent(this); - } - - get addPageTool() { - if (this.__addPageTool == null) - this.__addPageTool = new ABViewPropertyAddPage(); - - return this.__addPageTool; - } - - get editPageTool() { - if (this.__editPageTool == null) - this.__editPageTool = new ABViewPropertyEditPage(); - - return this.__editPageTool; - } -}; diff --git a/AppBuilder/platform/views/ABViewFormCustom.js b/AppBuilder/platform/views/ABViewFormCustom.js deleted file mode 100644 index 6d6e5c34..00000000 --- a/AppBuilder/platform/views/ABViewFormCustom.js +++ /dev/null @@ -1,13 +0,0 @@ -const ABViewFormCustomCore = require("../../core/views/ABViewFormCustomCore"); -const ABViewFormCustomComponent = require("./viewComponent/ABViewFormCustomComponent"); - -module.exports = class ABViewFormCustom extends ABViewFormCustomCore { - /** - * @method component() - * return a UI component based upon this view. - * @return {obj} UI component - */ - component() { - return new ABViewFormCustomComponent(this); - } -}; diff --git a/AppBuilder/platform/views/ABViewFormDatepicker.js b/AppBuilder/platform/views/ABViewFormDatepicker.js deleted file mode 100644 index 8047add4..00000000 --- a/AppBuilder/platform/views/ABViewFormDatepicker.js +++ /dev/null @@ -1,13 +0,0 @@ -const ABViewFormDatepickerCore = require("../../core/views/ABViewFormDatepickerCore"); -const ABViewFormDatepickerComponent = require("./viewComponent/ABViewFormDatepickerComponent"); - -module.exports = class ABViewFormDatepicker extends ABViewFormDatepickerCore { - /** - * @method component() - * return a UI component based upon this view. - * @return {obj} UI component - */ - component() { - return new ABViewFormDatepickerComponent(this); - } -}; diff --git a/AppBuilder/platform/views/ABViewFormItem.js b/AppBuilder/platform/views/ABViewFormItem.js deleted file mode 100644 index ecc0d404..00000000 --- a/AppBuilder/platform/views/ABViewFormItem.js +++ /dev/null @@ -1,42 +0,0 @@ -const ABViewFormItemCore = require("../../core/views/ABViewFormItemCore"); -const ABViewFormItemComponent = require("./viewComponent/ABViewFormItemComponent"); - -const ABViewFormFieldPropertyComponentDefaults = - ABViewFormItemCore.defaultValues(); - -module.exports = class ABViewFormItem extends ABViewFormItemCore { - // constructor(values, application, parent, defaultValues) { - // super(values, application, parent, defaultValues); - // } - - static get componentUI() { - return ABViewFormItemComponent; - } - - /** - * @method component() - * return a UI component based upon this view. - * @return {obj} UI component - */ - component() { - return new ABViewFormItemComponent(this); - } - - /** - * @method parentFormUniqueID - * return a unique ID based upon the closest form object this component is on. - * @param {string} key The basic id string we will try to make unique - * @return {string} - */ - parentFormUniqueID(key) { - var form = this.parentFormComponent(); - var uniqueInstanceID; - if (form) { - uniqueInstanceID = form.uniqueInstanceID; - } else { - uniqueInstanceID = webix.uid(); - } - - return key + uniqueInstanceID; - } -}; diff --git a/AppBuilder/platform/views/ABViewFormJson.js b/AppBuilder/platform/views/ABViewFormJson.js deleted file mode 100644 index 528e110a..00000000 --- a/AppBuilder/platform/views/ABViewFormJson.js +++ /dev/null @@ -1,13 +0,0 @@ -const ABViewFormJsonCore = require("../../core/views/ABViewFormJsonCore"); -const ABViewFormJsonComponent = require("./viewComponent/ABViewFormJsonComponent"); - -module.exports = class ABViewFormJson extends ABViewFormJsonCore { - /** - * @method component() - * return a UI component based upon this view. - * @return {obj} UI component - */ - component() { - return new ABViewFormJsonComponent(this); - } -}; diff --git a/AppBuilder/platform/views/ABViewFormSelectMultiple.js b/AppBuilder/platform/views/ABViewFormSelectMultiple.js deleted file mode 100644 index fbc5f121..00000000 --- a/AppBuilder/platform/views/ABViewFormSelectMultiple.js +++ /dev/null @@ -1,15 +0,0 @@ -const ABViewFormSelectMultipleCore = require("../../core/views/ABViewFormSelectMultipleCore"); -const ABViewFormSelectMultipleComponent = require("./viewComponent/ABViewFormSelectMultipleComponent"); - -module.exports = class ABViewFormSelectMultiple extends ( - ABViewFormSelectMultipleCore -) { - /** - * @method component() - * return a UI component based upon this view. - * @return {obj} UI component - */ - component() { - return new ABViewFormSelectMultipleComponent(this); - } -}; diff --git a/AppBuilder/platform/views/ABViewFormTextbox.js b/AppBuilder/platform/views/ABViewFormTextbox.js deleted file mode 100644 index 40a06733..00000000 --- a/AppBuilder/platform/views/ABViewFormTextbox.js +++ /dev/null @@ -1,13 +0,0 @@ -const ABViewFormTextboxCore = require("../../core/views/ABViewFormTextboxCore"); -const ABViewFormTextboxComponent = require("./viewComponent/ABViewFormTextboxComponent"); - -module.exports = class ABViewFormTextbox extends ABViewFormTextboxCore { - /** - * @method component() - * return a UI component based upon this view. - * @return {obj} UI component - */ - component() { - return new ABViewFormTextboxComponent(this); - } -}; diff --git a/AppBuilder/platform/views/ABViewKanbanFormSidePanel.js b/AppBuilder/platform/views/ABViewKanbanFormSidePanel.js index a98847b3..ca1a074f 100644 --- a/AppBuilder/platform/views/ABViewKanbanFormSidePanel.js +++ b/AppBuilder/platform/views/ABViewKanbanFormSidePanel.js @@ -6,8 +6,6 @@ */ const ABViewComponent = require("./viewComponent/ABViewComponent").default; -const ABViewForm = require("./ABViewForm"); -const ABViewFormButton = require("./ABViewFormButton"); var L = null; // multilingual Label fn() @@ -131,7 +129,7 @@ module.exports = class ABWorkObjectKanBan extends ABViewComponent { let formAttrs = { id: `${this.ids.component}_sideform`, - key: ABViewForm.common().key, + key: "form", settings: { columns: 1, labelPosition: "top", @@ -157,22 +155,17 @@ module.exports = class ABWorkObjectKanBan extends ABViewComponent { }); // add default button (Save button) - form._views.push( - new ABViewFormButton( - { - settings: { - includeSave: true, - includeCancel: false, - includeReset: false, - }, - position: { - y: CurrentObject.fields().length, // yPosition - }, - }, - this._mockApp, - form - ) - ); + form.viewNew({ + key: "button", + settings: { + includeSave: true, + includeCancel: false, + includeReset: false, + }, + position: { + y: CurrentObject.fields().length, // yPosition + }, + }); // add temp id to views form._views.forEach( diff --git a/AppBuilder/platform/views/viewComponent/ABViewContainerComponent.js b/AppBuilder/platform/views/viewComponent/ABViewContainerComponent.js index 90dceeb6..d7fa7779 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewContainerComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewContainerComponent.js @@ -88,6 +88,8 @@ module.exports = class ABViewContainerComponent extends ABViewComponent { const defaultSettings = this.view.constructor.defaultValues(); views.forEach((v) => { + if (v === this.view) return; + // let component = v.component(/* App, idPrefix */); // NOTE: PONG - Just temporary to be compatible old & new versions let component; diff --git a/AppBuilder/platform/views/viewComponent/ABViewDetailItemComponent.js b/AppBuilder/platform/views/viewComponent/ABViewDetailItemComponent.js index 2096c023..2a35d41c 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewDetailItemComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewDetailItemComponent.js @@ -119,7 +119,8 @@ module.exports = class ABViewDetailItemComponent extends ABViewComponent { switch (field?.key) { case "string": case "LongText": { - const strVal = val + const strVal = (val || "") + .toString() // Sanitize all of HTML tags .replace(/[<]/gm, "<") // Allow safe HTML tags @@ -131,6 +132,16 @@ module.exports = class ABViewDetailItemComponent extends ABViewComponent { $detailItem.setValues({ display: strVal }); break; } + case "json": { + let jsonVal = val; + + if (typeof val == "object") { + jsonVal = JSON.stringify(val, null, 2); + } + + $detailItem.setValues({ display: jsonVal }); + break; + } default: $detailItem.setValues({ display: val }); break; diff --git a/AppBuilder/platform/views/viewComponent/ABViewDetailTreeComponent.js b/AppBuilder/platform/views/viewComponent/ABViewDetailTreeComponent.js index 84a2d8da..ba3bae66 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewDetailTreeComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewDetailTreeComponent.js @@ -28,9 +28,36 @@ module.exports = class ABViewDetailTreeComponent extends ( setValue(val) { // convert value to array - const vals = []; - - if (val && !Array.isArray(val)) vals.push(val); + let vals = []; + + if (Array.isArray(val)) { + vals = val; + } else if (val) { + // if it is the initial html string, then just set it and return + if (typeof val == "string" && val.indexOf(this.className) > -1) { + super.setValue(val); + return; + } + + try { + const parsed = JSON.parse(val); + + if (Array.isArray(parsed)) { + vals = parsed; + } else { + vals.push(parsed); + } + } catch (e) { + if (typeof val == "string") + vals = val.split(",").filter((v) => v !== ""); + else vals.push(val); + } + + // Normalize all entries to IDs + vals = vals.map((v) => + v && typeof v === "object" && v.id ? v.id : v + ); + } setTimeout(() => { // get tree dom @@ -41,27 +68,25 @@ module.exports = class ABViewDetailTreeComponent extends ( const field = this.view.field(); const branches = []; - if (typeof field.settings.options.data === "undefined") - field.settings.options = new this.AB.Webix.TreeCollection({ - data: field.settings.options, - }); + let selectOptions = this.AB.cloneDeep(field.settings.options); + + selectOptions = new this.AB.Webix.TreeCollection({ + data: selectOptions, + }); - field.settings.options.data.each(function (obj) { - if (vals.indexOf(obj.id) !== -1) { + selectOptions.data.each(function (obj) { + if (vals.some((v) => v == obj.id)) { let html = ""; let rootid = obj.id; - while (this.getParentId(rootid)) { - field.settings.options.data.each(function (par) { - if ( - field.settings.options.data.getParentId(rootid) === - par.id - ) { + while (selectOptions.data.getParentId(rootid)) { + selectOptions.data.each(function (par) { + if (selectOptions.data.getParentId(rootid) === par.id) { html = `${par.text}: ${html}`; } }); - rootid = this.getParentId(rootid); + rootid = selectOptions.data.getParentId(rootid); } html += obj.text; diff --git a/AppBuilder/platform/views/viewComponent/ABViewFormButtonComponent.js b/AppBuilder/platform/views/viewComponent/ABViewFormButtonComponent.js deleted file mode 100644 index c5db07a0..00000000 --- a/AppBuilder/platform/views/viewComponent/ABViewFormButtonComponent.js +++ /dev/null @@ -1,239 +0,0 @@ -const ABViewFormItemComponent = require("./ABViewFormItemComponent"); - -module.exports = class ABViewFormButton extends ABViewFormItemComponent { - constructor(baseView, idBase, ids) { - super(baseView, idBase || `ABViewFormButton_${baseView.id}`, ids); - } - - ui() { - const self = this; - const baseView = this.view; - const form = baseView.parentFormComponent(); - const settings = baseView.settings ?? {}; - - const alignment = - settings.alignment || baseView.constructor.defaultValues().alignment; - - const _ui = { - cols: [], - }; - - // spacer - if (alignment === "center" || alignment === "right") { - _ui.cols.push({}); - } - - // delete button - if (settings.includeDelete) { - _ui.cols.push( - { - view: "button", - autowidth: true, - value: settings.deleteLabel || this.label("Delete"), - css: "webix_danger", - click: function () { - self.onDelete(this); - }, - on: { - onAfterRender: function () { - this.getInputNode().setAttribute( - "data-cy", - `button delete ${form.id}` - ); - }, - }, - }, - { - width: 10, - } - ); - } - - // cancel button - if (settings.includeCancel) { - _ui.cols.push( - { - view: "button", - autowidth: true, - value: settings.cancelLabel || this.label("Cancel"), - click: function () { - self.onCancel(this); - }, - on: { - onAfterRender: function () { - this.getInputNode().setAttribute( - "data-cy", - `button cancel ${form.id}` - ); - }, - }, - }, - { - width: 10, - } - ); - } - - // reset button - if (settings.includeReset) { - _ui.cols.push( - { - view: "button", - autowidth: true, - value: settings.resetLabel || this.label("Reset"), - click: function () { - self.onClear(this); - }, - on: { - onAfterRender: function () { - this.getInputNode().setAttribute( - "data-cy", - `button reset ${form.id}` - ); - }, - }, - }, - { - width: 10, - } - ); - } - - // save button - if (settings.includeSave) { - _ui.cols.push({ - view: "button", - type: "form", - css: "webix_primary", - autowidth: true, - value: settings.saveLabel || this.label("Save"), - click: function () { - self.onSave(this); - }, - on: { - onAfterRender: function () { - this.getInputNode().setAttribute( - "data-cy", - `button save ${form.id}` - ); - }, - }, - }); - } - - // spacer - if (alignment === "center" || alignment === "left") _ui.cols.push({}); - - return super.ui(_ui); - } - - onCancel(cancelButton) { - const baseView = this.view; - const settings = baseView.settings ?? {}; - - // get form component - const form = baseView.parentFormComponent(); - - // get ABDatacollection - const dc = form.datacollection; - - // clear cursor of DC if not set to follow another - if (!dc?.isCursorFollow) { - dc?.setCursor(null); - } - // dc?.setStaticCursor(); // unless it should be static - - cancelButton?.getFormView?.().clear(); - - if (settings.afterCancel) form.changePage(settings.afterCancel); - // If the redirect page is not defined, then redirect to parent page - else { - const noPopupFilter = (p) => p.settings && p.settings.type != "popup"; - - const pageCurr = this.view.pageParent(); - if (pageCurr) { - const pageParent = pageCurr.pageParent(noPopupFilter) ?? pageCurr; - - if (pageParent) form.changePage(pageParent.id); - } - } - } - - onClear(resetButton) { - // get form component - const form = this.view.parentFormComponent(); - - // get ABDatacollection - const dc = form.datacollection; - - // clear cursor of DC - if (dc) { - dc.setCursor(null); - } - - resetButton?.getFormView?.().clear(); - } - - onSave(saveButton) { - if (!saveButton) { - console.error("Require the button element"); - return; - } - // get form component - const form = this.view.parentFormComponent(); - const formView = saveButton.getFormView(); - - // disable the save button - saveButton.disable?.(); - - // save data - form - .saveData(formView) - .then(() => { - saveButton.enable?.(); - - //Focus on first focusable component - form.focusOnFirst(); - }) - .catch((err) => { - console.error(err); - // Catch uncaught error reported in Sentry and add context - // APPBUILDER-WEB-1A3(https://appdev-designs.sentry.io/issues/4631880265/) - try { - saveButton.enable?.(); - } catch (e) { - this.AB.notify.developer(e, { - context: - "formButton.onSave > catch err > saveButton.enable()", - buttonID: this?.view?.id, - formID: this?.view?.parent?.id, - }); - } - }); - } - - onDelete(deleteButton) { - this.AB.Webix.confirm({ - title: this.label("Delete data"), - text: this.label("Do you want to delete this data?"), - callback: async (confirm) => { - if (!confirm) return; - - deleteButton.disable?.(); - - try { - // get form component - const form = this.view.parentFormComponent(); - const $formView = deleteButton.getFormView(); - - // delete a record row - await form.deleteData($formView); - } catch (err) { - console.error(err); - } finally { - deleteButton.enable?.(); - } - }, - }); - } -}; diff --git a/AppBuilder/platform/views/viewComponent/ABViewFormComponent.js b/AppBuilder/platform/views/viewComponent/ABViewFormComponent.js deleted file mode 100644 index ddff2b1b..00000000 --- a/AppBuilder/platform/views/viewComponent/ABViewFormComponent.js +++ /dev/null @@ -1,586 +0,0 @@ -const ABViewComponent = require("./ABViewComponent").default; -const ABViewFormItem = require("../ABViewFormItem"); -const ABViewFormConnect = require("../ABViewFormConnect"); -const ABViewFormCustom = require("../ABViewFormCustom"); -const ABViewFormTextbox = require("../ABViewFormTextbox"); -const ABViewFormJson = require("../ABViewFormJson"); - -async function timeout(ms) { - return new Promise((resolve) => setTimeout(resolve, ms)); -} - -const fieldValidations = []; - -module.exports = class ABViewFormComponent extends ABViewComponent { - constructor(baseView, idBase, ids) { - super( - baseView, - idBase || `ABViewForm_${baseView.id}`, - Object.assign( - { - form: "", - - layout: "", - filterComplex: "", - }, - ids - ) - ); - - this.timerId = null; - this._showed = false; - } - - ui() { - const baseView = this.view; - const superComponent = baseView.superComponent(); - const rows = superComponent.ui().rows ?? []; - const fieldValidationsHolder = this.uiValidationHolder(); - const _ui = super.ui([ - { - id: this.ids.form, - view: "form", - abid: baseView.id, - rows: rows.concat(fieldValidationsHolder), - }, - ]); - - delete _ui.type; - - return _ui; - } - - uiValidationHolder() { - const result = [ - { - hidden: true, - rows: [], - }, - ]; - - // NOTE: this._currentObject can be set in the KanBan Side Panel - const baseView = this.view; - const object = this.datacollection?.datasource ?? baseView._currentObject; - - if (!object) return result; - - const validationUI = []; - const existsFields = baseView.fieldComponents(); - - object - // Pull fields that have validation rules - .fields((f) => f?.settings?.validationRules) - .forEach((f) => { - const view = existsFields.find( - (com) => f.id === com.settings.fieldId - ); - if (!view) return; - - // parse the rules because they were stored as a string - // check if rules are still a string...if so lets parse them - if (typeof f.settings.validationRules === "string") { - f.settings.validationRules = JSON.parse( - f.settings.validationRules - ); - } - - // there could be more than one so lets loop through and build the UI - f.settings.validationRules.forEach((rule, indx) => { - const Filter = this.AB.filterComplexNew( - `${f.columnName}_${indx}` - ); - // add the new ui to an array so we can add them all at the same time - if (typeof Filter.ui === "function") { - validationUI.push(Filter.ui()); - } else { - // Legacy v1 method: - validationUI.push(Filter.ui); - } - - // store the filter's info so we can assign values and settings after the ui is rendered - fieldValidations.push({ - filter: Filter, - view: Filter.ids.querybuilder, - columnName: f.columnName, - validationRules: rule.rules, - invalidMessage: rule.invalidMessage, - }); - }); - }); - - result.rows = validationUI; - - return result; - } - - async init(AB, accessLevel, options = {}) { - await super.init(AB); - - this.view.superComponent().init(AB, accessLevel, options); - - this.initCallbacks(options); - this.initEvents(); - this.initValidationRules(); - - const abWebix = this.AB.Webix; - const $form = $$(this.ids.form); - - if ($form) { - abWebix.extend($form, abWebix.ProgressBar); - } - - if (accessLevel < 2) $form.disable(); - } - - initCallbacks(options = {}) { - // ? We need to determine from these options whether to clear on load? - if (options?.clearOnLoad) { - // does this need to be a function? - this.view.settings.clearOnLoad = options.clearOnLoad(); - } - // Q: Should we use emit the event instead ? - const baseView = this.view; - - if (options.onBeforeSaveData) - baseView._callbacks.onBeforeSaveData = options.onBeforeSaveData; - else baseView._callbacks.onBeforeSaveData = () => true; - } - - initEvents() { - // bind a data collection to form component - const dc = this.datacollection; - - if (!dc) return; - - // listen DC events - ["changeCursor", "cursorStale"].forEach((key) => { - this.eventAdd({ - emitter: dc, - eventName: key, - listener: (rowData) => { - const baseView = this.view; - const linkViaOneConnection = baseView.fieldComponents( - (comp) => comp instanceof ABViewFormConnect - ); - // clear previous xxx->one selections and add new from - // cursor change - linkViaOneConnection.forEach((f) => { - const field = f.field(); - if ( - field?.settings?.linkViaType == "one" && - field?.linkViaOneValues - ) { - delete field.linkViaOneValues; - const relationVals = - rowData?.[field.relationName()] ?? - rowData?.[field.columnName]; - if (relationVals) { - if (Array.isArray(relationVals)) { - const valArray = []; - relationVals.forEach((v) => { - valArray.push( - field.getRelationValue(v, { forUpdate: true }) - ); - }); - field.linkViaOneValues = valArray.join(","); - } else { - field.linkViaOneValues = field.getRelationValue( - relationVals, - { forUpdate: true } - ); - } - } - } - }); - - this.displayData(rowData); - }, - }); - }); - - const ids = this.ids; - - this.eventAdd({ - emitter: dc, - eventName: "initializingData", - listener: () => { - const $form = $$(ids.form); - - if ($form) { - $form.disable(); - - $form.showProgress?.({ type: "icon" }); - } - }, - }); - - this.eventAdd({ - emitter: dc, - eventName: "initializedData", - listener: () => { - const $form = $$(ids.form); - - if ($form) { - $form.enable(); - - $form.hideProgress?.(); - } - }, - }); - - // I think this case is currently handled by the DC.[changeCursor, cursorStale] - // events: - // this.eventAdd({ - // emitter: dc, - // eventName: "ab.datacollection.update", - // listener: (msg, data) => { - // if (!data?.objectId) return; - - // const object = dc.datasource; - - // if (!object) return; - - // if ( - // object.id === data.objectId || - // object.fields((f) => f.settings.linkObject === data.objectId) - // .length > 0 - // ) { - // const currData = dc.getCursor(); - - // if (currData) this.displayData(currData); - // } - // }, - // }); - - // bind the cursor event of the parent DC - const linkDv = dc.datacollectionLink; - - if (linkDv) - // update the value of link field when data of the parent dc is changed - ["changeCursor", "cursorStale"].forEach((key) => { - this.eventAdd({ - emitter: linkDv, - eventName: key, - listener: (rowData) => { - this.displayParentData(rowData); - }, - }); - }); - } - - initValidationRules() { - const dc = this.datacollection; - - if (!dc) return; - - if (!fieldValidations.length) return; - - // we need to store the rules for use later so lets build a container array - const complexValidations = []; - - fieldValidations.forEach((f) => { - // init each ui to have the properties (app and fields) of the object we are editing - f.filter.applicationLoad?.(dc.datasource.application); // depreciated. - f.filter.fieldsLoad(dc.datasource.fields()); - // now we can set the value because the fields are properly initialized - f.filter.setValue(f.validationRules); - - // if there are validation rules present we need to store them in a lookup hash - // so multiple rules can be stored on a single field - if (!Array.isArray(complexValidations[f.columnName])) - complexValidations[f.columnName] = []; - - // now we can push the rules into the hash - // what happens if $$(f.view) isn't present? - if ($$(f.view)) { - complexValidations[f.columnName].push({ - filters: $$(f.view).getFilterHelper(), - // values: $$(ids.form).getValues(), - invalidMessage: f.invalidMessage, - }); - } - }); - - const ids = this.ids; - - // use the lookup to build the validation rules - Object.keys(complexValidations).forEach((key) => { - // get our field that has validation rules - const formField = $$(ids.form).queryView({ - name: key, - }); - - if (!formField) return; - - // store the rules in a data param to be used later - formField.$view.complexValidations = complexValidations[key]; - // define validation rules - formField.define("validate", function (nval, oval, field) { - // get field now that we are validating - const fieldValidating = $$(ids.form)?.queryView({ - name: field, - }); - if (!fieldValidating) return true; - - // default valid is true - let isValid = true; - - // check each rule that was stored previously on the element - fieldValidating.$view.complexValidations.forEach((filter) => { - const object = dc.datasource; - const data = this.getValues(); - - // convert rowData from { colName : data } to { id : data } - const newData = {}; - - (object.fields() || []).forEach((field) => { - newData[field.id] = data[field.columnName]; - }); - - // for the case of "this_object" conditions: - if (data.uuid) newData["this_object"] = data.uuid; - - // use helper funtion to check if valid - const ruleValid = filter.filters(newData); - - // if invalid we need to tell the field - if (!ruleValid) { - isValid = false; - // we also need to define an error message - fieldValidating.define( - "invalidMessage", - filter.invalidMessage - ); - } - }); - - return isValid; - }); - - formField.refresh(); - }); - } - - async onShow(data) { - this.saveButton?.disable(); - - this._showed = true; - - const baseView = this.view; - - // call .onShow in the base component - const superComponent = baseView.superComponent(); - await superComponent.onShow(); - - const $form = $$(this.ids.form); - const dc = this.datacollection; - - if (dc) { - // clear current cursor on load - // if (this.settings.clearOnLoad || _logic.callbacks.clearOnLoad() ) { - const settings = this.settings; - - if (settings.clearOnLoad) { - dc.setCursor(null); - } - - // pull data of current cursor - // await dc.waitReady(); - const rowData = dc.getCursor(); - - if ($form) dc.bind($form); - - // do this for the initial form display so we can see defaults - await this.displayData(rowData); - } - // show blank data in the form - else await this.displayData(data ?? {}); - - //Focus on first focusable component - this.focusOnFirst(); - - if ($form) $form.adjust(); - - // Load data of DCs that are use in record rules here - // no need to wait until they are done. (Let the save button enable) - // It will be re-check again when saving. - baseView.loadDcDataOfRecordRules(); - - this.saveButton?.enable(); - } - - async displayData(rowData) { - // If setTimeout is already scheduled, no need to do anything - if (this.timerId) return; - else this.timerId = await timeout(80); - - const baseView = this.view; - const customFields = baseView.fieldComponents( - (comp) => - comp instanceof ABViewFormCustom || - // rich text - (comp instanceof ABViewFormTextbox && - comp.settings.type === "rich") || - (comp instanceof ABViewFormJson && comp.settings.type === "filter") - ); - - const normalFields = baseView.fieldComponents( - (comp) => - comp instanceof ABViewFormItem && - !(comp instanceof ABViewFormCustom) - ); - - // Set default values - if (!rowData) { - customFields.forEach((f) => { - const field = f.field(); - if (!field) return; - - const comp = baseView.viewComponents[f.id]; - if (!comp) return; - - // var colName = field.columnName; - if (this._showed) comp?.onShow?.(); - - // set value to each components - const defaultRowData = {}; - - field.defaultValue(defaultRowData); - field.setValue($$(comp.ids.formItem), defaultRowData); - - comp?.refresh?.(defaultRowData); - }); - - normalFields.forEach((f) => { - if (f.key === "button") return; - - const field = f.field(); - if (!field) return; - - const comp = baseView.viewComponents[f.id]; - if (!comp) return; - - const colName = field.columnName; - - // set value to each components - const values = {}; - - field.defaultValue(values); - $$(comp.ids.formItem)?.setValue(values[colName] ?? ""); - }); - - // select parent data to default value - const dc = this.datacollection; - const linkDv = dc.datacollectionLink; - - if (linkDv) { - const parentData = linkDv.getCursor(); - - this.displayParentData(parentData); - } - } - - // Populate value to custom fields - else { - customFields.forEach((f) => { - const comp = baseView.viewComponents[f.id]; - if (!comp) return; - - if (this._showed) comp?.onShow?.(); - - // set value to each components - f?.field()?.setValue($$(comp.ids.formItem), rowData); - - comp?.refresh?.(rowData); - }); - - normalFields.forEach((f) => { - if (f.key === "button") return; - - const field = f.field(); - if (!field) return; - - const comp = baseView.viewComponents[f.id]; - if (!comp) return; - // - if (f.key === "datepicker") { - // Not sure why, but the local format isn't applied correctly - // without a timeout here - setTimeout(() => { - field.setValue($$(comp.ids.formItem), rowData); - }, 200); - return; - } - - field.setValue($$(comp.ids.formItem), rowData); - }); - } - - this.timerId = null; - } - - displayParentData(rowData) { - const dc = this.datacollection; - - // If the cursor is selected, then it will not update value of the parent field - const currCursor = dc.getCursor(); - if (currCursor) return; - - const relationField = dc.fieldLink; - if (!relationField) return; - - const baseView = this.view; - // Pull a component of relation field - const relationFieldCom = baseView.fieldComponents((comp) => { - if (!(comp instanceof ABViewFormItem)) return false; - - return comp.field()?.id === relationField.id; - })[0]; - if (!relationFieldCom) return; - - const relationFieldView = baseView.viewComponents[relationFieldCom.id]; - if (!relationFieldView) return; - - const $relationFieldView = $$(relationFieldView.ids.formItem), - relationName = relationField.relationName(); - - // pull data of parent's dc - const formData = {}; - - formData[relationName] = rowData; - - // set data of parent to default value - relationField.setValue($relationFieldView, formData); - } - - detatch() { - // TODO: remove any handlers we have attached. - } - - focusOnFirst() { - const baseView = this.view; - - let topPosition = 0; - let topPositionId = ""; - - baseView.views().forEach((item) => { - if (item.key === "textbox" || item.key === "numberbox") - if (item.position.y === topPosition) { - topPosition = item.position.y; - topPositionId = item.id; - } - }); - - const childComponent = baseView.viewComponents[topPositionId]; - - if (childComponent && $$(childComponent.ids.formItem)) - $$(childComponent.ids.formItem).focus(); - } - - get saveButton() { - return $$(this.ids.form)?.queryView({ - view: "button", - type: "form", - }); - } -}; diff --git a/AppBuilder/platform/views/viewComponent/ABViewFormConnectComponent.js b/AppBuilder/platform/views/viewComponent/ABViewFormConnectComponent.js deleted file mode 100644 index aa17c1f3..00000000 --- a/AppBuilder/platform/views/viewComponent/ABViewFormConnectComponent.js +++ /dev/null @@ -1,674 +0,0 @@ -const ABViewFormItemComponent = require("./ABViewFormItemComponent"); - -module.exports = class ABViewFormConnectComponent extends ( - ABViewFormItemComponent -) { - constructor(baseView, idBase, ids) { - super( - baseView, - idBase || `ABViewFormConnect_${baseView.id}`, - Object.assign( - { - popup: "", - editpopup: "", - }, - ids - ) - ); - - this.addPageComponent = null; - this.editPageComponent = null; - } - - get field() { - return this.view.field(); - } - - get multiselect() { - return this.field?.settings.linkType == "many"; - } - - ui() { - const field = this.field; - const baseView = this.view; - const form = baseView.parentFormComponent(); - const settings = this.settings; - - if (!field) { - console.error(`This field could not found : ${settings.fieldId}`); - - return super.ui({ - view: "label", - label: "", - }); - } - - const multiselect = this.multiselect; // field.settings.linkType == "many"; - const formSettings = form?.settings || {}; - const ids = this.ids; - - let _ui = { - id: ids.formItem, - view: multiselect ? "multicombo" : "combo", - name: field.columnName, - required: - field?.settings?.required || parseInt(settings?.required) || false, - // label: field.label, - // labelWidth: settings.labelWidth, - dataFieldId: field.id, - on: { - onItemClick: (id, e) => { - if ( - e.target.classList.contains("editConnectedPage") && - e.target.dataset.itemId - ) { - const rowId = e.target.dataset.itemId; - if (!rowId) return; - this.goToEditPage(rowId); - } - }, - onChange: (data) => { - this._onChange(data); - }, - }, - }; - - if (formSettings.showLabel) { - _ui.label = field.label; - _ui.labelWidth = formSettings.labelWidth; - _ui.labelPosition = formSettings.labelPosition; - } - - this.initAddEditTool(); - - _ui.suggest = { - button: true, - selectAll: multiselect ? true : false, - body: { - data: [], - template: `${ - baseView?.settings?.editForm - ? '' - : "" - }#value#`, - }, - on: { - onShow: () => { - field.populateOptionsDataCy($$(ids.formItem), field, form); - }, - }, - // Support partial matches - filter: ({ value }, search) => - value.toLowerCase().includes(search.toLowerCase()), - }; - - _ui.onClick = { - customField: (id, e, trg) => { - if (settings.disable === 1) return; - - const rowData = {}; - const $formItem = $$(ids.formItem); - - if ($formItem) { - const node = $formItem.$view; - - field.customEdit(rowData, /* App,*/ node); - } - }, - }; - - let apcUI = this.addPageComponent?.ui; - if (apcUI) { - // reset some component vals to make room for button - _ui.label = ""; - _ui.labelWidth = 0; - - // add click event to add new button - apcUI.on = { - onItemClick: (/*id, evt*/) => { - // let $form = $$(id).getFormView(); - this.addPageComponent?.onClick(form.datacollection); - - return false; - }, - }; - - if (_ui.labelPosition == "top") { - _ui.labelPosition = "left"; - _ui = { - inputId: ids.formItem, - rows: [ - { - view: "label", - label: field.label, - // height: 22, - align: "left", - }, - { - cols: [apcUI, _ui], - }, - ], - }; - } else { - _ui = { - inputId: ids.formItem, - rows: [ - { - cols: [ - { - view: "label", - label: field.label, - width: formSettings.labelWidth, - align: "left", - }, - apcUI, - _ui, - ], - }, - ], - }; - } - - _ui = super.ui(_ui); - } else { - _ui = { - inputId: ids.formItem, - rows: [_ui], - }; - - _ui = super.ui(_ui); - - delete _ui.rows[0].id; - } - - return _ui; - } - - async _onChange(data) { - const ids = this.ids; - const field = this.field; - const baseView = this.view; - - if (this.multiselect) { - if (typeof data == "string") { - data = data.split(","); - } - } - - let selectedValues; - if (Array.isArray(data)) { - selectedValues = []; - data.forEach((record) => { - selectedValues.push(record.id || record); - // let recordObj = record; - // if (typeof record != "object") { - // // we need to convert either index or uuid to full data object - // recordObj = field.getItemFromVal(record); - // } - // if (recordObj?.id) selectedValues.push(recordObj.id); - }); - } else { - selectedValues = data; - if (typeof data != "object") { - // we need to convert either index or uuid to full data object - selectedValues = field.getItemFromVal(data); - } - // selectedValues = field.pullRecordRelationValues(selectedValues); - if (selectedValues?.id) { - selectedValues = selectedValues.id; - } else { - selectedValues = data; - } - } - - // We can now set the new value but we need to block event listening - // so it doesn't trigger onChange again - const $formItem = $$(ids.formItem); - - // Q: if we don't have a $formItem, is any of the rest valid? - if ($formItem) { - // for xxx->one connections we need to populate again before setting - // values because we need to use the selected values to add options - // to the UI - if (this?.field?.settings?.linkViaType == "one") { - this.busy(); - await field.getAndPopulateOptions( - $formItem, - baseView.options, - field, - baseView.parentFormComponent() - ); - this.ready(); - } - - $formItem.blockEvent(); - - // store the user's selected option in local storage. - field.saveSelect(selectedValues); - - const prepedVals = selectedValues.join - ? selectedValues.join() - : selectedValues; - - $formItem.setValue(prepedVals); - $formItem.unblockEvent(); - } - } - - async init(AB, options) { - await super.init(AB); - - const $formItem = $$(this.ids.formItem); - if ($formItem) webix.extend($formItem, webix.ProgressBar); - - this.initAddEditTool(); - } - - initAddEditTool() { - const baseView = this.view; - - // Initial add/edit page tools - const addFormID = baseView?.settings?.formView; - if (addFormID && baseView && !this.addPageComponent) { - this.addPageComponent = baseView.addPageTool.component( - this.AB, - `${baseView.id}_${addFormID}` - ); - this.addPageComponent.applicationLoad(baseView.application); - this.addPageComponent.init({ - onSaveData: this.callbackSaveData.bind(this), - onCancelClick: this.callbackCancel.bind(this), - clearOnLoad: this.callbackClearOnLoad.bind(this), - }); - } - - const editFormID = baseView?.settings?.editForm; - if (editFormID && baseView && !this.editPageComponent) { - this.editPageComponent = baseView.editPageTool.component( - this.AB, - `${baseView.id}_${editFormID}` - ); - this.editPageComponent.applicationLoad(baseView.application); - this.editPageComponent.init({ - onSaveData: this.callbackSaveData.bind(this), - onCancelClick: this.callbackCancel.bind(this), - clearOnLoad: this.callbackClearOnLoad.bind(this), - }); - } - } - - async callbackSaveData(saveData) { - if (saveData == null) return; - else if (!Array.isArray(saveData)) saveData = [saveData]; - - const ids = this.ids; - const field = this.field; - - // find the select component - const $formItem = $$(ids.formItem); - if (!$formItem) return; - - // Refresh option list - this.busy(); - field.clearStorage(this.view.settings.filterConditions); - const data = await field.getAndPopulateOptions( - $formItem, - this.view.options, - field, - this.view.parentFormComponent() - ); - this.ready(); - - // field.once("option.data", (data) => { - data.forEach((item) => { - item.value = item.text; - }); - - $formItem.getList().clearAll(); - $formItem.getList().define("data", data); - - if (field.settings.linkType === "many") { - let selectedItems = $formItem.getValue(); - saveData.forEach((sData) => { - if (selectedItems.indexOf(sData.id) === -1) - selectedItems = selectedItems - ? `${selectedItems},${sData.id}` - : sData.id; - }); - - $formItem.setValue(selectedItems); - } else { - $formItem.setValue(saveData[0].id); - } - // close the popup when we are finished - // $$(ids.popup)?.close(); - // $$(ids.editpopup)?.close(); - // }); - - // field.getOptions(this.settings.filterConditions, ""); - // .then(function (data) { - // // we need new option that will be returned from server (above) - // // so we will not set this and then just reset it. - // }); - } - - callbackCancel() { - $$(this.ids?.popup)?.close?.(); - - return false; - } - - callbackClearOnLoad() { - return true; - } - - getValue(rowData) { - return this.field.getValue($$(this.ids.formItem), rowData); - } - - formBusy($form) { - if (!$form) return; - - $form.disable?.(); - $form.showProgress?.({ type: "icon" }); - } - - formReady($form) { - if (!$form) return; - - $form.enable?.(); - $form.hideProgress?.(); - } - - goToEditPage(rowId) { - const settings = this.settings; - - if (!settings.editForm) return; - - const editForm = this.view.application.urlResolve(settings.editForm); - - if (!editForm) return; - - const $form = $$(this.ids.formItem).getFormView() || null; - - // Open the form popup - this.editPageComponent.onClick().then(() => { - const dc = editForm.datacollection; - - if (dc) { - dc.setCursor(rowId); - - this.__editFormDcEvent = - this.__editFormDcEvent || - dc.on("initializedData", () => { - dc.setCursor(rowId); - }); - } - }); - } - - async onShow() { - const ids = this.ids; - const $formItem = $$(ids.formItem); - - if (!$formItem) return; - - const field = this.field; - - if (!field) return; - - const node = $formItem.$view; - - if (!node) return; - - const $node = $$(node); - - if (!$node) return; - - const settings = this.settings; - let filterConditions = { - glue: "and", - rules: [], - }; - - if (settings?.filterConditions?.rules?.length) { - filterConditions = this.AB.cloneDeep( - this.view.settings.filterConditions - ); - } - - // NOTE: compatible with version 1. This code should not be here too long. - if ( - !filterConditions?.rules?.length && - settings?.objectWorkspace?.filterConditions?.rules?.length - ) { - filterConditions = this.AB.cloneDeep( - settings.objectWorkspace.filterConditions - ); - } - - // Add the filter connected value - if ((settings?.filterConnectedValue ?? "").indexOf(":") > -1) { - const values = settings.filterConnectedValue.split(":"), - uiConfigName = values[0], - connectFieldId = values[1]; - - filterConditions.rules.push({ - key: connectFieldId, - rule: "filterByConnectValue", - value: uiConfigName, - }); - } - - const getFilterByConnectValues = (conditions, depth = 0) => { - return [ - ...conditions.rules - .filter((e) => e.rule === "filterByConnectValue") - .map((e) => { - const filterByConnectValue = Object.assign({}, e); - - filterByConnectValue.depth = depth; - - return filterByConnectValue; - }), - ].concat( - ...conditions.rules - .filter((e) => e.glue) - .map((e) => getFilterByConnectValues(e, depth + 1)) - ); - }; - - const baseView = this.view; - const filterByConnectValues = getFilterByConnectValues( - filterConditions - ).map((e) => { - for (const key in baseView.parent.viewComponents) { - if ( - !( - baseView.parent.viewComponents[key] instanceof - this.constructor - ) - ) - continue; - - const $ui = $$( - baseView.parent.viewComponents[key] - .ui() - .rows.find((vc) => vc.inputId)?.inputId - ); - - if ($ui?.config?.name === e.value) { - // we need to use the element id stored in the settings to find out what the - // ui component id is so later we can use it to look up its current value - e.filterValue = $ui; - - break; - } - } - - const ab = this.AB; - const field = ab - .objectByID(settings.objectId) - .fieldByID(settings.fieldId); - const linkedObject = ab.objectByID(field.settings.linkObject); - const linkedField = linkedObject.fieldByID(e.key); - - if (linkedField?.settings?.isCustomFK) { - // finally if this is a custom foreign key we need the stored columnName by - // default uuid is passed for all non CFK - e.filterColumn = ab - .objectByID(linkedField.settings.linkObject) - .fields( - (filter) => - filter.id === linkedField.settings.indexField || - linkedField.settings.indexField2 - )[0].columnName; - } else e.filterColumn = null; - - return e; - }); - - baseView.options = { - formView: settings.formView, - filters: filterConditions, - // NOTE: settings.objectWorkspace.xxx is a depreciated setting. - // We will be phasing this out. - sort: settings.sortFields ?? settings.objectWorkspace?.sortFields, - editable: settings.disable === 1 ? false : true, - editPage: - !settings.editForm || settings.editForm === "none" ? false : true, - filterByConnectValues, - }; - - // if this field's options are filtered off another field's value we need - // to make sure the UX helps the user know what to do. - // fetch the options and set placeholder text for this view - if (baseView.options.editable) { - const parentFields = []; - - filterByConnectValues.forEach((fv) => { - if (fv.filterValue && fv.key) { - const $filterValueConfig = $$(fv.filterValue.config.id); - - let parentField = null; - - if (!$filterValueConfig) { - // this happens in the Interface Builder when only the single form UI is displayed - parentField = { - id: "perentElement", - label: this.label("PARENT ELEMENT"), - }; - } else { - const value = field.getValue($filterValueConfig); - - if (!value) { - // if there isn't a value on the parent select element set this one to readonly and change placeholder text - parentField = { - id: fv.filterValue.config.id, - label: $filterValueConfig.config.label, - }; - } - - $filterValueConfig.attachEvent( - "onChange", - async (e) => { - const parentVal = $filterValueConfig.getValue(); - - if (parentVal) { - $node.define("disabled", false); - $node.define( - "placeholder", - this.label("Select items") - ); - this.busy(); - await field.getAndPopulateOptions( - $node, - baseView.options, - field, - baseView.parentFormComponent() - ); - this.ready(); - } else { - $node.define("disabled", true); - $node.define( - "placeholder", - this.label("Must select item from '{0}' first.", [ - $filterValueConfig.config.label, - ]) - ); - } - - // TODO: Do we need to clear selected value? - // $node.setValue(""); - $node.refresh(); - }, - false - ); - } - - if ( - parentField && - parentFields.findIndex((e) => e.id === parentField.id) < 0 - ) - parentFields.push(parentField); - } - }); - - if (parentFields.length && !$node.getValue()) { - $node.define("disabled", true); - $node.define( - "placeholder", - this.label(`Must select item from '{0}' first.`, [ - parentFields.map((e) => e.label).join(", "), - ]) - ); - } else { - $node.define("disabled", false); - $node.define("placeholder", this.label("Select items")); - } - } else { - $node.define("placeholder", ""); - $node.define("disabled", true); - } - - $node.refresh(); - - // Add data-cy attributes - const dataCy = `${field.key} ${field.columnName} ${field.id} ${baseView.parent.id}`; - node.setAttribute("data-cy", dataCy); - - this.busy(); - try { - await field.getAndPopulateOptions( - // $node, - $formItem, - baseView.options, - field, - baseView.parentFormComponent() - ); - } catch (err) { - this.AB.notify.developer(err, { - context: - "ABViewFormConnectComponent > onShow() error calling field.getAndPopulateOptions", - }); - } - this.ready(); - - // Need to refresh selected values when they are custom index - this._onChange($formItem.getValue()); - } - - busy() { - const $formItem = $$(this.ids.formItem); - - $formItem?.disable(); - $formItem?.showProgress?.({ type: "icon" }); - } - - ready() { - const $formItem = $$(this.ids.formItem); - - $formItem?.enable(); - $formItem?.hideProgress?.(); - } -}; diff --git a/AppBuilder/platform/views/viewComponent/ABViewFormCustomComponent.js b/AppBuilder/platform/views/viewComponent/ABViewFormCustomComponent.js deleted file mode 100644 index cc0916be..00000000 --- a/AppBuilder/platform/views/viewComponent/ABViewFormCustomComponent.js +++ /dev/null @@ -1,188 +0,0 @@ -const ABViewFormItemComponent = require("./ABViewFormItemComponent"); -const ABFieldImage = require("../../dataFields/ABFieldImage"); -const FocusableTemplate = require("../../../../webix_custom_components/focusableTemplate"); - -const DEFAULT_HEIGHT = 80; - -module.exports = class ABViewFormCustomComponent extends ( - ABViewFormItemComponent -) { - constructor(baseView, idBase, ids) { - super(baseView, idBase || `ABViewFormCustom_${baseView.id}`, ids); - } - - get new_width() { - const baseView = this.view; - const form = baseView.parentFormComponent(); - const formSettings = form?.settings ?? {}; - const settings = baseView.settings ?? {}; - - let newWidth = formSettings.labelWidth; - - if (settings.formView) newWidth += 40; - else if (formSettings.showLabel && formSettings.labelPosition === "top") - newWidth = 0; - - return newWidth; - } - - ui() { - const baseView = this.view; - const field = baseView.field(); - const form = baseView.parentFormComponent(); - const formSettings = form?.settings ?? {}; - const settings = field?.settings ?? baseView.settings ?? {}; - - const requiredClass = - field?.settings?.required || this.settings.required - ? "webix_required" - : ""; - - let templateLabel = ""; - - if (formSettings.showLabel) { - if (formSettings.labelPosition === "top") - templateLabel = ``; - else - templateLabel = ``; - } - - let height = 38; - let width = this.new_width; - - if (typeof field == "undefined") { - console.warn( - `BaseView[${baseView.id}] returned an undefined field()`, - baseView.toObj() - ); - } - - if (field instanceof ABFieldImage) { - if (settings.useHeight) { - if (formSettings.labelPosition === "top") { - height = parseInt(settings.imageHeight) || DEFAULT_HEIGHT; - height += 38; - } else { - height = parseInt(settings.imageHeight) || DEFAULT_HEIGHT; - } - } else if (formSettings.labelPosition === "top") { - height = DEFAULT_HEIGHT + 38; - } else { - if (DEFAULT_HEIGHT > 38) { - height = DEFAULT_HEIGHT; - } - } - width = - settings.useWidth && settings.imageWidth ? settings.imageWidth : 0; - } else if (formSettings.showLabel && formSettings.labelPosition === "top") - height = DEFAULT_HEIGHT; - - let template = `
    ${ - formSettings.labelPosition == "top" ? "" : templateLabel - }#template#
    ` - .replace(/#width#/g, formSettings.labelWidth) - .replace(/#label#/g, field?.label ?? "") - .replace( - /#template#/g, - field - ?.columnHeader({ - width: width, - height: height, - editable: true, - }) - .template({}) ?? "" - ); - - if (settings.useWidth == 0) { - template = template.replace( - /"ab-image-data-field" style="float: left; width: 100%/g, - '"ab-image-data-field" style="float: left; width: calc(100% - ' + - formSettings.labelWidth + - "px)" - ); - } - - return super.ui({ - view: "forminput", - labelWidth: 0, - paddingY: 0, - paddingX: 0, - css: "ab-custom-field", - // label: field.label, - // labelPosition: settings.labelPosition, // webix.forminput does not have .labelPosition T T - // labelWidth: settings.labelWidth, - body: { - view: new FocusableTemplate(this.AB._App).key, - css: "customFieldCls", - borderless: true, - template: template, - height: height, - onClick: { - customField: (evt, e, trg) => { - if (settings.disable === 1) return; - - let rowData = {}; - - const formView = - this?.parentFormComponent?.() || - this.view?.parentFormComponent?.(); - - if (formView) { - const dv = formView.datacollection; - if (dv) rowData = dv.getCursor() || {}; - } - - // var node = $$(ids.formItem).$view; - let node = $$(trg).getParentView().$view; - field?.customEdit( - rowData, - this.AB_App, - node, - this.ids.formItem, - evt - ); - }, - }, - }, - }); - } - - onShow() { - const ids = this.ids; - const $formItem = $$(ids.formItem); - - if (!$formItem) return; - - const baseView = this.view; - const field = baseView.field(), - rowData = {}, - node = $formItem.$view; - - // Add data-cy attributes - const dataCy = `${baseView.key} ${field.key} ${field.columnName} ${baseView.id} ${baseView.parent.id}`; - node.setAttribute("data-cy", dataCy); - - const options = { - formId: ids.formItem, - editable: baseView.settings.disable === 1 ? false : true, - }; - - if (field instanceof ABFieldImage) { - options.height = field.settings.useHeight - ? parseInt(field.settings.imageHeight) || DEFAULT_HEIGHT - : DEFAULT_HEIGHT; - options.width = field.settings.useWidth - ? parseInt(field.settings.imageWidth) || 0 - : 0; - } - - field.customDisplay(rowData, this.AB._App, node, options); - } - - getValue(rowData) { - const field = this.view.field(); - const $formItem = $$(this.ids.formItem); - - return field.getValue($formItem, rowData); - } -}; diff --git a/AppBuilder/platform/views/viewComponent/ABViewFormDatepickerComponent.js b/AppBuilder/platform/views/viewComponent/ABViewFormDatepickerComponent.js deleted file mode 100644 index d2f73a4a..00000000 --- a/AppBuilder/platform/views/viewComponent/ABViewFormDatepickerComponent.js +++ /dev/null @@ -1,120 +0,0 @@ -const ABViewFormItemComponent = require("./ABViewFormItemComponent"); - -module.exports = class ABViewFormDatepickerComponent extends ( - ABViewFormItemComponent -) { - constructor(baseView, idBase, ids) { - super(baseView, idBase || `ABViewFormDatepicker_${baseView.id}`, ids); - } - - ui() { - const self = this; - const field = this.view.field(); - - const _ui = { - view: "datepicker", - suggest: { - body: { - view: - this.AB.Account?._config?.languageCode == "th" - ? "thaicalendar" - : "calendar", - type: field.settings?.dateFormat === 1 ? "time" : "", - timepicker: - field.key === "datetime" && field.settings?.timeFormat !== 1 - ? true - : false, - editable: true, - on: { - onAfterDateSelect: function (date) { - this.getParentView().setMasterValue({ - value: date, - }); - }, - onTodaySet: function (date) { - this.getParentView().setMasterValue({ - value: date, - }); - }, - onDateClear: function (date) { - this.getParentView().setMasterValue({ - value: date, - }); - }, - }, - }, - on: { - onShow: function () { - const text = this.getMasterValue(); - const field = self.view.field(); - if (!text || !field) return true; - - const vals = {}; - vals[field.columnName] = text; - const date = self.getValue(vals); - - const $calendar = this.getChildViews()[0]; - $calendar.setValue(date); - }, - }, - }, - }; - - if (!field) return _ui; - - // Ignore date - Only time picker - if (field.settings?.dateFormat === 1) _ui.type = "time"; - - // Date & Time picker - if (field.key === "datetime" && field.settings?.timeFormat !== 1) - _ui.timepicker = true; - - // allows entering characters in datepicker input, false by default - _ui.editable = true; - - // default value - if (_ui.value && !(_ui.value instanceof Date)) - _ui.value = new Date(_ui.value); - - // if we have webix locale set, will use the date format form there. - if (!window.webixLocale) _ui.format = field.getFormat(); - - return super.ui(_ui); - } - - getValue(rowData) { - const field = this.view.field(); - const text = rowData[field.columnName]; - if (!field || !text) return null; - - // Sentry Fix: caught an error where this.AB was not set, but this.view was... - // attempt to catch this situation and post more data: - if (!this.AB) { - if (this.view.AB) { - this.AB = this.view.AB; - } else { - let errNoAB = new Error( - "ABViewFormDatePicerComponent:getValue(): AB was not set." - ); - // sentry logs the console before the error, so dump the offending view here: - console.log("view:", JSON.stringify(this.view.toObj())); - throw errNoAB; - } - } - // NOTE: if we are using the Thai language we force the format to be "%d/%m/%Y" in th-TH.js:13 - // so we have to use that format here - let dateVal = this.AB.Webix.Date.strToDate(field.getFormat())(text); - if (this.AB.Account?._config?.languageCode == "th") { - dateVal = this.AB.Webix.Date.strToDate("%j/%m/%Y")(text); - } - const date = dateVal; - - if ( - this.AB.Account?._config?.languageCode == "th" && - field.settings?.dateFormat !== 1 - ) - date.setFullYear(date.getFullYear() - 543); - - return date; - } -}; diff --git a/AppBuilder/platform/views/viewComponent/ABViewFormItemComponent.js b/AppBuilder/platform/views/viewComponent/ABViewFormItemComponent.js deleted file mode 100644 index 1a8cfbf2..00000000 --- a/AppBuilder/platform/views/viewComponent/ABViewFormItemComponent.js +++ /dev/null @@ -1,101 +0,0 @@ -const ABViewComponent = require("./ABViewComponent").default; - -module.exports = class ABViewFormItemComponent extends ABViewComponent { - constructor(baseView, idBase, ids) { - super( - baseView, - idBase || `ABViewFormItem_${baseView.id}`, - Object.assign({ formItem: "" }, ids) - ); - } - - ui(uiFormItemComponent = {}) { - // setup 'label' of the element - const baseView = this.view; - const form = baseView.parentFormComponent(), - field = baseView.field?.() || null, - label = ""; - const settings = form?.settings || {}; - const _uiFormItem = { - id: this.ids.formItem, - labelPosition: settings.labelPosition, - labelWidth: settings.labelWidth, - label, - }; - - if (field) { - _uiFormItem.name = field.columnName; - - // default value - const data = {}; - - field.defaultValue(data); - - if (data[field.columnName]) _uiFormItem.value = data[field.columnName]; - - if (settings.showLabel) _uiFormItem.label = field.label; - - if (field.settings.required || baseView.settings?.required) - _uiFormItem.required = 1; - - if (baseView.settings?.disable === 1) _uiFormItem.disabled = true; - - // add data-cy to form element for better testing code - _uiFormItem.on = { - onAfterRender() { - if (this.getList) { - const popup = this.getPopup(); - - if (!popup) return; - - this.getList().data.each((option) => { - if (!option) return; - - // our option.ids are based on builder input and can include the ' character - const node = popup.$view.querySelector( - `[webix_l_id='${(option?.id ?? "") - .toString() - .replaceAll("'", "\\'")}']` - ); - - if (!node) return; - - node.setAttribute( - "data-cy", - `${field.key} options ${option.id} ${field.id} ${ - form?.id || "nf" - }` - ); - }); - } - - this.getInputNode?.().setAttribute?.( - "data-cy", - `${field.key} ${field.columnName} ${field.id} ${ - form?.id || "nf" - }` - ); - }, - }; - - // this may be needed if we want to format data at this point - // if (field.format) data = field.format(data); - - _uiFormItem.validate = (val, data, colName) => { - const validator = this.AB.Validation.validator(); - - field.isValidData(data, validator); - - return validator.pass(); - }; - } - - const _ui = super.ui([ - Object.assign({}, _uiFormItem, uiFormItemComponent), - ]); - - delete _ui.type; - - return _ui; - } -}; diff --git a/AppBuilder/platform/views/viewComponent/ABViewFormJsonComponent.js b/AppBuilder/platform/views/viewComponent/ABViewFormJsonComponent.js deleted file mode 100644 index 07f1d19a..00000000 --- a/AppBuilder/platform/views/viewComponent/ABViewFormJsonComponent.js +++ /dev/null @@ -1,166 +0,0 @@ -const ABViewFormItemComponent = require("./ABViewFormItemComponent"); - -module.exports = class ABViewFormJsonComponent extends ABViewFormItemComponent { - constructor(baseView, idBase, ids) { - super(baseView, idBase || `ABViewFormJson_${baseView.id}`, ids); - if (this.settings.type == "filter") { - this.rowFilter = this.AB.filterComplexNew( - `${baseView.id}_filterComplex`, - { - isSaveHidden: true, - height: 300, - borderless: false, - showObjectName: true, - } - ); - } - } - - getFilterField(instance) { - if ( - instance?.settings?.filterField && - instance?.view?.parent?.viewComponents - ) { - let filterField = ""; - for (const [key, value] of Object.entries( - instance.view.parent.viewComponents - )) { - if (value.settings.fieldId == instance.settings.filterField) { - filterField = value; - } - } - - if (filterField?.ids?.formItem) { - return filterField.ids.formItem; - } else { - return ""; - } - } else { - return ""; - } - } - - get getSystemObjects() { - // get list of all objects in the app - let objects = this.AB.objects(); - // reformat objects into simple array for Webix multicombo - // if you do not the data causes a maximum stack error - let objectsArray = []; - objects.forEach((obj) => { - objectsArray.push({ id: obj.id, label: obj.label }); - }); - // return the simple array - return objectsArray; - } - - refreshFilter(values) { - if (values) { - let fieldDefs = []; - values.forEach((obj) => { - let object = this.AB.objectByID(obj); - let fields = object.fields(); - if (fields.length) { - fields.forEach((f) => { - fieldDefs.push(f); - }); - } - }); - this.rowFilter.fieldsLoad(fieldDefs); - if ($$(this.ids.formItem).config.value) - this.rowFilter.setValue($$(this.ids.formItem).config.value); - } else { - this.rowFilter.fieldsLoad([]); - if ($$(this.ids.formItem).config.value) - this.rowFilter.setValue($$(this.ids.formItem).config.value); - } - } - - getValue() { - return this.rowFilter.getValue(); - } - - setValue(formVals) { - $$(this.ids.formItem).config.value = formVals; - } - - ui() { - const _ui = {}; - - switch ( - this.settings.type || - this.view.settings.type || - this.view.constructor.defaultValues().type - ) { - case "string": - _ui.view = "textarea"; - _ui.disabled = true; - _ui.height = 200; - _ui.format = { - parse: function (parsed) { - try { - parsed = JSON.parse(parsed); - } catch (err) { - // already parsed - } - return parsed; - }, - edit: function (stringify) { - try { - stringify = JSON.stringify(stringify); - } catch (err) { - // already a string - } - return stringify; - }, - }; - break; - case "systemObject": - _ui.view = "multicombo"; - _ui.placeholder = this.label("Select one or more system objects"); - _ui.button = false; - _ui.stringResult = false; - _ui.suggest = { - selectAll: true, - body: { - data: this.getSystemObjects, - template: webix.template("#label#"), - }, - }; - break; - case "filter": - _ui.view = "forminput"; - _ui.css = "ab-custom-field"; - _ui.body = this.rowFilter.ui; - break; - } - - return super.ui(_ui); - } - - init() { - // if (this.settings.type == "filter") { - // this.rowFilter.init({ showObjectName: true }); - // } - } - - onShow() { - const _ui = this.ui(); - if (this?.settings?.type == "filter") { - let filterField = this.getFilterField(this); - if (!$$(filterField)) return; - $$(filterField).detachEvent("onChange"); - $$(filterField).attachEvent("onChange", (values) => { - this.refreshFilter(values); - }); - this.rowFilter.init({ showObjectName: true }); - this.rowFilter.on("changed", (val) => { - this.setValue(val); - }); - if ($$(this.ids.formItem).config.value) { - this.rowFilter.setValue($$(this.ids.formItem).config.value); - } else { - this.rowFilter.setValue(""); - } - } - } -}; diff --git a/AppBuilder/platform/views/viewComponent/ABViewFormSelectMultipleComponent.js b/AppBuilder/platform/views/viewComponent/ABViewFormSelectMultipleComponent.js deleted file mode 100644 index fa683419..00000000 --- a/AppBuilder/platform/views/viewComponent/ABViewFormSelectMultipleComponent.js +++ /dev/null @@ -1,92 +0,0 @@ -const ABViewFormItemComponent = require("./ABViewFormItemComponent"); - -module.exports = class ABViewFormSelectMultipleComponentComponent extends ( - ABViewFormItemComponent -) { - constructor(baseView, idBase, ids) { - super(baseView, idBase || `ABViewFormSelectMultiple_${baseView.id}`, ids); - } - - ui() { - const baseView = this.view; - const field = baseView.field(), - settings = this.settings; - const options = []; - - if (field?.key === "user") options.push(...field.getUsers()); - else if (field) - options.push(...(field.settings.options ?? settings.options ?? [])); - - const ids = this.ids; - const _ui = { - id: ids.formItem, - view: settings.type || baseView.constructor.defaultValues().type, - options: options.map((opt) => { - return { - id: opt.id, - value: opt.text, - hex: opt.hex, - }; - }), - }; - - switch (_ui.view) { - case "multicombo": - _ui.tagMode = false; - _ui.css = "hideWebixMulticomboTag"; - _ui.tagTemplate = (values) => { - const selectedOptions = []; - const $formItem = $$(ids.formItem) ?? $$(_ui.id); - - values.forEach((val) => { - selectedOptions.push($formItem.getList().getItem(val)); - }); - - let vals = selectedOptions; - - if (field.getSelectedOptions) - vals = field.getSelectedOptions(field, selectedOptions); - - const items = []; - - vals.forEach((val) => { - let hasCustomColor = ""; - let optionHex = ""; - - if (field.settings.hasColors && val.hex) { - hasCustomColor = "hascustomcolor"; - optionHex = `background: ${val.hex};`; - } - - const text = val.text ? val.text : val.value; - - items.push( - `${text}` - ); - }); - - return items.join(""); - }; - - break; - - case "checkbox": - // radio element could not be empty options - _ui.options.push({ - id: "temp", - value: this.label("Option"), - }); - - break; - } - - return super.ui(_ui); - } - - getValue(rowData) { - const field = this.view.field(), - $formItem = $$(this.ids.formItem); - - return field.getValue($formItem, rowData); - } -}; diff --git a/AppBuilder/platform/views/viewComponent/ABViewFormTextboxComponent.js b/AppBuilder/platform/views/viewComponent/ABViewFormTextboxComponent.js deleted file mode 100644 index d95dde71..00000000 --- a/AppBuilder/platform/views/viewComponent/ABViewFormTextboxComponent.js +++ /dev/null @@ -1,81 +0,0 @@ -const ABViewFormItemComponent = require("./ABViewFormItemComponent"); - -module.exports = class ABViewFormTextboxComponent extends ( - ABViewFormItemComponent -) { - constructor(baseView, idBase, ids) { - super(baseView, idBase || `ABViewFormTextbox_${baseView.id}`, ids); - this.type = - this.settings.type || - this.view.settings.type || - this.view.constructor.defaultValues().type; - } - - ui() { - const _ui = {}; - - switch (this.type) { - case "single": - _ui.view = "text"; - break; - case "multiple": - _ui.view = "textarea"; - _ui.height = 200; - break; - case "rich": - _ui.view = "forminput"; - _ui.height = 200; - _ui.css = "ab-rich-text"; - _ui.body = { - view: "tinymce-editor", - value: "", - cdn: "/js/webix/extras/tinymce", - config: { - plugins: "link", - menubar: "format edit", - toolbar: - "undo redo | styles | bold italic | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | fontsize | link", - }, - }; - break; - } - - return super.ui(_ui); - } - - async onShow() { - if (this.type !== "rich") return; - await this.initTinyMCE(); - const _ui = this.ui(); - const _uiFormItem = _ui.rows[0]; - let $formItem = $$(this.ids.formItem); - - // WORKAROUND : to fix breaks TinyMCE when switch pages/tabs - // https://forum.webix.com/discussion/6772/switching-tabs-breaks-tinymce - if ($formItem) { - // recreate rich editor - $formItem = this.AB.Webix.ui(_uiFormItem, $formItem); - - // Add dataCy to TinyMCE text editor - const baseView = this.view; - - $formItem - .getChildViews()[0] - .getEditor(true) - .then((editor) => { - const dataCy = `${baseView.key} rich ${_uiFormItem.name} ${ - baseView.id ?? "" - } ${baseView.parent?.id ?? ""}`; - - editor.contentAreaContainer.setAttribute("data-cy", dataCy); - }); - } - } - - /** - * Ensure TinyMCE has been loaded and initialized. - */ - async initTinyMCE() { - await this.AB.custom["tinymce-editor"].init(); - } -}; diff --git a/AppBuilder/platform/views/viewComponent/ABViewGridComponent.js b/AppBuilder/platform/views/viewComponent/ABViewGridComponent.js index 2f68d778..fd970090 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewGridComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewGridComponent.js @@ -240,7 +240,7 @@ export default class ABViewGridComponent extends ABViewComponent { }), rowData = this.getItem(data.row); - return selectField.customEdit(rowData, null, cellNode); + return selectField.customEdit(rowData, self.AB._App, cellNode, self); } else if (!settings.detailsPage && !settings.editPage) return false; }, diff --git a/AppBuilder/platform/views/viewProperties/ABViewPropertyAddPage.js b/AppBuilder/platform/views/viewProperties/ABViewPropertyAddPage.js index b966bf7e..68b5c4bd 100644 --- a/AppBuilder/platform/views/viewProperties/ABViewPropertyAddPage.js +++ b/AppBuilder/platform/views/viewProperties/ABViewPropertyAddPage.js @@ -1,6 +1,4 @@ import ABViewProperty from "./ABViewProperty"; -import ABViewFormButton from "../ABViewFormButton"; - let L = (...params) => AB.Multilingual.label(...params); @@ -216,15 +214,12 @@ export default class ABViewPropertyAddPage extends ABViewProperty { // Listen 'saved' event of the form widget const saveViews = pageClone.views( - (v) => - v instanceof ABViewFormButton || - v.key == "pdfImporter", + (v) => v.key == "button" || v.key == "pdfImporter", true ) ?? []; saveViews.forEach((view) => { - const v = - view instanceof ABViewFormButton ? view.parent : view; + const v = view.key == "button" ? view.parent : view; v.on("saved", (savedData) => { _logic?.callbacks?.onSaveData(savedData); // ? is there ever a case where we want to keep an add popup open after saving? diff --git a/package-lock.json b/package-lock.json index 284d6be2..fbb6a1ac 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "ab_platform_web", - "version": "1.22.7+c21401", + "version": "1.23.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ab_platform_web", - "version": "1.22.7+c21401", + "version": "1.23.0", "hasInstallScript": true, "license": "ISC", "dependencies": { diff --git a/package.json b/package.json index 8b22ad0c..fb26643a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ab_platform_web", - "version": "1.23.0", + "version": "1.23.1", "description": "AppBuilder runtime environment for the Web client.", "main": "index.js", "scripts": {