From fadf6d9a33ec69ac5e1759b184fb941e166f53d4 Mon Sep 17 00:00:00 2001 From: wongpratan Date: Wed, 23 Apr 2025 17:19:24 +0700 Subject: [PATCH 01/19] Update core --- AppBuilder/core | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AppBuilder/core b/AppBuilder/core index f90f7fec..d8ea7260 160000 --- a/AppBuilder/core +++ b/AppBuilder/core @@ -1 +1 @@ -Subproject commit f90f7fec5fcb880494057bda8678dbfdb190fb4d +Subproject commit d8ea7260264e059727c506a1781b3a5f7a919a07 From 0f86729f01941c3908927b77c00398a880922552 Mon Sep 17 00:00:00 2001 From: wongpratan Date: Mon, 28 Apr 2025 15:55:18 +0700 Subject: [PATCH 02/19] Add support for the user field in the CSV importer widget - https://github.com/digi-serve/ns_app/issues/553 --- .../viewComponent/ABViewCSVImporterComponent.js | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/AppBuilder/platform/views/viewComponent/ABViewCSVImporterComponent.js b/AppBuilder/platform/views/viewComponent/ABViewCSVImporterComponent.js index aa7d1e8e..6932fb35 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewCSVImporterComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewCSVImporterComponent.js @@ -1,3 +1,4 @@ +const ABFieldUser = require("../../dataFields/ABFieldUser"); const ABViewComponent = require("./ABViewComponent").default; const CSVImporter = require("../../CSVImporter"); @@ -785,7 +786,9 @@ module.exports = class ABViewCSVImporterComponent extends ABViewComponent { if (f.datasourceLink) { linkFieldOptions = f.datasourceLink - .fields((fld) => !fld.isConnection) + .fields( + (fld) => !fld.isConnection || fld instanceof ABFieldUser + ) .map((fld) => { return { id: fld.id, @@ -1564,11 +1567,16 @@ module.exports = class ABViewCSVImporterComponent extends ABViewComponent { const data = list.data || list; (data || []).forEach((row) => { - // store in hash[field.id] = { 'searchKey' : "uuid" } + if (row[f.searchField.columnName] == null) return; + + // store in hash[field.id] = { 'searchKey' : { "objectPK": uuid, "indexKey": any } } + const storeKeys = {}; + storeKeys[connectObject.PK()] = row[connectObject.PK()]; + storeKeys[linkIdKey] = row[linkIdKey]; hashLookups[connectField.id][ row[f.searchField.columnName] - ] = row[linkIdKey]; + ] = storeKeys; }); } catch (err) { console.error(err); From 35404a8edabd6f0bb36c2fef0eef43037927e9b0 Mon Sep 17 00:00:00 2001 From: wongpratan Date: Tue, 29 Apr 2025 15:16:20 +0700 Subject: [PATCH 03/19] *Fix import one:many data in the CSV Importer widget - https://github.com/digi-serve/ns_app/issues/553\#issuecomment-2837812249 --- .../ABViewCSVImporterComponent.js | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/AppBuilder/platform/views/viewComponent/ABViewCSVImporterComponent.js b/AppBuilder/platform/views/viewComponent/ABViewCSVImporterComponent.js index aa7d1e8e..fb707339 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewCSVImporterComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewCSVImporterComponent.js @@ -1,3 +1,4 @@ +const ABFieldUser = require("../../dataFields/ABFieldUser"); const ABViewComponent = require("./ABViewComponent").default; const CSVImporter = require("../../CSVImporter"); @@ -785,7 +786,9 @@ module.exports = class ABViewCSVImporterComponent extends ABViewComponent { if (f.datasourceLink) { linkFieldOptions = f.datasourceLink - .fields((fld) => !fld.isConnection) + .fields( + (fld) => !fld.isConnection || fld instanceof ABFieldUser + ) .map((fld) => { return { id: fld.id, @@ -1538,6 +1541,7 @@ module.exports = class ABViewCSVImporterComponent extends ABViewComponent { (connectedFields || []).forEach((f) => { const connectField = f.field; + const linkType = `${connectField.settings.linkType}:${connectField.settings.linkViaType}`; // const searchWord = newRowData[f.columnIndex]; const connectObject = connectField.datasourceLink; @@ -1564,11 +1568,21 @@ module.exports = class ABViewCSVImporterComponent extends ABViewComponent { const data = list.data || list; (data || []).forEach((row) => { - // store in hash[field.id] = { 'searchKey' : "uuid" } + if (row[f.searchField.columnName] == null) return; + + // store in hash[field.id] = { 'searchKey' : { "objectPK": uuid, "indexKey": any } } + let storeKeys; + if (linkType == "many:many" || linkType == "many:one") { + storeKeys = {}; + storeKeys[connectObject.PK()] = row[connectObject.PK()]; + storeKeys[linkIdKey] = row[linkIdKey]; + } else if (linkType == "one:many") { + storeKeys = row[linkIdKey]; + } hashLookups[connectField.id][ row[f.searchField.columnName] - ] = row[linkIdKey]; + ] = storeKeys; }); } catch (err) { console.error(err); From 8a398f55fed8bca9a49492dd92a1144389bbe1a5 Mon Sep 17 00:00:00 2001 From: wongpratan Date: Tue, 13 May 2025 11:48:00 +0700 Subject: [PATCH 04/19] *Update core - https://github.com/digi-serve/ns_app/issues/615 --- AppBuilder/core | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AppBuilder/core b/AppBuilder/core index d8ea7260..236aca93 160000 --- a/AppBuilder/core +++ b/AppBuilder/core @@ -1 +1 @@ -Subproject commit d8ea7260264e059727c506a1781b3a5f7a919a07 +Subproject commit 236aca93034c123dae4f741da750d0695302aa37 From 2d3c0686b97a4ab4fd41b63975ece40a65b43223 Mon Sep 17 00:00:00 2001 From: wongpratan Date: Wed, 21 May 2025 14:37:08 +0700 Subject: [PATCH 05/19] *Update core - https://github.com/digi-serve/ns_app/issues/615 --- AppBuilder/core | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AppBuilder/core b/AppBuilder/core index 236aca93..77c4184a 160000 --- a/AppBuilder/core +++ b/AppBuilder/core @@ -1 +1 @@ -Subproject commit 236aca93034c123dae4f741da750d0695302aa37 +Subproject commit 77c4184aa8bb361dd28130a6d22abb8cd30d5e25 From 5277e698c1fabb8c28a54d43f6d7090ed2b9cbf0 Mon Sep 17 00:00:00 2001 From: wongpratan Date: Fri, 23 May 2025 12:05:57 +0700 Subject: [PATCH 06/19] +Add Keys Option to CSV Importer widget - https://github.com/digi-serve/ns_app/issues/605 --- .../ABViewCSVImporterComponent.js | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/AppBuilder/platform/views/viewComponent/ABViewCSVImporterComponent.js b/AppBuilder/platform/views/viewComponent/ABViewCSVImporterComponent.js index aa7d1e8e..28514873 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewCSVImporterComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewCSVImporterComponent.js @@ -16,6 +16,7 @@ module.exports = class ABViewCSVImporterComponent extends ABViewComponent { uploadFileList: "", separatedBy: "", headerOnFirstLine: "", + keys: "", columnList: "", search: "", @@ -148,6 +149,27 @@ module.exports = class ABViewCSVImporterComponent extends ABViewComponent { }, }, }, + { + cols: [ + { + id: ids.keys, + view: "multiselect", + label: this.label("Keys"), + options: [], + labelWidth: 140, + disabled: true, + }, + { + view: "icon", + icon: "fa fa-info-circle", + align: "left", + disabled: false, + tooltip: this.label( + "Specifying keys will update existing records if matching keys are found; otherwise, new records will be inserted." + ), + }, + ], + }, ], }, { @@ -438,6 +460,7 @@ module.exports = class ABViewCSVImporterComponent extends ABViewComponent { this.AB.Webix.ui([], $$(ids.columnList)); $$(ids.headerOnFirstLine).disable(); + $$(ids.keys).disable(); $$(ids.importButton).disable(); $$(ids.search).setValue(""); @@ -528,9 +551,11 @@ module.exports = class ABViewCSVImporterComponent extends ABViewComponent { // read CSV file const $headerOnFirstLine = $$(ids.headerOnFirstLine); + const $keys = $$(ids.keys); const $importButton = $$(ids.importButton); $headerOnFirstLine.enable(); + $keys.enable(); $importButton.enable(); this._dataRows = await csvImporter.getDataRows( @@ -663,6 +688,7 @@ module.exports = class ABViewCSVImporterComponent extends ABViewComponent { onChange: function () { self.toggleLinkFields(this); self.loadDataToGrid(); + self.refreshKeysOption(); }, }, }; @@ -828,6 +854,7 @@ module.exports = class ABViewCSVImporterComponent extends ABViewComponent { }); abWebix.ui(uiColumns, $columnList); + this.refreshKeysOption(); this.loadDataToGrid(); } @@ -866,6 +893,23 @@ module.exports = class ABViewCSVImporterComponent extends ABViewComponent { return false; } + refreshKeysOption() { + const ids = this.ids; + + // populate Keys option + const matchFields = this.getMatchFields(); + const $keys = $$(ids.keys); + $keys.define({ + options: matchFields.map((f) => { + const fld = f.field; + return { + id: fld.id, + value: fld.label, + }; + }), + }); + } + loadDataToGrid() { const ids = this.ids; const $datatable = $$(ids.datatable); @@ -1306,6 +1350,13 @@ module.exports = class ABViewCSVImporterComponent extends ABViewComponent { position: 0.0001, }); + // Get keys of records + const $keys = $$(ids.keys); + const keys = $keys.getValue().split(","); + const keyColumnNames = keys + .map((key) => currentObject.fieldByID(key)?.columnName) + .filter((colName) => colName); + // get richselect components const matchFields = this.getMatchFields(); @@ -1637,6 +1688,7 @@ module.exports = class ABViewCSVImporterComponent extends ABViewComponent { try { const result = await objModel.batchCreate({ batch: newRowsData, + keyColumnNames, }); const resultErrors = result.errors; From 9f73666786656487f4664b14734ac4414ab0d7c3 Mon Sep 17 00:00:00 2001 From: wongpratan Date: Fri, 6 Jun 2025 16:31:31 +0700 Subject: [PATCH 07/19] *Improve performance to load the large options - https://github.com/digi-serve/ns_app/issues/624 --- .../platform/dataFields/ABFieldConnect.js | 27 +++++++++++++++---- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/AppBuilder/platform/dataFields/ABFieldConnect.js b/AppBuilder/platform/dataFields/ABFieldConnect.js index f18fdd15..68f9a2e9 100644 --- a/AppBuilder/platform/dataFields/ABFieldConnect.js +++ b/AppBuilder/platform/dataFields/ABFieldConnect.js @@ -154,8 +154,16 @@ module.exports = class ABFieldConnect extends ABFieldConnectCore { }, // Support partial matches - filter: ({ value }, search) => - (value ?? "").toLowerCase().includes((search ?? "").toLowerCase()), + filter: function ({ value }, search) { + if (this._largeOptions) { + field.getAndPopulateOptions(this, { search }, field); + return true; + } else { + return (value ?? "") + .toLowerCase() + .includes((search ?? "").toLowerCase()); + } + }, }; if (multiselect) { @@ -244,7 +252,7 @@ module.exports = class ABFieldConnect extends ABFieldConnectCore { * * @return {Promise} */ - async getOptions(whereClause, term, sort, editor, populate = false) { + async getOptions(whereClause, term, sort, editor) { const theEditor = editor; if (theEditor) { @@ -358,6 +366,7 @@ module.exports = class ABFieldConnect extends ABFieldConnectCore { } const storageID = this.getStorageID(where); + const OPTION_ITEM_LIMIT = 100; Promise.resolve() .then(async () => { @@ -383,7 +392,8 @@ module.exports = class ABFieldConnect extends ABFieldConnectCore { return linkedModel.findAll({ where: where, sort: sort, - populate, + populate: false, + limit: OPTION_ITEM_LIMIT, }); }; @@ -440,6 +450,7 @@ module.exports = class ABFieldConnect extends ABFieldConnectCore { where: whereRels, sort: sortRels, populate: false, + limit: OPTION_ITEM_LIMIT, }); }; } @@ -459,6 +470,12 @@ module.exports = class ABFieldConnect extends ABFieldConnectCore { opt.value = opt.text; }); + // If the number of available options exceeds the threshold, set a flag to indicate that the dataset is large. + // so that we can handle this in the editor. + // This is to prevent performance issues with large datasets. + editor._largeOptions = + results[0].total_count > OPTION_ITEM_LIMIT; + // 8/10/2023 - We are not actually using this (see line 338) - If we need to store // user data in local storage we should encrypt it. // cache options if not a xxx->one connection @@ -665,7 +682,7 @@ module.exports = class ABFieldConnect extends ABFieldConnectCore { return new Promise((resolve, reject) => { this.getOptions( combineFilters, - "", + options?.search ?? "", options?.sort ?? "", theEditor ).then(async (data) => { From 8a8d478c289c44d28697bf0469f1648b9b42a769 Mon Sep 17 00:00:00 2001 From: wongpratan Date: Mon, 9 Jun 2025 17:14:43 +0700 Subject: [PATCH 08/19] *Improve performance to load the large options - https://github.com/digi-serve/ns_app/issues/624 --- AppBuilder/platform/RowUpdater.js | 11 ++++++++++ .../platform/dataFields/ABFieldConnect.js | 22 ++++++++++++++----- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/AppBuilder/platform/RowUpdater.js b/AppBuilder/platform/RowUpdater.js index 57ac54e7..d1ffdc58 100644 --- a/AppBuilder/platform/RowUpdater.js +++ b/AppBuilder/platform/RowUpdater.js @@ -409,6 +409,17 @@ class RowUpdater extends ClassUI { case "connectObject": case "user": inputView = inputView.rows[0].rows[0]; + inputView.suggest.filter = function ({ value }, search) { + if (field._largeOptions) { + field.getAndPopulateOptions(this, { search }, field); + return true; + } else { + return (value ?? "") + .toLowerCase() + .includes((search ?? "").toLowerCase()); + } + }; + inputView.suggest.body.data = (await field.getOptions()).map((e) => { return { diff --git a/AppBuilder/platform/dataFields/ABFieldConnect.js b/AppBuilder/platform/dataFields/ABFieldConnect.js index 68f9a2e9..73d9d34b 100644 --- a/AppBuilder/platform/dataFields/ABFieldConnect.js +++ b/AppBuilder/platform/dataFields/ABFieldConnect.js @@ -155,7 +155,7 @@ module.exports = class ABFieldConnect extends ABFieldConnectCore { // Support partial matches filter: function ({ value }, search) { - if (this._largeOptions) { + if (field._largeOptions) { field.getAndPopulateOptions(this, { search }, field); return true; } else { @@ -268,7 +268,7 @@ module.exports = class ABFieldConnect extends ABFieldConnectCore { theEditor._getOptionsResolve = resolve; theEditor._getOptionsThrottle = setTimeout(() => { resolve(true); - }, 100); + }, 350); }); if (!theEditor._timeToPullData) return; } @@ -305,8 +305,6 @@ module.exports = class ABFieldConnect extends ABFieldConnectCore { if (!where.rules) where.rules = []; - term = term || ""; - // check if linked object value is not define, should return a empty array if (!this.settings.linkObject) return []; @@ -368,6 +366,20 @@ module.exports = class ABFieldConnect extends ABFieldConnectCore { const storageID = this.getStorageID(where); const OPTION_ITEM_LIMIT = 100; + // Searching for a term + term = term || ""; + if (term != null && term != "") { + linkedObj.fields().forEach((f) => { + if (f.key != "string" && f.key != "LongText") return; + + where.rules.push({ + key: f.id, + rule: "contains", + value: term, + }); + }); + } + Promise.resolve() .then(async () => { // Mar 23, 2023 disabling local storage of options because users @@ -473,7 +485,7 @@ module.exports = class ABFieldConnect extends ABFieldConnectCore { // If the number of available options exceeds the threshold, set a flag to indicate that the dataset is large. // so that we can handle this in the editor. // This is to prevent performance issues with large datasets. - editor._largeOptions = + this._largeOptions = results[0].total_count > OPTION_ITEM_LIMIT; // 8/10/2023 - We are not actually using this (see line 338) - If we need to store From 0be9f560f8ea853e5d4f6e9c63787187d43f628e Mon Sep 17 00:00:00 2001 From: wongpratan Date: Mon, 9 Jun 2025 17:51:45 +0700 Subject: [PATCH 09/19] *Improve performance to load the large options - https://github.com/digi-serve/ns_app/issues/624 --- AppBuilder/platform/dataFields/ABFieldConnect.js | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/AppBuilder/platform/dataFields/ABFieldConnect.js b/AppBuilder/platform/dataFields/ABFieldConnect.js index 73d9d34b..0a398171 100644 --- a/AppBuilder/platform/dataFields/ABFieldConnect.js +++ b/AppBuilder/platform/dataFields/ABFieldConnect.js @@ -369,15 +369,26 @@ module.exports = class ABFieldConnect extends ABFieldConnectCore { // Searching for a term term = term || ""; if (term != null && term != "") { + const termCond = { + glue: "or", + rules: [], + }; linkedObj.fields().forEach((f) => { - if (f.key != "string" && f.key != "LongText") return; + if ( + f.key != "string" && + f.key != "LongText" && + f.key != "AutoIndex" + ) + return; - where.rules.push({ + termCond.rules.push({ key: f.id, rule: "contains", value: term, }); }); + + where.rules.push(termCond); } Promise.resolve() From a84d8494d8d170e7e1fe80a1525e198f33b70091 Mon Sep 17 00:00:00 2001 From: wongpratan Date: Tue, 10 Jun 2025 10:33:16 +0700 Subject: [PATCH 10/19] *Fix fetching of large options during search - https://github.com/digi-serve/ns_app/issues/624 --- AppBuilder/platform/dataFields/ABFieldConnect.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/AppBuilder/platform/dataFields/ABFieldConnect.js b/AppBuilder/platform/dataFields/ABFieldConnect.js index 0a398171..423bb6cf 100644 --- a/AppBuilder/platform/dataFields/ABFieldConnect.js +++ b/AppBuilder/platform/dataFields/ABFieldConnect.js @@ -149,13 +149,15 @@ module.exports = class ABFieldConnect extends ABFieldConnectCore { config.suggest = { on: { onBeforeShow: function () { + this._search = null; // reset search field.openOptions(this); }, }, // Support partial matches filter: function ({ value }, search) { - if (field._largeOptions) { + if (field._largeOptions && this._search != search) { + this._search = search; field.getAndPopulateOptions(this, { search }, field); return true; } else { @@ -496,8 +498,9 @@ module.exports = class ABFieldConnect extends ABFieldConnectCore { // If the number of available options exceeds the threshold, set a flag to indicate that the dataset is large. // so that we can handle this in the editor. // This is to prevent performance issues with large datasets. - this._largeOptions = - results[0].total_count > OPTION_ITEM_LIMIT; + if (this._largeOptions == null) + this._largeOptions = + results[0].total_count > OPTION_ITEM_LIMIT; // 8/10/2023 - We are not actually using this (see line 338) - If we need to store // user data in local storage we should encrypt it. From e95cc64b3cb50bfb5094ca94f8b51dcb0530a59b Mon Sep 17 00:00:00 2001 From: wongpratan Date: Tue, 10 Jun 2025 19:50:11 +0700 Subject: [PATCH 11/19] *Improve performance to load the large options on the Form widget - https://github.com/digi-serve/ns_app/issues/624 --- .../viewComponent/ABViewFormConnectComponent.js | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/AppBuilder/platform/views/viewComponent/ABViewFormConnectComponent.js b/AppBuilder/platform/views/viewComponent/ABViewFormConnectComponent.js index aa17c1f3..60d06a5d 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewFormConnectComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewFormConnectComponent.js @@ -98,8 +98,17 @@ module.exports = class ABViewFormConnectComponent extends ( }, }, // Support partial matches - filter: ({ value }, search) => - value.toLowerCase().includes(search.toLowerCase()), + filter: function ({ value }, search) { + if (field._largeOptions && this._search != search) { + this._search = search; + field.getAndPopulateOptions(this, { search }, field); + return true; + } else { + return (value ?? "") + .toLowerCase() + .includes((search ?? "").toLowerCase()); + } + }, }; _ui.onClick = { From ed1645490c18ff65d82e32d63da9b410f35a6a9f Mon Sep 17 00:00:00 2001 From: wongpratan Date: Thu, 12 Jun 2025 12:41:22 +0700 Subject: [PATCH 12/19] *Fix selected item of the large options - https://github.com/digi-serve/ns_app/issues/631 --- .../platform/dataFields/ABFieldConnect.js | 21 ++++++++++++++++-- .../viewComponent/ABViewFormComponent.js | 5 +++-- .../ABViewFormConnectComponent.js | 22 ++++++++++--------- 3 files changed, 34 insertions(+), 14 deletions(-) diff --git a/AppBuilder/platform/dataFields/ABFieldConnect.js b/AppBuilder/platform/dataFields/ABFieldConnect.js index 423bb6cf..18d2c116 100644 --- a/AppBuilder/platform/dataFields/ABFieldConnect.js +++ b/AppBuilder/platform/dataFields/ABFieldConnect.js @@ -366,7 +366,7 @@ module.exports = class ABFieldConnect extends ABFieldConnectCore { } const storageID = this.getStorageID(where); - const OPTION_ITEM_LIMIT = 100; + const OPTION_ITEM_LIMIT = 50; // Searching for a term term = term || ""; @@ -434,7 +434,8 @@ module.exports = class ABFieldConnect extends ABFieldConnectCore { // if we are looking at a field in a form we look at linkViaOneValues // if we are looking at a grid we are editing we look at theEditor?.config?.value if ( - this?.settings?.linkViaType == "one" && + (this?.settings?.linkViaType == "one" || + this._largeOptions) && (this?.linkViaOneValues || theEditor?.config?.value) ) { let values = ""; @@ -469,6 +470,22 @@ module.exports = class ABFieldConnect extends ABFieldConnectCore { rule: "equals", value: v, }); + + if (this.indexField) { + whereRels.rules.push({ + key: this.indexField.id, + rule: "equals", + value: v, + }); + } + + if (this.indexField2) { + whereRels.rules.push({ + key: this.indexField2.id, + rule: "equals", + value: v, + }); + } }); selected = function () { return linkedModel.findAll({ diff --git a/AppBuilder/platform/views/viewComponent/ABViewFormComponent.js b/AppBuilder/platform/views/viewComponent/ABViewFormComponent.js index b2de3d48..2ed818b6 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewFormComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewFormComponent.js @@ -167,8 +167,9 @@ module.exports = class ABViewFormComponent extends ABViewComponent { linkViaOneConnection.forEach((f) => { const field = f.field(); if ( - field?.settings?.linkViaType == "one" && - field?.linkViaOneValues + (field?.settings?.linkViaType == "one" && + field?.linkViaOneValues) || + field?._largeOptions ) { delete field.linkViaOneValues; const relationVals = diff --git a/AppBuilder/platform/views/viewComponent/ABViewFormConnectComponent.js b/AppBuilder/platform/views/viewComponent/ABViewFormConnectComponent.js index 60d06a5d..a5b51ac7 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewFormConnectComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewFormConnectComponent.js @@ -236,10 +236,21 @@ module.exports = class ABViewFormConnectComponent extends ( // Q: if we don't have a $formItem, is any of the rest valid? if ($formItem) { + const prepedVals = selectedValues.join + ? selectedValues.join() + : selectedValues; + + $formItem.blockEvent(); + $formItem.setValue(prepedVals); + $formItem.unblockEvent(); + // 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") { + if ( + this?.field?.settings?.linkViaType == "one" || + this?.field?._largeOptions + ) { this.busy(); await field.getAndPopulateOptions( $formItem, @@ -250,17 +261,8 @@ module.exports = class ABViewFormConnectComponent extends ( 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(); } } From 77d6012c127576bea259dbf82272e2da5177144e Mon Sep 17 00:00:00 2001 From: "pong.promrat" Date: Thu, 12 Jun 2025 16:05:34 +0700 Subject: [PATCH 13/19] *Throttle options population - https://github.com/digi-serve/ns_app/issues/631 --- .../ABViewFormConnectComponent.js | 64 +++++++++++-------- 1 file changed, 38 insertions(+), 26 deletions(-) diff --git a/AppBuilder/platform/views/viewComponent/ABViewFormConnectComponent.js b/AppBuilder/platform/views/viewComponent/ABViewFormConnectComponent.js index a5b51ac7..479d642d 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewFormConnectComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewFormConnectComponent.js @@ -29,6 +29,7 @@ module.exports = class ABViewFormConnectComponent extends ( } ui() { + const _this = this; const field = this.field; const baseView = this.view; const form = baseView.parentFormComponent(); @@ -101,7 +102,7 @@ module.exports = class ABViewFormConnectComponent extends ( filter: function ({ value }, search) { if (field._largeOptions && this._search != search) { this._search = search; - field.getAndPopulateOptions(this, { search }, field); + _this._refreshOptions(search); return true; } else { return (value ?? "") @@ -252,12 +253,7 @@ module.exports = class ABViewFormConnectComponent extends ( this?.field?._largeOptions ) { this.busy(); - await field.getAndPopulateOptions( - $formItem, - baseView.options, - field, - baseView.parentFormComponent() - ); + await this._refreshOptions(); this.ready(); } @@ -266,6 +262,38 @@ module.exports = class ABViewFormConnectComponent extends ( } } + _timeout(ms) { + return new Promise(resolve => { + this.__throttleRefreshOption = setTimeout(() => { + delete this.__throttleRefreshOption; + resolve(); + }, ms); + }); + } + + async _refreshOptions(search) { + if (this.__throttleRefreshOption) + clearTimeout(this.__throttleRefreshOption); + + await this._timeout(200); + + const field = this.field; + const idFormItem = this.ids.formItem; + const baseView = this.view; + let options = baseView.options ?? {}; + if (search) { + options = this.AB.cloneDeep(options); + options.search = search; + } + + return await field.getAndPopulateOptions( + $$(idFormItem), + options, + field, + baseView.parentFormComponent() + ); + } + async init(AB, options) { await super.init(AB); @@ -322,12 +350,7 @@ module.exports = class ABViewFormConnectComponent extends ( // Refresh option list this.busy(); field.clearStorage(this.view.settings.filterConditions); - const data = await field.getAndPopulateOptions( - $formItem, - this.view.options, - field, - this.view.parentFormComponent() - ); + const data = await this._refreshOptions(); this.ready(); // field.once("option.data", (data) => { @@ -592,12 +615,7 @@ module.exports = class ABViewFormConnectComponent extends ( this.label("Select items") ); this.busy(); - await field.getAndPopulateOptions( - $node, - baseView.options, - field, - baseView.parentFormComponent() - ); + await this._refreshOptions(); this.ready(); } else { $node.define("disabled", true); @@ -650,13 +668,7 @@ module.exports = class ABViewFormConnectComponent extends ( this.busy(); try { - await field.getAndPopulateOptions( - // $node, - $formItem, - baseView.options, - field, - baseView.parentFormComponent() - ); + await this._refreshOptions(); } catch (err) { this.AB.notify.developer(err, { context: From f751410bb00c5a97f3af1cd0c50a9bf6dae2283a Mon Sep 17 00:00:00 2001 From: "pong.promrat" Date: Tue, 17 Jun 2025 16:53:42 +0700 Subject: [PATCH 14/19] *Update core - https://github.com/digi-serve/ns_app/issues/626 --- AppBuilder/core | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AppBuilder/core b/AppBuilder/core index 77c4184a..9e622454 160000 --- a/AppBuilder/core +++ b/AppBuilder/core @@ -1 +1 @@ -Subproject commit 77c4184aa8bb361dd28130a6d22abb8cd30d5e25 +Subproject commit 9e6224545094f9921a13db32fc05e66338785acd From f5ec9b55e658928108b4af128614138bd18e2142 Mon Sep 17 00:00:00 2001 From: "pong.promrat" Date: Tue, 17 Jun 2025 16:55:40 +0700 Subject: [PATCH 15/19] *Update how to pull DOCKER_USERNAME value --- .github/workflows/docker-build-custom.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker-build-custom.yml b/.github/workflows/docker-build-custom.yml index 255a563d..2d6b58f4 100644 --- a/.github/workflows/docker-build-custom.yml +++ b/.github/workflows/docker-build-custom.yml @@ -54,7 +54,7 @@ jobs: - name: Login to Docker Hub uses: docker/login-action@v3 with: - username: ${{ secrets.DOCKER_USERNAME }} + username: ${{ vars.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - name: Build and push From af48d768481d40a04e5d7c9e6d8c8f244efcdf7b Mon Sep 17 00:00:00 2001 From: "pong.promrat" Date: Wed, 18 Jun 2025 13:51:17 +0700 Subject: [PATCH 16/19] *Fix search the number field - https://github.com/digi-serve/ns_app/issues/634 --- AppBuilder/platform/dataFields/ABFieldConnect.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/AppBuilder/platform/dataFields/ABFieldConnect.js b/AppBuilder/platform/dataFields/ABFieldConnect.js index 18d2c116..b4178030 100644 --- a/AppBuilder/platform/dataFields/ABFieldConnect.js +++ b/AppBuilder/platform/dataFields/ABFieldConnect.js @@ -379,7 +379,8 @@ module.exports = class ABFieldConnect extends ABFieldConnectCore { if ( f.key != "string" && f.key != "LongText" && - f.key != "AutoIndex" + f.key != "AutoIndex" && + f.key != "number" ) return; From d2e69e7ca4d1aa3e9ad00135180107ccdf8d292e Mon Sep 17 00:00:00 2001 From: "pong.promrat" Date: Tue, 15 Jul 2025 17:43:16 +0700 Subject: [PATCH 17/19] *fixing the issue with loading large options so that the selected value is properly included - https://github.com/digi-serve/ns_app/issues/639 https://github.com/digi-serve/ns_app/issues/640 --- .../platform/dataFields/ABFieldConnect.js | 26 +++++++++++-------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/AppBuilder/platform/dataFields/ABFieldConnect.js b/AppBuilder/platform/dataFields/ABFieldConnect.js index b4178030..22428cb8 100644 --- a/AppBuilder/platform/dataFields/ABFieldConnect.js +++ b/AppBuilder/platform/dataFields/ABFieldConnect.js @@ -435,9 +435,10 @@ module.exports = class ABFieldConnect extends ABFieldConnectCore { // if we are looking at a field in a form we look at linkViaOneValues // if we are looking at a grid we are editing we look at theEditor?.config?.value if ( - (this?.settings?.linkViaType == "one" || - this._largeOptions) && - (this?.linkViaOneValues || theEditor?.config?.value) + // this?.settings?.linkViaType == "one" && + this?.linkViaOneValues || + theEditor?.config?.value || + this._largeOptions ) { let values = ""; // determine if we are looking in a grid or at a form field @@ -488,14 +489,17 @@ module.exports = class ABFieldConnect extends ABFieldConnectCore { }); } }); - selected = function () { - return linkedModel.findAll({ - where: whereRels, - sort: sortRels, - populate: false, - limit: OPTION_ITEM_LIMIT, - }); - }; + + if (whereRels.rules.length > 0) { + selected = function () { + return linkedModel.findAll({ + where: whereRels, + sort: sortRels, + populate: false, + limit: OPTION_ITEM_LIMIT, + }); + }; + } } try { const results = await Promise.all([options(), selected()]); From 0b3e70e1422df2f9b156317b5cc1157cca491dd2 Mon Sep 17 00:00:00 2001 From: "pong.promrat" Date: Thu, 18 Sep 2025 16:13:38 +0700 Subject: [PATCH 18/19] *Fix selected connect field option loading - https://github.com/digi-serve/ns_app/issues/642 --- .../platform/dataFields/ABFieldConnect.js | 55 +++++++++---------- 1 file changed, 27 insertions(+), 28 deletions(-) diff --git a/AppBuilder/platform/dataFields/ABFieldConnect.js b/AppBuilder/platform/dataFields/ABFieldConnect.js index 22428cb8..0709b744 100644 --- a/AppBuilder/platform/dataFields/ABFieldConnect.js +++ b/AppBuilder/platform/dataFields/ABFieldConnect.js @@ -440,19 +440,20 @@ module.exports = class ABFieldConnect extends ABFieldConnectCore { theEditor?.config?.value || this._largeOptions ) { - let values = ""; + let values = []; // determine if we are looking in a grid or at a form field if ( (theEditor?.config?.view == "multicombo" || theEditor?.config?.view == "combo") && this?.linkViaOneValues ) { - values = this?.linkViaOneValues; - } else if (theEditor?.config?.value) { + values.push(this?.linkViaOneValues); + } + if (theEditor?.config?.value) { if (Array.isArray(theEditor.config.value)) { - values = theEditor?.config?.value.join(); + values = values.concat(theEditor?.config?.value); } else { - values = theEditor?.config?.value; + values.push(theEditor?.config?.value); } } let whereRels = {}; @@ -461,34 +462,32 @@ module.exports = class ABFieldConnect extends ABFieldConnectCore { whereRels.glue = "or"; whereRels.rules = []; - // make sure values are unique: - let valHash = {}; - values.split(",").forEach((v) => { - valHash[v] = v; - }); - Object.keys(valHash).forEach((v) => { - whereRels.rules.push({ - key: linkedObj.PK(), - rule: "equals", - value: v, - }); - - if (this.indexField) { + values + // make sure values are unique: + .filter((v, pos) => values.indexOf(v) == pos) + .forEach((v) => { whereRels.rules.push({ - key: this.indexField.id, + key: linkedObj.PK(), rule: "equals", value: v, }); - } - if (this.indexField2) { - whereRels.rules.push({ - key: this.indexField2.id, - rule: "equals", - value: v, - }); - } - }); + if (this.indexField) { + whereRels.rules.push({ + key: this.indexField.id, + rule: "equals", + value: v, + }); + } + + if (this.indexField2) { + whereRels.rules.push({ + key: this.indexField2.id, + rule: "equals", + value: v, + }); + } + }); if (whereRels.rules.length > 0) { selected = function () { From af663baf2cc53420ad2cfea579fc20ee72972b40 Mon Sep 17 00:00:00 2001 From: "pong.promrat" Date: Thu, 18 Sep 2025 17:38:04 +0700 Subject: [PATCH 19/19] Merge from #master --- .github/ISSUE_TEMPLATE/Task.md | 12 ++ .github/ISSUE_TEMPLATE/bug_report.md | 28 ++++ .github/ISSUE_TEMPLATE/bug_report_form.yml | 66 +++++++++ .github/ISSUE_TEMPLATE/enhancement.md | 20 +++ .github/ISSUE_TEMPLATE/feature.md | 18 +++ .../ISSUE_TEMPLATE/performance-enhancement.md | 20 +++ .github/workflows/addissuetoproject.yml | 2 +- .github/workflows/dispatch-web-update.yml | 28 ---- .github/workflows/docker-build-custom.yml | 2 +- .github/workflows/e2e-tests.yml | 14 +- .github/workflows/lighthouse.yml | 6 +- .github/workflows/pr-label-check.yml | 2 +- .github/workflows/pr-merge-release.yml | 17 ++- .github/workflows/update-core-version.yml | 32 ++-- AppBuilder/ABFactory.js | 12 ++ AppBuilder/platform/ABDataCollection.js | 20 ++- AppBuilder/platform/ABModel.js | 41 ++++++ AppBuilder/platform/ABProcess.js | 14 +- .../platform/dataFields/ABFieldConnect.js | 14 +- .../tasks/ABProcessGatewayExclusive.js | 3 + .../process/tasks/ABProcessTaskServiceApi.js | 80 ++++++++++ .../tasks/ABProcessTaskServiceCalculate.js | 1 + .../viewComponent/ABViewGridComponent.js | 5 +- README.md | 10 +- package-lock.json | 30 ++-- package.json | 5 +- resources/Network.js | 2 +- resources/NetworkRest.js | 15 +- resources/NetworkRestSocket.js | 24 ++- ui/portal_auth_login_resetRequest.js | 7 +- ui/portal_work.js | 34 ++++- ui/portal_work_inbox.js | 1 + ui/portal_work_inbox_taskWindow.js | 3 +- ui/portal_work_task_user_form.js | 32 ++-- webix_custom_components/formioBuilder.js | 137 ++++++++++-------- 35 files changed, 582 insertions(+), 175 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/Task.md create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/bug_report_form.yml create mode 100644 .github/ISSUE_TEMPLATE/enhancement.md create mode 100644 .github/ISSUE_TEMPLATE/feature.md create mode 100644 .github/ISSUE_TEMPLATE/performance-enhancement.md delete mode 100644 .github/workflows/dispatch-web-update.yml create mode 100644 AppBuilder/platform/process/tasks/ABProcessTaskServiceApi.js diff --git a/.github/ISSUE_TEMPLATE/Task.md b/.github/ISSUE_TEMPLATE/Task.md new file mode 100644 index 00000000..112ba3c7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/Task.md @@ -0,0 +1,12 @@ +--- +name: Task +about: when the work cannot be accurately represented by the other issue types +title: '' +assignees: '' + +--- + +## Task Summary + + + diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..fb21c496 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,28 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: 'bug :beetle:' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** + +**Server** +Staging, Production, etc. + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/bug_report_form.yml b/.github/ISSUE_TEMPLATE/bug_report_form.yml new file mode 100644 index 00000000..a445ba49 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report_form.yml @@ -0,0 +1,66 @@ +name: Bug Report Form +description: File a bug Report +labels: ["bug :lady_beetle:", "bug"] +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to fill out this bug report! + - type: input + id: description + attributes: + label: Short Description + description: Briefly describe what happened. + validations: + required: true + - type: dropdown + id: bug-severity + attributes: + label: Does the bug prevent users from working? + options: + - The bug does not prevent users from working + - The bug is an inconvenience but does not prevent users from working + - The bug prevents users from completing some tasks + - The bug prevents users from completing critical tasks (e.g., saving data) + validations: + required: true + - type: textarea + id: reproduce + attributes: + label: Steps to Reproduce + description: Please tell us what you did, so we can reproduce the bug. + value: | + 1. Go to '...' + 2. Click on '....' + 3. Scroll down to '....' + 4. See error + validations: + required: true + - type: input + id: expected + attributes: + label: Expected Behavior + description: What did you expect to happen? + validations: + required: true + - type: input + id: server + attributes: + label: What Server are you using? + description: Where do you see this bug? + validations: + required: false + - type: textarea + id: screenshots + attributes: + label: Screenshots + placeholder: Paste Here (ctrl + v) + validations: + required: false + - type: textarea + id: other + attributes: + label: Anything else? + description: Add any other information that could help us fix this. + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/enhancement.md b/.github/ISSUE_TEMPLATE/enhancement.md new file mode 100644 index 00000000..aadf909a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/enhancement.md @@ -0,0 +1,20 @@ +--- +name: Enhancement +about: Small changes to make things work better +title: '' +labels: 'enhancement :sparkles:' +assignees: '' + +--- + +**Is this related to a problem?** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Desired Solution** +A clear and concise description of what you want to happen. + +**Alternatives** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/ISSUE_TEMPLATE/feature.md b/.github/ISSUE_TEMPLATE/feature.md new file mode 100644 index 00000000..5e84477e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature.md @@ -0,0 +1,18 @@ +--- +name: Feature +about: A new feature +title: '' +labels: 'feature :star2:' +assignees: '' + +--- + +> “As a [user], I [want to], [so that].” + +## Requirements +1. + +### Tasks +- [ ] Write Documentation + +## Reference diff --git a/.github/ISSUE_TEMPLATE/performance-enhancement.md b/.github/ISSUE_TEMPLATE/performance-enhancement.md new file mode 100644 index 00000000..8d7e67cb --- /dev/null +++ b/.github/ISSUE_TEMPLATE/performance-enhancement.md @@ -0,0 +1,20 @@ +--- +name: Performance Enhancement +about: For improvements to performance +title: '' +labels: 'enhancement/performance :racing_car:' +assignees: '' + +--- + +**Where do you experience the issue?** +App, server. Any specifics to reproduce? + +**Desired Solution** +A clear and concise description of what you want to happen. + +**Alternatives** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/workflows/addissuetoproject.yml b/.github/workflows/addissuetoproject.yml index 3a61e679..834e5d7e 100644 --- a/.github/workflows/addissuetoproject.yml +++ b/.github/workflows/addissuetoproject.yml @@ -17,7 +17,7 @@ jobs: - name: Get project data env: GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }} - ORGANIZATION: digi-serve + ORGANIZATION: CruGlobal PROJECT_NUMBER: 2 run: | gh api graphql -f query=' diff --git a/.github/workflows/dispatch-web-update.yml b/.github/workflows/dispatch-web-update.yml deleted file mode 100644 index 7e5f2e53..00000000 --- a/.github/workflows/dispatch-web-update.yml +++ /dev/null @@ -1,28 +0,0 @@ -name: Dipsatch Web Service Update -on: - workflow_call: - inputs: - type: - type: string - required: true - version: - type: string - required: true - repo: - type: string - required: true - secrets: - TOKEN: - required: true -jobs: - dispatch-web-update: - name: Dipsatch Web Service Update - runs-on: ubuntu-latest - steps: - - name: Repository Dispatch - uses: peter-evans/repository-dispatch@v2 - with: - token: ${{ secrets.TOKEN }} - repository: digi-serve/ab_service_web - event-type: web_new_version - client-payload: '{"type": "${{ inputs.type }}", "version": "${{ inputs.version }}", "repo": "${{ inputs.repo }}"}' diff --git a/.github/workflows/docker-build-custom.yml b/.github/workflows/docker-build-custom.yml index 2d6b58f4..13a54920 100644 --- a/.github/workflows/docker-build-custom.yml +++ b/.github/workflows/docker-build-custom.yml @@ -30,7 +30,7 @@ jobs: uses: actions/checkout@v3 with: path: web - repository: digi-serve/ab_service_web + repository: CruGlobal/ab_service_web token: ${{ secrets.GITHUB_TOKEN }} - name: Install dependencies diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 2b10544f..02971523 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -22,15 +22,15 @@ jobs: - branch: master webpack: update steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: path: ab_platform_web submodules: true ref: ${{ inputs.ref }} - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: path: web - repository: digi-serve/ab_service_web + repository: CruGlobal/ab_service_web token: ${{ secrets.GITHUB_TOKEN }} ref: ${{ matrix.branch }} @@ -44,16 +44,16 @@ jobs: # webpack expects the folder to be called "web" ab-install action expects "ab_service_web" - run: mv web ab_service_web - - uses: digi-serve/ab-install-action@v1 + - uses: CruGlobal/ab-install-action@v1 with: port: 8080 folder: ab - repository: digi-serve/ab_service_web + repository: CruGlobal/ab_service_web - name: Check out kitchen-sink tests - uses: actions/checkout@v2 + uses: actions/checkout@v4 with: - repository: digi-serve/kitchensink_app + repository: CruGlobal/kitchensink_app path: ab/test/e2e/cypress/e2e/kitchensink_app # These next steps are to save our ablogs to file diff --git a/.github/workflows/lighthouse.yml b/.github/workflows/lighthouse.yml index 2879b3e0..5ba4bce6 100644 --- a/.github/workflows/lighthouse.yml +++ b/.github/workflows/lighthouse.yml @@ -18,7 +18,7 @@ jobs: uses: actions/checkout@v3 with: path: web - repository: digi-serve/ab_service_web + repository: CruGlobal/ab_service_web token: ${{ secrets.GITHUB_TOKEN }} ref: master @@ -35,9 +35,9 @@ jobs: run: mv web ab_service_web - name: Install AppBuilder - uses: digi-serve/ab-install-action@v1 + uses: CruGlobal/ab-install-action@v1 with: - repository: digi-serve/ab_service_web + repository: CruGlobal/ab_service_web - name: Wait for AppBuilder # Skipping the wait step. Cypress has a bit of wait time built in. It might be enough. diff --git a/.github/workflows/pr-label-check.yml b/.github/workflows/pr-label-check.yml index 32459248..86752c3b 100644 --- a/.github/workflows/pr-label-check.yml +++ b/.github/workflows/pr-label-check.yml @@ -6,4 +6,4 @@ on: jobs: call-check-pr: name: Check - uses: digi-serve/.github/.github/workflows/check-pr-release-label.yml@master + uses: CruGlobal/.github/.github/workflows/check-pr-release-label.yml@main diff --git a/.github/workflows/pr-merge-release.yml b/.github/workflows/pr-merge-release.yml index 7ca10748..96f48a9d 100644 --- a/.github/workflows/pr-merge-release.yml +++ b/.github/workflows/pr-merge-release.yml @@ -10,7 +10,7 @@ jobs: name: Label # Only run if the PR closed by merging and we have a label if: ${{ github.event.pull_request.merged }} - uses: digi-serve/.github/.github/workflows/get-pr-release-label.yml@master + uses: CruGlobal/.github/.github/workflows/get-pr-release-label.yml@main call-e2e-tests: name: Test needs: [call-get-label] @@ -21,23 +21,24 @@ jobs: name: Version # Only run if tests pass needs: [ call-get-label, call-e2e-tests ] - uses: digi-serve/.github/.github/workflows/bump-version.yml@master + uses: CruGlobal/.github/.github/workflows/bump-version.yml@main with: ref: ${{ github.ref }} type: ${{ needs.call-get-label.outputs.label }} call-create-release: name: Release - uses: digi-serve/.github/.github/workflows/create-release.yml@master + uses: CruGlobal/.github/.github/workflows/create-release.yml@main needs: [call-bump-version] with: tag: v${{ needs.call-bump-version.outputs.new_version }} - dispatch-web-update: - name: Dipsatch Web Service Update + call-dispatch-web-update: needs: [ call-bump-version, call-get-label ] - uses: ./.github/workflows/dispatch-web-update.yml + uses: CruGlobal/.github/.github/workflows/dispatch-update.yml@main with: + dispatch_repos: '["ab_service_web"]' + dispatch_type: "web_new_version" type: ${{ needs.call-get-label.outputs.label }} version: ${{ needs.call-bump-version.outputs.new_version }} - repo: ab_platform_web + app_id: ${{ vars.GS_DEV_APP_ID }} secrets: - TOKEN: ${{ secrets.PAT }} + app_secret: ${{ secrets.GS_DEV_APP_PK }} diff --git a/.github/workflows/update-core-version.yml b/.github/workflows/update-core-version.yml index fb793c22..6e7046d9 100644 --- a/.github/workflows/update-core-version.yml +++ b/.github/workflows/update-core-version.yml @@ -2,15 +2,18 @@ name: "Update Core Version" run-name: Update core to ${{ github.event.client_payload.version }} on: repository_dispatch: - type: [core_new_version] + types: [core_new_version] +permissions: + # Needed for Update Sub Repo Job + contents: write jobs: call-update-sub: name: Update - uses: digi-serve/.github/.github/workflows/update-sub-repo.yml@master + uses: CruGlobal/.github/.github/workflows/update-sub-repo.yml@main secrets: - TOKEN: ${{ secrets.PAT }} + TOKEN: ${{ secrets.GITHUB_TOKEN }} with: - repository: digi-serve/appbuilder_class_core + repository: CruGlobal/appbuilder_class_core short_name: core folder: AppBuilder/core version: ${{ github.event.client_payload.version }} @@ -24,7 +27,7 @@ jobs: call-generate-build-meta: name: Meta - uses: digi-serve/.github/.github/workflows/generate-build-meta.yml@master + uses: CruGlobal/.github/.github/workflows/generate-build-meta.yml@main with: version: ${{ github.event.client_payload.version }} identifier: c @@ -33,7 +36,7 @@ jobs: name: Version if: ${{ github.event.client_payload.type == 'minor' || github.event.client_payload.type == 'patch' }} needs: [ call-update-sub, call-run-cy-test, call-generate-build-meta ] - uses: digi-serve/.github/.github/workflows/bump-version.yml@master + uses: CruGlobal/.github/.github/workflows/bump-version.yml@main with: ref: ${{ needs.call-update-sub.outputs.branch }} # Consider core updates as patch @@ -43,28 +46,29 @@ jobs: call-merge-release: name: Merge needs: [ call-update-sub, call-bump-version ] - uses: digi-serve/.github/.github/workflows/branch-merge-release.yml@master + uses: CruGlobal/.github/.github/workflows/branch-merge-release.yml@main with: branch: ${{ needs.call-update-sub.outputs.branch }} tag: v${{ needs.call-bump-version.outputs.new_version }} body: "- core updated to v${{ github.event.client_payload.version }}" - call-dispatch-runtime-update: - name: Dipsatch AB Runtime Update + call-dispatch-web-update: needs: [ call-merge-release, call-bump-version ] - uses: ./.github/workflows/dispatch-web-update.yml + uses: CruGlobal/.github/.github/workflows/dispatch-update.yml@main with: + dispatch_repos: '["ab_service_web"]' + dispatch_type: "web_new_version" type: patch version: ${{ needs.call-bump-version.outputs.new_version }} - repo: ab_platform_web + app_id: ${{ vars.GS_DEV_APP_ID }} secrets: - TOKEN: ${{ secrets.PAT }} + app_secret: ${{ secrets.GS_DEV_APP_PK }} call-open-pr-fail: name: Tests Failed needs: [ call-update-sub, call-run-cy-test ] if: ${{ failure() && github.event.client_payload.type != 'major' }} - uses: digi-serve/.github/.github/workflows/open-pr.yml@master + uses: CruGlobal/.github/.github/workflows/open-pr.yml@main with: branch: ${{ needs.call-update-sub.outputs.branch }} title: Update core to ${{ github.event.client_payload.version }} (from GitHub Actions Workflow) @@ -75,7 +79,7 @@ jobs: name: Major Change needs: [ call-update-sub ] if: ${{ github.event.client_payload.type == 'major' }} - uses: digi-serve/.github/.github/workflows/open-pr.yml@master + uses: CruGlobal/.github/.github/workflows/open-pr.yml@main with: branch: ${{ needs.call-update-sub.outputs.branch }} title: Update core to ${{ github.event.client_payload.version }} (from GitHub Actions Workflow) diff --git a/AppBuilder/ABFactory.js b/AppBuilder/ABFactory.js index 07bb4e06..56df120f 100644 --- a/AppBuilder/ABFactory.js +++ b/AppBuilder/ABFactory.js @@ -7,6 +7,7 @@ import { v4 as uuidv4 } from "uuid"; import performance from "../utils/performance"; import FilterComplex from "./platform/FilterComplex"; import SortPopup from "./platform/views/ABViewGridPopupSortFields"; +import Papa from "papaparse"; // // Our Common Resources @@ -1058,6 +1059,17 @@ class ABFactory extends ABFactoryCore { urls = urls.filter((u) => u); await Promise.all(urls.map((url) => this.cssLoad(url))); } + + csvToJson(csvData) { + return Papa.parse(csvData, { + header: true, + skipEmptyLines: true, + }); + } + + jsonToCsv(jsonData) { + return Papa.unparse(jsonData); + } } export default ABFactory; diff --git a/AppBuilder/platform/ABDataCollection.js b/AppBuilder/platform/ABDataCollection.js index 447b3109..3268bab5 100644 --- a/AppBuilder/platform/ABDataCollection.js +++ b/AppBuilder/platform/ABDataCollection.js @@ -5,6 +5,12 @@ module.exports = class ABDataCollection extends ABDataCollectionCore { constructor(attributes, AB) { super(attributes, AB); this.setMaxListeners(0); + this.blacklistLoadData = {}; + // { key : ?? } + // keep track of previous loadData() calls that might not + // have fully completed yet. We don't want to get in a + // race condition where we keep trying to load the same frame + // over and over again. } /** @@ -134,6 +140,7 @@ module.exports = class ABDataCollection extends ABDataCollectionCore { } loadData(start, limit = 20) { + console.log(`loadData: ${start}, ${limit}`); return super.loadData(start, limit).catch((err) => { // hideProgressOfComponents() is a platform specific action. this.hideProgressOfComponents(); @@ -429,8 +436,19 @@ module.exports = class ABDataCollection extends ABDataCollectionCore { (start, count) => { if (start < 0) start = 0; + // since the where clause can change if we are following + // another cursor, include the where as part of the key: + let [where] = this.getWhereClause(start, 0); + let key = `${JSON.stringify(where)}-${start}-${count}`; + if (this.blacklistLoadData[key]) { + return false; + } + this.blacklistLoadData[key] = true; // load more data to the data collection - this.loadData(start, count); + this.loadData(start, count).finally(() => { + // remove from blacklist + delete this.blacklistLoadData[key]; + }); return false; // <-- prevent the default "onDataRequest" } diff --git a/AppBuilder/platform/ABModel.js b/AppBuilder/platform/ABModel.js index ca154df2..537f21c6 100644 --- a/AppBuilder/platform/ABModel.js +++ b/AppBuilder/platform/ABModel.js @@ -98,13 +98,54 @@ module.exports = class ABModel extends ABModelCore { return; } + if (this.isCsvPacked(data)) { + let lengthPacked = JSON.stringify(data).length; + data = this.csvUnpack(data); + + // JOHNNY: getting "RangeError: Invalid string length" + // when data.data is too large. So we are just going + // to .stringify() the rows individually and count the + // length of each one. + + let lengthUnpacked = 0; + if (Array.isArray(data.data)) { + for (var d = 0; d < data.data.length; d++) { + lengthUnpacked += JSON.stringify(data.data[d]).length; + } + } else { + lengthUnpacked += JSON.stringify(data.data).length; + } + + Object.keys(data) + .filter((k) => k != "data") + .map((k) => { + lengthUnpacked += `${k}:${data[k]},`.length; + }); + + lengthUnpacked += 5; // for the brackets + + console.log( + `CSV Pack: ${lengthUnpacked} -> ${lengthPacked} (${( + (lengthPacked / lengthUnpacked) * + 100 + ).toFixed(2)}%)` + ); + } + // let jobID = this.AB.jobID(); // console.log(`${jobID} : normalization begin`); // let timeFrom = performance.now(); if (key) { // on "update" & "create" we want to normalizeData() if (key.indexOf("delete") == -1) { + // on anything with a key, we shouldn't have data.data + data = data.data || data; + this.normalizeData(data); + } else { + // triggers to ab.datacollection.delete need to send the .id + // of the item deleted: + data = data.data || context.id; } } else { // on a findAll we normalize data.data diff --git a/AppBuilder/platform/ABProcess.js b/AppBuilder/platform/ABProcess.js index 08b2330f..9f545a85 100644 --- a/AppBuilder/platform/ABProcess.js +++ b/AppBuilder/platform/ABProcess.js @@ -75,7 +75,7 @@ module.exports = class ABProcess extends ABProcessCore { * @return {Promise} * .resolve( {this} ) */ - save() { + save(skipElements = false) { // if this is an update: // if (this.id) { // return ABDefinition.update(this.id, this.toDefinition()); @@ -86,10 +86,12 @@ module.exports = class ABProcess extends ABProcessCore { // make sure all our tasks have save()ed. var allSaves = []; - var allTasks = this.elements(); - allTasks.forEach((t) => { - allSaves.push(t.save()); - }); + if (!skipElements) { + var allTasks = this.elements(); + allTasks.forEach((t) => { + allSaves.push(t.save()); + }); + } return Promise.all(allSaves).then(() => { // now we can save our Process definition return this.toDefinition() @@ -122,7 +124,7 @@ module.exports = class ABProcess extends ABProcessCore { }); if (needSave) { - return this.save(); + return this.save(true); } }); }); diff --git a/AppBuilder/platform/dataFields/ABFieldConnect.js b/AppBuilder/platform/dataFields/ABFieldConnect.js index 0709b744..e5f25563 100644 --- a/AppBuilder/platform/dataFields/ABFieldConnect.js +++ b/AppBuilder/platform/dataFields/ABFieldConnect.js @@ -230,10 +230,7 @@ module.exports = class ABFieldConnect extends ABFieldConnectCore { } formComponentMobile() { - if (this.settings.linkType == "many") { - return super.formComponent("mobile-selectmultiple"); - } - return super.formComponent("mobile-selectsingle"); + return super.formComponent("mobile-connect"); } detailComponent() { @@ -254,7 +251,7 @@ module.exports = class ABFieldConnect extends ABFieldConnectCore { * * @return {Promise} */ - async getOptions(whereClause, term, sort, editor) { + async getOptions(whereClause, term, sort, editor, populate = false) { const theEditor = editor; if (theEditor) { @@ -418,7 +415,7 @@ module.exports = class ABFieldConnect extends ABFieldConnectCore { return linkedModel.findAll({ where: where, sort: sort, - populate: false, + populate, limit: OPTION_ITEM_LIMIT, }); }; @@ -431,13 +428,16 @@ module.exports = class ABFieldConnect extends ABFieldConnectCore { }); }; + const selectedValue = theEditor?.config?.value; + // we also need to get selected values of xxx->one connections // if we are looking at a field in a form we look at linkViaOneValues // if we are looking at a grid we are editing we look at theEditor?.config?.value if ( // this?.settings?.linkViaType == "one" && this?.linkViaOneValues || - theEditor?.config?.value || + ((!Array.isArray(selectedValue) && selectedValue) || + (Array.isArray(selectedValue) && selectedValue.length)) || this._largeOptions ) { let values = []; diff --git a/AppBuilder/platform/process/tasks/ABProcessGatewayExclusive.js b/AppBuilder/platform/process/tasks/ABProcessGatewayExclusive.js index 443fd07a..6740e044 100644 --- a/AppBuilder/platform/process/tasks/ABProcessGatewayExclusive.js +++ b/AppBuilder/platform/process/tasks/ABProcessGatewayExclusive.js @@ -58,6 +58,9 @@ module.exports = class ABProcessGatewayExclusive extends ( // a condition: let numCondWithOne = 0; myOutgoingConnections.forEach((c) => { + this.conditions[c.id] = this.conditions[c.id] ?? {}; + this.conditions[c.id].filterValue = this.conditions[c.id] + .filterValue ?? { glue: "and", rules: [] }; if ((this.conditions[c.id]?.filterValue.rules?.length ?? 0) == 0) { numCondWithOne++; } diff --git a/AppBuilder/platform/process/tasks/ABProcessTaskServiceApi.js b/AppBuilder/platform/process/tasks/ABProcessTaskServiceApi.js new file mode 100644 index 00000000..fbdcf322 --- /dev/null +++ b/AppBuilder/platform/process/tasks/ABProcessTaskServiceApi.js @@ -0,0 +1,80 @@ +const ApiTaskCore = require("../../../core/process/tasks/ABProcessTaskServiceApiCore.js"); + +// let L = (...params) => AB.Multilingual.label(...params); + +module.exports = class ApiTask extends ApiTaskCore { + static defaults() { + return { key: "Api" }; + } + + fromValues(values) { + super.fromValues(values); + // These are raw values on the client, need to be saved so we can update + // the server. There they will be encrypted and stored seperate from our + // definition. + this.secrets = values.secrets; + } + + toObj() { + const obj = super.toObj(); + obj.secrets = this.secrets; + return obj; + } + + //// + //// Process Instance Methods + //// + + warningsEval() { + super.warningsEval(); + + ["url", "method"].forEach( + (prop) => !this[prop] && this.warningMessage(`is missing a ${prop}`) + ); + + // Verify secrets / process data patterns are valid + const dataPattern = /<%= (.+?) %>/g; + const dataToCheck = []; + ["body", "url"].forEach((prop) => { + if (!this[prop]) return; + const matches = (this[prop].match(dataPattern) ?? []).map((m) => ({ + location: prop, + match: m, + })); + dataToCheck.push(...matches); + }); + if (this.headers) { + this.headers.forEach(({ value }) => { + const matches = (value.match(dataPattern) ?? []).map((m) => ({ + location: "header", + match: m, + })); + dataToCheck.push(...matches); + }); + } + if (dataToCheck.length == 0) return; + const processData = this.process + .processDataFields(this) + .filter((i) => i) + .map((i) => i.key); + const secrets = this.storedSecrets ?? []; + this.secrets?.forEach((s) => secrets.push(s.name)); + dataToCheck.forEach(({ location, match }) => { + const [, secret] = /<%= Secret: (.+?) %>/.exec(match) ?? []; + if (secret) { + if (!secrets.includes(secret)) { + this.warningMessage( + `is missing secret '${secret}' in ${location}.` + ); + } + } else { + const [, data] = /<%= (.+?) %>/.exec(match) ?? []; + if (!processData.includes(data)) { + this.warningMessage( + `references unkown data field '${data}' in ${location}` + ); + } + } + }); + } +}; diff --git a/AppBuilder/platform/process/tasks/ABProcessTaskServiceCalculate.js b/AppBuilder/platform/process/tasks/ABProcessTaskServiceCalculate.js index 5b26831c..3faef4b8 100644 --- a/AppBuilder/platform/process/tasks/ABProcessTaskServiceCalculate.js +++ b/AppBuilder/platform/process/tasks/ABProcessTaskServiceCalculate.js @@ -17,6 +17,7 @@ module.exports = class CalculateTask extends CalculateTaskCore { if (this.formulaText) { const hash = {}; (this.process.processDataFields(this) || []).forEach((item) => { + if (!item) return; hash[`{${item.label}}`] = item; }); diff --git a/AppBuilder/platform/views/viewComponent/ABViewGridComponent.js b/AppBuilder/platform/views/viewComponent/ABViewGridComponent.js index 9f21aab9..2f68d778 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewGridComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewGridComponent.js @@ -1495,7 +1495,7 @@ export default class ABViewGridComponent extends ABViewComponent { // } // }); } else validator.updateGrid(editor.row, $DataTable); - } else $DataTable.clearSelection(); + } else $DataTable?.clearSelection(); return false; @@ -1733,7 +1733,8 @@ export default class ABViewGridComponent extends ABViewComponent { columnHeaders = ab.cloneDeep(this.settings.columnConfig); // if that is empty for some reason, rebuild from our CurrentObject - if (columnHeaders.length === 0) columnHeaders = objColumnHeaders; + if (!columnHeaders || columnHeaders.length === 0) + columnHeaders = objColumnHeaders; // sanity check: // columnHeaders can't contain a column that doesn't exist in objColumHeaders: diff --git a/README.md b/README.md index e53c26aa..86a7bc1e 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@ -[![GitHub Workflow Status (with event)](https://img.shields.io/github/actions/workflow/status/digi-serve/ab_platform_web/pr-merge-release.yml?logo=github&label=Build%20%26%20Test)](https://github.com/digi-serve/ab_platform_web/actions/workflows/pr-merge-release.yml) -[![GitHub tag (with filter)](https://img.shields.io/github/v/tag/digi-serve/ab_platform_web?logo=github&label=Latest%20Version) -](https://github.com/digi-serve/ab_platform_web/releases) +[![GitHub Workflow Status (with event)](https://img.shields.io/github/actions/workflow/status/CruGlobal/ab_platform_web/pr-merge-release.yml?logo=github&label=Build%20%26%20Test)](https://github.com/CruGlobal/ab_platform_web/actions/workflows/pr-merge-release.yml) +[![GitHub tag (with filter)](https://img.shields.io/github/v/tag/CruGlobal/ab_platform_web?logo=github&label=Latest%20Version) +](https://github.com/CruGlobal/ab_platform_web/releases) # AppBuilder Platfrom Web The framework for displaying our AppBuilder runtime in a web browser. ## Install -See [ab_cli](https://github.com/digi-serve/ab-cli) +See [ab_cli](https://github.com/CruGlobal/ab-cli) ## Pull Requests Pull Requests should be tagged with a label `major`, `minor` or `patch`. Use `major` for breaking changes, `minor` for new features, or `patch` for bug fixes. To merge without creating a release a `skip-release` tag can be added instead. @@ -29,4 +29,4 @@ Anything between those 2 lines will be used as release notes when creating a ver - Your changes exist locally, since you're `build` or `watch`ing them - Those changes end up in `/web` - Push those changes to a new branch on `AppBuilder Service Web` - - Follow the directions in that repo for building a [custom image](https://github.com/digi-serve/ab_service_web/actions/workflows/docker-build-custom.yml) + - Follow the directions in that repo for building a [custom image](https://github.com/CruGlobal/ab_service_web/actions/workflows/docker-build-custom.yml) diff --git a/package-lock.json b/package-lock.json index 767a0339..c9bf95e2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "ab_platform_web", - "version": "1.13.5+c20700", + "version": "1.15.9+c20715", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "ab_platform_web", - "version": "1.13.5+c20700", + "version": "1.15.9+c20715", "license": "ISC", "dependencies": { "@sentry/browser": "^7.69.0", @@ -19,9 +19,10 @@ "image-size": "^1.1.1", "jszip-utils": "^0.1.0", "nanoid": "^3.3.4", + "papaparse": "^5.5.2", "pdfjs-dist": "^4.2.67", "sails.io.js": "^1.2.1", - "semver": "^7.7.1", + "semver": "^7.7.2", "socket.io-client": "^2.5.0", "tinymce": "^5.10.6", "uuid": "^8.3.2" @@ -7982,6 +7983,12 @@ "node": ">=6" } }, + "node_modules/papaparse": { + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.5.2.tgz", + "integrity": "sha512-PZXg8UuAc4PcVwLosEEDYjPyfWnTEhOrUfdv+3Bx+NuAb+5NhDmXzg5fHWmdCh1mP5p7JAZfFr3IMQfcntNAdA==", + "license": "MIT" + }, "node_modules/param-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", @@ -9397,9 +9404,9 @@ } }, "node_modules/semver": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", - "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -16954,6 +16961,11 @@ "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", "dev": true }, + "papaparse": { + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.5.2.tgz", + "integrity": "sha512-PZXg8UuAc4PcVwLosEEDYjPyfWnTEhOrUfdv+3Bx+NuAb+5NhDmXzg5fHWmdCh1mP5p7JAZfFr3IMQfcntNAdA==" + }, "param-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", @@ -17969,9 +17981,9 @@ } }, "semver": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", - "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==" + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==" }, "serialize-error": { "version": "8.1.0", diff --git a/package.json b/package.json index 58322853..4843e463 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ab_platform_web", - "version": "1.14.0", + "version": "1.15.10+c20716", "description": "AppBuilder runtime environment for the Web client.", "main": "index.js", "scripts": { @@ -55,9 +55,10 @@ "image-size": "^1.1.1", "jszip-utils": "^0.1.0", "nanoid": "^3.3.4", + "papaparse": "^5.5.2", "pdfjs-dist": "^4.2.67", "sails.io.js": "^1.2.1", - "semver": "^7.7.1", + "semver": "^7.7.2", "socket.io-client": "^2.5.0", "tinymce": "^5.10.6", "uuid": "^8.3.2" diff --git a/resources/Network.js b/resources/Network.js index e416b231..6592d126 100644 --- a/resources/Network.js +++ b/resources/Network.js @@ -248,7 +248,7 @@ class Network extends EventEmitter { */ isNetworkConnected() { // if this is a Web Client and using sails.socket.io - if (io && io.socket && io.socket.isConnected) { + if (typeof io != "undefined" && io.socket && io.socket.isConnected) { return io.socket.isConnected(); } diff --git a/resources/NetworkRest.js b/resources/NetworkRest.js index f43f7599..3bb52d6b 100644 --- a/resources/NetworkRest.js +++ b/resources/NetworkRest.js @@ -222,7 +222,12 @@ class NetworkRest extends EventEmitter { if (this.AB.Account.authToken) { params.headers.Authorization = this.AB.Account.authToken; } - params.headers["Content-type"] = "application/json"; + // Fix: don't set content-type if passed in data is a FormData object. + if ( + Object.prototype.toString.call(params.data) !== "[object FormData]" + ) { + params.headers["Content-type"] = "application/json"; + } var tenantID = this.AB.Tenant.id(); if (tenantID) { @@ -329,10 +334,14 @@ class NetworkRest extends EventEmitter { null ); } - return reject(packet.data); + let error = new Error(packet.message ?? packet.data); + error.response = packet; + error.text = packet.message; + error.url = `${params.method} ${params.url}`; + return reject(error); } else { // unknown/unexpected error: - var error = new Error( + let error = new Error( `${err.status} ${err.statusText || err.message}: ${ params.method } ${params.url}` diff --git a/resources/NetworkRestSocket.js b/resources/NetworkRestSocket.js index afd2d852..2dd65e2b 100644 --- a/resources/NetworkRestSocket.js +++ b/resources/NetworkRestSocket.js @@ -158,6 +158,11 @@ class NetworkRestSocket extends NetworkRest { // Pass the io.socket.on(*) events to our AB factory. listSocketEvents.forEach((ev) => { io.socket.on(ev, (data) => { + // data should be in the format: + // { + // objectId: {uuid}, + // data: {object} + // } socketDataLog(this.AB, ev, data); // ensure we only process a network update 1x @@ -173,8 +178,25 @@ class NetworkRestSocket extends NetworkRest { if (values) { let obj = this.AB.objectByID(data.objectId); if (obj) { - let model = obj.model(); if (ev != "ab.datacollection.delete") { + // if data is packed, then unpack it + let model = obj.model(); + if (model.isCsvPacked(values)) { + let lengthPacked = data.__length; + delete data.__length; + values = model.csvUnpack(values); + data.data = values.data; + let lengthUnpacked = JSON.stringify(data).length; + data.__length = lengthUnpacked; + data.__lengthPacked = lengthPacked; + console.log( + `CSV Pack: ${lengthUnpacked} -> ${lengthPacked} (${( + (lengthPacked / lengthUnpacked) * + 100 + ).toFixed(2)}%)` + ); + } + let jobID = this.AB.jobID(); performance.mark(`${ev}:normalization`, { op: "function", diff --git a/ui/portal_auth_login_resetRequest.js b/ui/portal_auth_login_resetRequest.js index 9eeaae56..900d36a4 100644 --- a/ui/portal_auth_login_resetRequest.js +++ b/ui/portal_auth_login_resetRequest.js @@ -91,7 +91,8 @@ class PortalAuthLoginResetRequest extends ClassUI { ), validateEvent: "blur", attributes: { - "data-cy": "portal_reset_request_email", + "data-cy": + "portal_reset_request_email", }, }, { @@ -142,7 +143,9 @@ class PortalAuthLoginResetRequest extends ClassUI { }, on: { onAfterRender() { - ClassUI.CYPRESS_REF(this); + ClassUI.CYPRESS_REF( + this + ); }, }, }, diff --git a/ui/portal_work.js b/ui/portal_work.js index ee304604..74f8a94b 100644 --- a/ui/portal_work.js +++ b/ui/portal_work.js @@ -492,8 +492,36 @@ class PortalWork extends ClassUI { // // Step 1: prepare the AppState so we can determine which options // should be pre selected. - // - const AppState = (await this.AB.Storage.get(this.storageKey)) ?? { + + /** + * @typedef {Object} AppState + * @property {string} lastSelectedApp ABApplication.id of the last App selected, + * @property {Object} lastPages a lookup of all the last selected Pages for each Application {hash} { ABApplication.id : ABPage.id } + */ + + // 1.1 Check for App & Page secified on the route (query params /?app=...&page=...) + // Ref: https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams + const queryParams = new URLSearchParams(window.location.search); + if (queryParams.has("app") && queryParams.has("page")) { + const appParam = queryParams.get("app"); + // Check its a real appID to address: https://github.com/CruGlobal/ab_platform_web/security/code-scanning/630 + const app = this.AB.applicationByID(appParam); + if (!app) { + console.error(`Trying to Navigate to unknown app ${appParam}`); + } else { + this.AppState = { + lastSelectedApp: app.id, + lastPages: {}, + }; + this.AppState.lastPages[app.id] = queryParams.get("page"); + } + } + + // 1.2 Load the last app / page from storage + this.AppState = + this.AppState ?? (await this.AB.Storage.get(this.storageKey)); + // 1.3 Create a new AppState + this.AppState = this.AppState ?? { lastSelectedApp: null, // {string} the ABApplication.id of the last App selected @@ -502,8 +530,6 @@ class PortalWork extends ClassUI { // a lookup of all the last selected Pages for each Application }; - this.AppState = AppState; - // set default selected App if not already set // just choose the 1st App in the list (must have pages that we have // access to) diff --git a/ui/portal_work_inbox.js b/ui/portal_work_inbox.js index 963eeb09..a9d2acb1 100644 --- a/ui/portal_work_inbox.js +++ b/ui/portal_work_inbox.js @@ -34,6 +34,7 @@ class PortalWorkInbox extends ClassUI { return { id: this.id, view: "window", + move: true, head: { view: "toolbar", css: "webix_dark inbox_drawer", diff --git a/ui/portal_work_inbox_taskWindow.js b/ui/portal_work_inbox_taskWindow.js index 63bd2d57..f14712c2 100644 --- a/ui/portal_work_inbox_taskWindow.js +++ b/ui/portal_work_inbox_taskWindow.js @@ -23,9 +23,10 @@ class PortalWorkInboxTaskwindow extends ClassUI { state.height = state.maxHeight * 0.7; }, modal: true, + move: true, head: { view: "toolbar", - css: "webix_dark", + css: "webix_dark team-form-header", cols: [ { width: 17 }, { diff --git a/ui/portal_work_task_user_form.js b/ui/portal_work_task_user_form.js index f012a1df..96a4bc9c 100644 --- a/ui/portal_work_task_user_form.js +++ b/ui/portal_work_task_user_form.js @@ -17,18 +17,21 @@ class PortalWorkTaskUserForm extends ClassUI { width: 600, position: "center", modal: true, + move: true, resize: true, head: { view: "toolbar", css: "webix_dark", cols: [ - {}, { - view: "label", - label: this.label(""), - autowidth: true, + css: { cursor: "move" }, }, - {}, + // { + // view: "label", + // label: this.label(""), + // autowidth: true, + // }, + // {}, { view: "button", width: 35, @@ -53,7 +56,8 @@ class PortalWorkTaskUserForm extends ClassUI { processId, taskId, instanceId, - formComponents = { components: [] } + formComponents = { components: [] }, + formData ) { const ids = this.ids; const _this = this; @@ -65,6 +69,7 @@ class PortalWorkTaskUserForm extends ClassUI { taskId, instanceId, formComponents: formComponents, + formData, onButton: function () { _this.submitData(this.processId, this.taskId, this.instanceId); }, @@ -75,14 +80,16 @@ class PortalWorkTaskUserForm extends ClassUI { processId, taskId, instanceId, - formComponents = { components: [] } + formComponents = { components: [] }, + formData ) { const ids = this.ids; const formIoDef = this.uiFormIO( processId, taskId, instanceId, - formComponents + formComponents, + formData ); this.AB.Webix.ui(formIoDef, $$(ids.formIO)); @@ -99,7 +106,8 @@ class PortalWorkTaskUserForm extends ClassUI { data.processId, data.taskId, data.instanceId, - data.formio + data.formio, + data.formData ); this.show(); }); @@ -107,7 +115,11 @@ class PortalWorkTaskUserForm extends ClassUI { show() { const $popup = $$(this.ids.component); - $popup?.show(); + try { + $popup?.show(); + } catch { + // Catch the error i.render is not function. + } } hide() { diff --git a/webix_custom_components/formioBuilder.js b/webix_custom_components/formioBuilder.js index 353a7197..03a25de8 100644 --- a/webix_custom_components/formioBuilder.js +++ b/webix_custom_components/formioBuilder.js @@ -54,15 +54,19 @@ export default class ABCustomFormBuilderBuilder extends ABLazyCustomComponent { autofit: true, }, $init: async function (config) { - let comp, defaultComponent; - - if (config.dataFields) { - comp = this.parseDataFields(config.dataFields); - defaultComponent = comp.approveButton.schema; - } else { - comp = _this.inputComponents(); + let comp = {}, + defaultComponent; + if (config.dataFields) + Object.assign( + comp, + this.parseDataFields(config.dataFields, { + isCommonForm: config.isCommonForm, + }) + ); + if (config.isCommonForm) { + Object.assign(comp, _this.inputComponents()); defaultComponent = comp.saveButton.schema; - } + } else defaultComponent = comp.approveButton.schema; const formComponents = config.formComponents ? config.formComponents @@ -114,10 +118,24 @@ export default class ABCustomFormBuilderBuilder extends ABLazyCustomComponent { * @param {object[]} fields {field: ABField, key, label, object: ABObject} * @returns {object} each key is a formio component */ - parseDataFields(fields) { + parseDataFields(fields, { isCommonForm } = { isCommonForm: true }) { const components = {}; fields?.forEach(({ field, key, label }) => { - if (!field) return; + if (!field) { + components[key] = { + title: label, + key, + schema: { + label: label.split("->")[1], + disabled: true, + key, + _key: key, + type: "textarea", + input: true, + }, + }; + return; + } const schema = { abFieldID: field.id, @@ -219,57 +237,60 @@ export default class ABCustomFormBuilderBuilder extends ABLazyCustomComponent { }; }); - components["approveButton"] = { - title: this.label("Approve Button"), - key: "approve", - icon: "check-square", - schema: { - label: this.label("Approve"), - type: "button", + if (!isCommonForm) { + components["approveButton"] = { + title: this.label("Approve Button"), key: "approve", - event: "approve", - block: true, - size: "lg", - input: false, - leftIcon: "fa fa-thumbs-up", - action: "event", - theme: "success", - }, - }; - components["denyButton"] = { - title: this.label("Deny Button"), - key: "deny", - icon: "ban", - schema: { - label: this.label("Deny"), - type: "button", + icon: "check-square", + schema: { + label: this.label("Approve"), + type: "button", + key: "approve", + event: "approve", + block: true, + size: "lg", + input: false, + leftIcon: "fa fa-thumbs-up", + action: "event", + theme: "success", + }, + }; + components["denyButton"] = { + title: this.label("Deny Button"), key: "deny", - event: "deny", - block: true, - size: "lg", - input: false, - leftIcon: "fa fa-thumbs-down", - action: "event", - theme: "danger", - }, - }; - components["customButton"] = { - title: this.label("Custom Action Button"), - key: "custom", - icon: "cog", - schema: { - label: this.label("Custom Name"), - type: "button", + icon: "ban", + schema: { + label: this.label("Deny"), + type: "button", + key: "deny", + event: "deny", + block: true, + size: "lg", + input: false, + leftIcon: "fa fa-thumbs-down", + action: "event", + theme: "danger", + }, + }; + components["customButton"] = { + title: this.label("Custom Action Button"), key: "custom", - event: "yourEvent", - block: true, - size: "lg", - input: false, - leftIcon: "fa fa-cog", - action: "event", - theme: "primary", - }, - }; + icon: "cog", + schema: { + label: this.label("Custom Name"), + type: "button", + key: "custom", + event: "yourEvent", + block: true, + size: "lg", + input: false, + leftIcon: "fa fa-cog", + action: "event", + theme: "primary", + }, + }; + } + return components; }