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": {