diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index c258ac8..61d87be 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -113,7 +113,9 @@ jobs: run: pm2 start ./logs.js -- --toFile logs/ABServices.log working-directory: ./AppBuilder - name: Wait for AB - uses: ifaxity/wait-on-action@v1 + # Skipping the wait step. Cypress has a bit of wait time built in. + if: false + uses: ifaxity/wait-on-action@v1.1.0 with: resource: http://localhost:80 timeout: 300000 @@ -131,4 +133,4 @@ jobs: with: name: ABServices.log path: ./AppBuilder/logs/ABServices.log - \ No newline at end of file + diff --git a/ABDataCollectionCore.js b/ABDataCollectionCore.js index 38f6d74..f5a617b 100644 --- a/ABDataCollectionCore.js +++ b/ABDataCollectionCore.js @@ -103,6 +103,11 @@ module.exports = class ABDataCollectionCore extends ABMLClass { // {QueryCondition} // A passed in Query Condition for filtering our DataCollection. // This value is ANDed with our normal filter conditions. + + this.__model = null; + // {ABModel} + // An instance of the ABModel used for this DataCollection to + // access data on the server. } /** @@ -146,15 +151,21 @@ module.exports = class ABDataCollectionCore extends ABMLClass { this.settings.linkDatacollectionID = values.settings.linkDatacollectionID || DefaultValues.settings.linkDatacollectionID; - // {string} .settings.linkDaacollectionID + // {string} .settings.linkDatacollectionID // the uuid of another ABDataCollection that provides the link/trigger // for filtering the values of this ABDataCollection. this.settings.linkFieldID = values.settings.linkFieldID || DefaultValues.settings.linkFieldID; // {string} .settings.linkFieldID - // the uuid of the ABDataField of the .linkDatacollection ABObject - // whose value is the trigger value for this ABDataCollection + // this.fieldLink is intended to be the way to connect to the column in + // the datacollectionLink that we are following. However this field + // is the field in this.datasource that connects to the field in + // datacollectionLink that has the value we are linked to. + // So, If this DC(ObjB), and our datacollectionLink (ObjA) + // then this.fieldLink = ObjB->FieldB + // To find the corresponding field in ObjA, we do: + // this.fieldLink.fieldLink (ObjA->FieldA) this.settings.followDatacollectionID = values.settings.followDatacollectionID || @@ -175,13 +186,13 @@ module.exports = class ABDataCollectionCore extends ABMLClass { // Convert to boolean this.settings.loadAll = JSON.parse( - values.settings.loadAll || DefaultValues.settings.loadAll + values.settings.loadAll || DefaultValues.settings.loadAll, ); // {bool} .settings.loadAll // do we load all the data at one time? false == load by pages. this.settings.isQuery = JSON.parse( - values.settings.isQuery || DefaultValues.settings.isQuery + values.settings.isQuery || DefaultValues.settings.isQuery, ); // {bool} .settings.isQuery // is the data source for this ABDataCollection based upon an @@ -206,7 +217,7 @@ module.exports = class ABDataCollectionCore extends ABMLClass { // Convert to number this.settings.syncType = parseInt( - values.settings.syncType || DefaultValues.settings.syncType + values.settings.syncType || DefaultValues.settings.syncType, ); // {int} .settings.syncType // how is the data between this ABDataCollection and it's @@ -247,7 +258,7 @@ module.exports = class ABDataCollectionCore extends ABMLClass { } } else { console.error( - `ABDataCollection[${this.name}][${this.id}] unable to find datasource [${this.settings.datasourceID}]` + `ABDataCollection[${this.name}][${this.id}] unable to find datasource [${this.settings.datasourceID}]`, ); } } @@ -379,7 +390,7 @@ module.exports = class ABDataCollectionCore extends ABMLClass { // occassionally we have blank DCs (without .id or .name) // and I don't want to see errors for those var err = new Error( - `DataCollection[${this.name}][${this.id}] missing reference datasource` + `DataCollection[${this.name}][${this.id}] missing reference datasource`, ); this.AB.notify("builder", err, { datacollection: this.toObj() }); } @@ -446,9 +457,12 @@ module.exports = class ABDataCollectionCore extends ABMLClass { * @return ABModel */ get model() { - let object = this.datasource; // already notified + if (!this.__model) { + let object = this.datasource; // already notified - return object ? object.model() : null; + this.__model = object ? object.model() : null; + } + return this.__model; } get dataStatusFlag() { @@ -538,7 +552,7 @@ module.exports = class ABDataCollectionCore extends ABMLClass { // filter current id for serialize this.__treeCollection.filter( - (item) => item._itemId == currItem._itemId + (item) => item._itemId == currItem._itemId, ); // pull item with child items @@ -586,6 +600,13 @@ module.exports = class ABDataCollectionCore extends ABMLClass { * If the data collection is bound to another and it is the child connection * it finds it's parents current set cursor and then filters its data * based off of the cursor. + * + * In cases where a DC has set .loadAll, our job is to filter existing data + * that is already loaded in the internal __dataCollection. + * + * Otherwise this is not the place to trigger a data refresh. We depend + * on other mechanisms (.reloadData(), datacollection .select()) to trigger + * an update. */ refreshLinkCursor(force = false) { // our filter conditions need to know there was an updated cursor. @@ -689,6 +710,10 @@ module.exports = class ABDataCollectionCore extends ABMLClass { // if (rowId) { this.__dataCollection.setCursor(rowId || null); + if (this.__dataCollection.data.count() == 0) { + this.emit("collectionEmpty", {}); + } + this.setCursorTree(rowId); // } } @@ -707,7 +732,7 @@ module.exports = class ABDataCollectionCore extends ABMLClass { // If no data but the parent DC set cursor, then this should be reload data. const dcFollow = this.datacollectionFollow; - if (!rowId && dcFollow.getCursor()) { + if (!rowId && dcFollow?.getCursor()) { this.loadData(); } } @@ -754,8 +779,11 @@ module.exports = class ABDataCollectionCore extends ABMLClass { // events this.on("ab.datacollection.create", (data) => { - // If this DC is following cursor for other DC, then it should not add the new item to their list. - if (this.isCursorFollow) return; + // NOTE: UPDATing this process to add another check. + // .isCursorFollow only invalidates the 1st half of the routine. + // .isCursorFollow STILL needs to follow the 2nd half of the routine + // // If this DC is following cursor for other DC, then it should not add the new item to their list. + // if (this.isCursorFollow) return; let obj = this.datasource; if (!obj) return; @@ -765,271 +793,468 @@ module.exports = class ABDataCollectionCore extends ABMLClass { let needAdd = false; let updatedVals = []; - Promise.resolve() - .then(() => { - return new Promise((next, bad) => { - // Query - if (obj instanceof this.AB.Class.ABObjectQuery) { - let objList = - obj.objects((o) => o.id == data.objectId) || []; - - needAdd = objList.length > 0; - - if (!needAdd) return next(); - - let where = { - glue: "or", - rules: [], - }; + // Don't do First Step if .isCursorFollow + if (!this.isCursorFollow) { + // First Step + // Does this new entry need to be part of the data we are currently + // tracking? If so, add it. + Promise.resolve() + .then(() => { + return new Promise((next, bad) => { + // Query + if (obj instanceof this.AB.Class.ABObjectQuery) { + let objList = + obj.objects((o) => o.id == data.objectId) || []; + + needAdd = objList.length > 0; + + if (!needAdd) return next(); + + let where = { + glue: "or", + rules: [], + }; + + objList.forEach((o) => { + let newDataId = data.data[`${o.PK()}`]; + if (!newDataId) return; + + where.rules.push({ + key: `${ + o.alias || obj.objectAlias(o.id) + }.${o.PK()}`, + rule: "equals", + value: newDataId, + }); + }); - objList.forEach((o) => { - let newDataId = data.data[`${o.PK()}`]; - if (!newDataId) return; + obj.model() + .findAll({ + where: where, + }) + .then((newQueryData) => { + updatedVals = newQueryData.data || []; + updatedVals.forEach((v) => { + delete v.id; + }); - where.rules.push({ - key: `${o.alias || obj.objectAlias(o.id)}.${o.PK()}`, - rule: "equals", - value: newDataId, - }); + next(); + }) + .catch(bad); + } + // Object + else { + needAdd = obj.id == data.objectId; + updatedVals = [data.data]; + next(); + } + }); + }) + .then(() => { + if (needAdd) { + (updatedVals || []).forEach((updatedV) => { + // If this DC uses a query, it pulls refreshed data from the server in the previous step, + // so there is no need to recheck the query's filter. + const skipDatasourceFilter = + obj instanceof this.AB.Class.ABObjectQuery; + + // filter condition before add + if (!this.isValidData(updatedV, skipDatasourceFilter)) + return; + + // filter the cursor of parent DC + const dcLink = this.datacollectionLink; + if (dcLink && !this.isParentFilterValid(updatedV)) + return; + + // check to see if item already exisits in data collection + // and check to see that we are not loading the data serverside from cursor + if ( + !this.__dataCollection.exists( + updatedV[`${obj.PK()}`], + ) && + !this.__reloadWheres + ) { + this.__dataCollection.add(updatedV, 0); + this.emit("create", updatedV); + // this.__dataCollection.setCursor(rowData.id); + } else if ( + !this.__dataCollection.exists( + updatedV[`${obj.PK()}`], + ) && + this.__reloadWheres + ) { + // debugger; + if (this.isParentFilterValid(updatedV)) { + // we track bound components and flexlayout components + var attachedComponents = + this.__bindComponentIds.concat( + this.__flexComponentIds, + ); + attachedComponents.forEach((bcids) => { + // if the reload button already exisits move on + if ($$(bcids + "_reloadView")) { + return false; + } + + // find the position of the data view + var pos = 0; + var parent = $$(bcids).getParentView(); + if ($$(bcids).getParentView().index) { + pos = $$(bcids) + .getParentView() + .index($$(bcids)); + } else if ( + $$(bcids).getParentView().getParentView() + .index + ) { + // this is a data view and it is inside a + // scroll view that is inside an accodion + // so we need to go deeper to add the button + parent = $$(bcids) + .getParentView() + .getParentView(); + pos = $$(bcids) + .getParentView() + .getParentView() + .index($$(bcids).getParentView()); + } + + // store the datacollection so we can pass it to the button later + var DC = this; + // add a button that reloads the view when clicked + if (parent.addView) { + var L = this.AB.Label(); + parent.addView( + { + id: bcids + "_reloadView", + view: "button", + value: L( + "New data available. Click to reload.", + ), + css: "webix_primary webix_warn", + click: function (id, event) { + DC.reloadData(); + $$(id) + .getParentView() + .removeView(id); + }, + }, + pos, + ); + } + }); + // this.emit("create", updatedV); + } + } }); - obj.model() - .findAll({ - where: where, - }) - .then((newQueryData) => { - updatedVals = newQueryData.data || []; - updatedVals.forEach((v) => { - delete v.id; - }); - - next(); - }) - .catch(bad); - } - // Object - else { - needAdd = obj.id == data.objectId; - updatedVals = [data.data]; - next(); + if ( + this.__treeCollection // && this.__treeCollection.exists(updatedVals.id) + ) { + this.parseTreeCollection({ + data: updatedVals, + }); + } } - }); - }) - .then(() => { - if (needAdd) { - // normalize data before add to data collection - // var model = obj.model(); - // UPDATE: this should already have happened in NetworkRestSocket - // when the initial data is received. - //model.normalizeData(updatedVals); - - (updatedVals || []).forEach((updatedV) => { - // If this DC uses a query, it pulls refreshed data from the server in the previous step, - // so there is no need to recheck the query's filter. - const skipDatasourceFilter = - obj instanceof this.AB.Class.ABObjectQuery; - - // filter condition before add - if (!this.isValidData(updatedV, skipDatasourceFilter)) - return; + // ABObject only + if (!(obj instanceof this.AB.Class.ABObjectQuery)) { + // if it is a linked object + let connectedFields = this.datasource.connectFields( + (f) => + f.datasourceLink && + f.datasourceLink.id == data.objectId, + ); - // filter the cursor of parent DC - const dcLink = this.datacollectionLink; - if (dcLink && !this.isParentFilterValid(updatedV)) return; + // It should always be only one item for ABObject + updatedVals = updatedVals[0]; - // check to see if item already exisits in data collection - // and check to see that we are not loading the data serverside from cursor + // update relation data if ( - !this.__dataCollection.exists( - updatedV[`${obj.PK()}`] - ) && - !this.__reloadWheres - ) { - this.__dataCollection.add(updatedV, 0); - this.emit("create", updatedV); - // this.__dataCollection.setCursor(rowData.id); - } else if ( - !this.__dataCollection.exists( - updatedV[`${obj.PK()}`] - ) && - this.__reloadWheres + updatedVals && + connectedFields && + connectedFields.length > 0 ) { - // debugger; - if (this.isParentFilterValid(updatedV)) { - // we track bound components and flexlayout components - var attachedComponents = - this.__bindComponentIds.concat( - this.__flexComponentIds - ); - attachedComponents.forEach((bcids) => { - // if the reload button already exisits move on - if ($$(bcids + "_reloadView")) { - return false; + // various PK name + let PK = connectedFields[0].object.PK(); + if (!updatedVals.id && PK != "id") + updatedVals.id = updatedVals[PK]; + + this.__dataCollection.find({}).forEach((d) => { + let updateItemData = {}; + + connectedFields.forEach((f) => { + var updateRelateVal = {}; + if (f && f.fieldLink) { + updateRelateVal = + updatedVals[f.fieldLink.relationName()] || + {}; } - // find the position of the data view - var pos = 0; - var parent = $$(bcids).getParentView(); - if ($$(bcids).getParentView().index) { - pos = $$(bcids) - .getParentView() - .index($$(bcids)); + let rowRelateVal = d[f.relationName()] || {}; + + let valIsRelated = isRelated( + updateRelateVal, + d.id, + PK, + ); + + // Relate data + if ( + Array.isArray(rowRelateVal) && + rowRelateVal.filter( + (v) => + v == updatedVals.id || + v.id == updatedVals.id || + v[PK] == updatedVals.id, + ).length < 1 && + valIsRelated + ) { + rowRelateVal.push(updatedVals); + + updateItemData[f.relationName()] = + rowRelateVal; + updateItemData[f.columnName] = updateItemData[ + f.relationName() + ].map((v) => v.id || v[PK] || v); } else if ( - $$(bcids).getParentView().getParentView().index + !Array.isArray(rowRelateVal) && + (rowRelateVal != updatedVals.id || + rowRelateVal.id != updatedVals.id || + rowRelateVal[PK] != updatedVals.id) && + valIsRelated ) { - // this is a data view and it is inside a - // scroll view that is inside an accodion - // so we need to go deeper to add the button - parent = $$(bcids) - .getParentView() - .getParentView(); - pos = $$(bcids) - .getParentView() - .getParentView() - .index($$(bcids).getParentView()); + updateItemData[f.relationName()] = updatedVals; + updateItemData[f.columnName] = + updatedVals.id || updatedVals; } + }); - // store the datacollection so we can pass it to the button later - var DC = this; - // add a button that reloads the view when clicked - if (parent.addView) { - var L = this.AB.Label(); - parent.addView( - { - id: bcids + "_reloadView", - view: "button", - value: L( - "New data available. Click to reload." - ), - css: "webix_primary webix_warn", - click: function (id, event) { - DC.reloadData(); - $$(id).getParentView().removeView(id); - }, - }, - pos + // If this item needs to update + if (Object.keys(updateItemData).length > 0) { + this.__dataCollection.updateItem( + d.id, + updateItemData, + ); + + if (this.__treeCollection) + this.__treeCollection.updateItem( + d.id, + updateItemData, ); - } - }); - // this.emit("create", updatedV); - } - } - }); - if ( - this.__treeCollection // && this.__treeCollection.exists(updatedVals.id) - ) { - this.parseTreeCollection({ - data: updatedVals, - }); + this.emit( + "update", + this.__dataCollection.getItem(d.id), + ); + } + }); + } } - } - // ABObject only - if (!(obj instanceof this.AB.Class.ABObjectQuery)) { - // if it is a linked object - let connectedFields = this.datasource.connectFields( - (f) => - f.datasourceLink && f.datasourceLink.id == data.objectId - ); + this.updateRelationalDataFromLinkDC(data.objectId, data.data); + // filter link data collection's cursor + this.refreshLinkCursor(); + this.setStaticCursor(); + }); + } - // It should always be only one item for ABObject - updatedVals = updatedVals[0]; + // SECOND Step: + // Try to detect if this newly created entry connects to one of the + // values this DC is currently managing. If so, than add this value + // to the connection. + + let incomingObj = this.AB.objectByID(data.objectId); + if (!incomingObj) return; + + // find any of incomingObj connections that are tied to my object: + let connectedFields = incomingObj + .connectFields((f) => f.datasourceLink?.id == obj.id) + .filter((f) => f); // <-- safety check, no undefined or nulls + if (connectedFields.length == 0) return; + + let iPK = incomingObj.PK(); + let PK = obj.PK(); + let newData = this.AB.cloneDeep(data.data); + + let currCursor = this.getCursor(); + let needsRefresh = false; + + connectedFields.forEach((f) => { + // pull the values in this connected fields + let values = data.data[f.columnName]; // just need the PK + + if (!Array.isArray(values)) + values = [values].filter((v) => !this.AB.isNil(v)); + + values.forEach((v) => { + v = v[PK] || v; // just in case this was an {} and not the {uuid} + + // if this is one of the items we are managing + if (this.__dataCollection.exists(v)) { + let localField = f.fieldLink; + if (localField) { + let row = this.__dataCollection.getItem(v); + let colName = localField.columnName; + let relName = localField.relationName(); + + if (localField.linkType() == "many") { + if (!Array.isArray(row[colName])) { + row[colName] = [row[colName]].filter( + (r) => !this.AB.isNil(r), + ); + } + // if it isn't already in the array, add it + let rval = localField.getRelationValue(newData); + if (row[colName].indexOf(rval) == -1) { + row[colName].push(rval); + } - // update relation data - if ( - updatedVals && - connectedFields && - connectedFields.length > 0 - ) { - // various PK name - let PK = connectedFields[0].object.PK(); - if (!updatedVals.id && PK != "id") - updatedVals.id = updatedVals[PK]; - - this.__dataCollection.find({}).forEach((d) => { - let updateItemData = {}; - - connectedFields.forEach((f) => { - var updateRelateVal = {}; - if (f && f.fieldLink) { - updateRelateVal = - updatedVals[f.fieldLink.relationName()] || {}; - } + if (!Array.isArray(row[relName])) { + row[relName] = [row[relName]].filter( + (r) => !this.AB.isNil(r), + ); + } + if ( + row[relName].filter((r) => r[iPK] == newData[iPK]) + .length == 0 + ) { + row[relName].push(newData); + } + } else { + row[colName] = localField.getRelationValue(newData); + row[relName] = newData; + } - let rowRelateVal = d[f.relationName()] || {}; + // pass updated data back into DC: + this.__dataCollection.updateItem(v, row); - let valIsRelated = isRelated( - updateRelateVal, - d.id, - PK - ); + if (this.__treeCollection) + this.__treeCollection.updateItem(v, row); - // Relate data - if ( - Array.isArray(rowRelateVal) && - rowRelateVal.filter( - (v) => - v == updatedVals.id || - v.id == updatedVals.id || - v[PK] == updatedVals.id - ).length < 1 && - valIsRelated - ) { - rowRelateVal.push(updatedVals); - - updateItemData[f.relationName()] = rowRelateVal; - updateItemData[f.columnName] = updateItemData[ - f.relationName() - ].map((v) => v.id || v[PK] || v); - } else if ( - !Array.isArray(rowRelateVal) && - (rowRelateVal != updatedVals.id || - rowRelateVal.id != updatedVals.id || - rowRelateVal[PK] != updatedVals.id) && - valIsRelated - ) { - updateItemData[f.relationName()] = updatedVals; - updateItemData[f.columnName] = - updatedVals.id || updatedVals; - } - }); + this.emit("update", this.__dataCollection.getItem(v)); - // If this item needs to update - if (Object.keys(updateItemData).length > 0) { - // normalize data before add to data collection - var model = obj.model(); + // if we just updated our Current Cursor, we need to + // let our connected DC's know to refresh. + if (currCursor?.id == v) { + needsRefresh = true; + } + } + } + }); + }); - // UPDATE: this should already have happened in NetworkRestSocket - // when the initial data is received. - // model.normalizeData(updateItemData); + if (needsRefresh) { + this.emit("cursorStale", null); + } + }); - this.__dataCollection.updateItem( - d.id, - updateItemData - ); + this.on("ab.datacollection.connection-added", (data) => { + // This event notifies us of a specific relation being created between + // two records. In this case the source object that was originally + // operated on, is sent along in data.data. + // + // eg: if this was a `create` operation, the newly created value is + // included in data.data. + // eg: if this was an `update` operation, the row that was modified + // is included. + // + // {json} data + // incoming socket payload: + // data.objectID {string} + // the ABObject this connection is being added to + // data.fieldID {string} + // which connect Field in this ABObject the value is added to + // data.rowID {string} + // which specific row/entry this is being added to + // data.data {json} + // The value being added. + // - if (this.__treeCollection) - this.__treeCollection.updateItem( - d.id, - updateItemData - ); + let obj = this.datasource; + if (!obj) return; - this.emit( - "update", - this.__dataCollection.getItem(d.id) + if (!data.rowID) return; + + // ABObject only + if (!(obj instanceof this.AB.Class.ABObjectQuery)) { + // if this is the object we are managing + if (obj.id === data.objectID) { + let field = obj.fieldByID(data.fieldID); + if (field) { + // figure out if we actually have the value that was changed: + let isExists = this.__dataCollection.exists(data.rowID); + if (isExists) { + let newData = this.AB.cloneDeep(data.data); + let row = this.__dataCollection.getItem(data.rowID); + + let colName = field.columnName; + let relName = field.relationName(); + + if (field.linkType() == "many") { + if (!Array.isArray(row[colName])) { + // in case row[col]:{obj} this will make sure it + // is included as an [ {obj} ], but will also prevent + // [ undefined ] if row[col] isn't set: + row[colName] = [row[colName]].filter( + (r) => !this.AB.isNil(r), ); } - }); + row[colName].push(f.getRelationValue(newData)); + + if (!Array.isArray(row[relName])) { + row[relName] = [row[relName]].filter( + (r) => !this.AB.isNil(r), + ); + } + row[relName].push(newData); + } else { + row[colName] = f.getRelationValue(newData); + row[relName] = newData; + } + + // pass updated data back into DC: + this.__dataCollection.updateItem(data.rowID, row); + + if (this.__treeCollection) + this.__treeCollection.updateItem(data.rowID, row); + + this.emit( + "update", + this.__dataCollection.getItem(data.rowID), + ); } } - - this.updateRelationalDataFromLinkDC(data.objectId, data.data); - // filter link data collection's cursor - this.refreshLinkCursor(); - this.setStaticCursor(); - }); + } + } else if (obj instanceof this.AB.Class.ABObjectQuery) { + // ABQuery + + // NOTE: that in this case, if this change is about one of the + // objects we track, we will most likely have to reload the data + // to make sure we are displaying proper data. + + // This will follow the same logic as in "ab.datacollection.create" + // so instead of repeating that here, let's call our "create" + // handler instead: + + ///// LEFT OFF HERE: + // need to pull out the data in data.data that represents the + // connection to data.objectId + + //// WAIT!!!!!! + //// Instead, of this "connection-added", lets add the logic in + //// a "created" handler to detect if we should add the new entry + //// into A value we currently track. + //// ===> This way we only have to send out "Created" messages + //// + + let newPacket = { + objectId: data.objectId, + data: this.AB.cloneDeep(data.data), + }; + this.emit("ab.datacollection.create", newPacket); + } }); this.on("ab.datacollection.update", (data) => { @@ -1038,7 +1263,6 @@ module.exports = class ABDataCollectionCore extends ABMLClass { // data.objectId {string} uuid of the ABObject's row that was updated // data.data {json} the new updated value of that row entry. - // debugger; let obj = this.datasource; if (!obj) return; @@ -1046,13 +1270,15 @@ module.exports = class ABDataCollectionCore extends ABMLClass { let values = data.data; if (!values) return; + // #Johnny: removing this check. A DC that is following another cursor + // still has a value that might need updating. // DC who is following cursor should update only current cursor. - if ( - this.isCursorFollow && - this.getCursor()?.id != (values[obj.PK()] ?? values.id) - ) { - return; - } + // if ( + // this.isCursorFollow && + // this.getCursor()?.id != (values[obj.PK()] ?? values.id) + // ) { + // return; + // } let needUpdate = false; let isExists = false; @@ -1064,6 +1290,14 @@ module.exports = class ABDataCollectionCore extends ABMLClass { let updatedTreeIds = []; let updatedVals = {}; + // + // Case 1: This DC contains the value that was updated + // In this case, we want to replace our current entry with + // the new one passed in. + // EX: This is a DC of Users, and the incoming Entry is a User + // that we are already displaying. + // + // Query if (obj instanceof this.AB.Class.ABObjectQuery) { let objList = obj.objects((o) => o.id == data.objectId) || []; @@ -1076,12 +1310,12 @@ module.exports = class ABDataCollectionCore extends ABMLClass { return ( item[ `${this.datasource.objectAlias( - o.id + o.id, )}.${o.PK()}` ] == (values[o.PK()] || values.id) ); }) - .map((o) => o.id) || [] + .map((o) => o.id) || [], ); // grouped queries @@ -1092,12 +1326,12 @@ module.exports = class ABDataCollectionCore extends ABMLClass { return ( item[ `${this.datasource.objectAlias( - o.id + o.id, )}.${o.PK()}` ] == (values[o.PK()] || values.id) ); }) - .map((o) => o.id) || [] + .map((o) => o.id) || [], ); } }); @@ -1125,12 +1359,10 @@ module.exports = class ABDataCollectionCore extends ABMLClass { if (needUpdate) { if (isExists) { if (this.isValidData(updatedVals)) { - // NOTE: this is now done in NetworkRestSocket before - // we start the update events. - // normalize data before update data collection - // var model = obj.model(); - // model.normalizeData(updatedVals); - + // only spread around cloned copies because some objects (I'm + // looking at you ABFieldUser) will modify some data for local + // usage. + updatedVals = this.AB.cloneDeep(updatedVals); if (this.__dataCollection) { updatedIds = this.AB.uniq(updatedIds); updatedIds.forEach((itemId) => { @@ -1150,18 +1382,18 @@ module.exports = class ABDataCollectionCore extends ABMLClass { // If the update item is current cursor, then should tell components to update. let currData = this.getCursor(); - if (currData && currData.id == updatedVals.id) { - this.emit("changeCursor", currData); + if (currData?.id == updatedVals.id) { + this.emit("cursorStale", currData); } } else { // Johnny: Here we are simply removing the DataCollection Entries that are // no longer valid. // Just cycle through the collected updatedIds and remove them. + let currData = this.getCursor(); updatedIds.forEach((id) => { // If the item is current cursor, then the current cursor should be cleared. - let currData = this.getCursor(); - if (currData && currData.id == id) - this.emit("changeCursor", null); + + if (currData?.id == id) this.emit("cursorStale", null); this.__dataCollection.remove(id); @@ -1189,9 +1421,21 @@ module.exports = class ABDataCollectionCore extends ABMLClass { } } + // + // Case 2: This DC has entries that CONNECT to the updated value. + // We need to make sure our connections, properly reflect the + // current state of the incoming data. + // + // EG: This DC is a list of Roles that connect to User, and an updated + // User is passed in. + + let currCursor = this.getCursor(); + let updateCursor = null; + // if it is a linked object + // look for connected fields that link to the incoming objectId let connectedFields = obj.connectFields( - (f) => f.datasourceLink && f.datasourceLink.id == data.objectId + (f) => f.datasourceLink && f.datasourceLink.id == data.objectId, ); // update relation data @@ -1200,7 +1444,9 @@ module.exports = class ABDataCollectionCore extends ABMLClass { connectedFields?.length > 0 ) { // various PK name - let PK = connectedFields[0].object.PK(); + // webix datacollections require an .id value, so make sure + // this incoming value has an .id set + let PK = obj.PK(); if (!values.id && PK != "id") values.id = values[PK]; if (this.__dataCollection.count() > 0) { @@ -1219,26 +1465,32 @@ module.exports = class ABDataCollectionCore extends ABMLClass { updateRelateVal = values[f.fieldLink.relationName()] || {}; + // check to see if we are supposed to be related to this let valIsRelated = isRelated(updateRelateVal, d.id, PK); - // Unrelate data + // If NO, then make sure we Unrelate data + // if this is an array -> filter out the entry if ( Array.isArray(rowRelateVal) && rowRelateVal.filter( (v) => v == values.id || v.id == values.id || - v[PK] == values.id + v[PK] == values.id, ).length > 0 && !valIsRelated ) { updateItemData[f.relationName()] = rowRelateVal.filter( - (v) => (v.id || v[PK] || v) != values.id + // NOTE: Special case: the incoming value.id will be .uuid + // however in case of User Fields, v.id == username and not .uuid + // so we put our default check to be v[PK] here to play nice + (v) => (v[PK] || v.id || v) != values.id, ); updateItemData[f.columnName] = updateItemData[ f.relationName() ].map((v) => v.id || v[PK] || v); } else if ( + // this is not an array so set link to null !Array.isArray(rowRelateVal) && (rowRelateVal == values.id || rowRelateVal.id == values.id || @@ -1249,7 +1501,12 @@ module.exports = class ABDataCollectionCore extends ABMLClass { updateItemData[f.columnName] = null; } - // Relate data or Update + // However, if we are supposed to be related => make sure we are + // If this is an array, then add to list + // AND YES: make sure it is cloned + if (valIsRelated) { + values = this.AB.cloneDeep(values); + } if (Array.isArray(rowRelateVal) && valIsRelated) { // update relate data if ( @@ -1257,9 +1514,11 @@ module.exports = class ABDataCollectionCore extends ABMLClass { (v) => v == values.id || v.id == values.id || - v[PK] == values.id + v[PK] == values.id, ).length > 0 ) { + // just update the one entry in my array with the new + // value rowRelateVal.forEach((v, index) => { if ( v == values.id || @@ -1277,7 +1536,9 @@ module.exports = class ABDataCollectionCore extends ABMLClass { updateItemData[f.relationName()] = rowRelateVal; updateItemData[f.columnName] = updateItemData[ f.relationName() - ].map((v) => v.id || v[PK] || v); + ].map( + (v) => f.getRelationValue(v) /*v.id || v[PK] || v*/, + ); } else if ( !Array.isArray(rowRelateVal) && (rowRelateVal != values.id || @@ -1286,12 +1547,15 @@ module.exports = class ABDataCollectionCore extends ABMLClass { valIsRelated ) { updateItemData[f.relationName()] = values; - updateItemData[f.columnName] = values.id || values; + // make ConnectedField use .getRelationValue() here! + updateItemData[f.columnName] = + f.getRelationValue(values); } }); // If this item needs to update - if (Object.keys(updateItemData).length > 0) { + // meaning there is > 1 key in the object (we always have .id) + if (Object.keys(updateItemData).length > 1) { // normalize data before add to data collection // UPDATE: this should already have happened in NetworkRestSocket // when the initial data is received. @@ -1302,7 +1566,7 @@ module.exports = class ABDataCollectionCore extends ABMLClass { if (this.__treeCollection?.exists(d.id)) { const treeItem = Object.assign( this.__treeCollection.getItem(d.id), - updateItemData + updateItemData, ); this.__treeCollection.updateItem(d.id, treeItem); } @@ -1310,21 +1574,34 @@ module.exports = class ABDataCollectionCore extends ABMLClass { if (this.__dataCollection?.exists(d.id)) { const dcItem = Object.assign( this.__dataCollection.getItem(d.id), - updateItemData + updateItemData, ); this.__dataCollection.updateItem(d.id, dcItem); this.emit( "update", - this.__dataCollection.getItem(d.id) + this.__dataCollection.getItem(d.id), ); + if (currCursor?.id == dcItem.id) { + updateCursor = dcItem; + } } } }); } } - this.updateRelationalDataFromLinkDC(data.objectId, values); - this.refreshLinkCursor(true); + // + // Case 3: Our DC is linked to a DC that was effected by this update. + // + // We will approach it from another direction, if the current DC made + // an update to it's current Cursor, then we will emit a "cursorStale" + // event, so our linked DCs will update themselves with the new value: + if (updateCursor) { + this.emit("cursorStale", null); + } + // this.updateRelationalDataFromLinkDC(data.objectId, values); + this.refreshLinkCursor(); + this.setStaticCursor(); }); @@ -1383,20 +1660,20 @@ module.exports = class ABDataCollectionCore extends ABMLClass { if (this.__dataCollection.exists(values[PK])) { this.__dataCollection.updateItem( values[PK], - res.data[0] + res.data[0], ); } // If the update item is current cursor, then should tell components to update. var currData = this.getCursor(); - if (currData && currData[PK] == values[PK]) { - this.emit("changeCursor", currData); + if (currData?.[PK] == values[PK]) { + this.emit("cursorStale", currData); } } else { // If there is no data in the object then it was deleted...lets clean things up // If the deleted item is current cursor, then the current cursor should be cleared. var currId = this.getCursor(); - if (currId == values[PK]) this.emit("changeCursor", null); + if (currId == values[PK]) this.emit("cursorStale", null); this.__dataCollection.remove(values[PK]); this.emit("delete", values[PK]); @@ -1459,8 +1736,8 @@ module.exports = class ABDataCollectionCore extends ABMLClass { var currData = this.getCursor(); deletedIds.forEach((delId) => { - if (currData && currData[obj.PK()] == delId) - this.emit("changeCursor", null); + if (currData?.[obj.PK()] == delId) + this.emit("cursorStale", null); if (this.__dataCollection.exists(delId)) this.__dataCollection.remove(delId); @@ -1478,7 +1755,7 @@ module.exports = class ABDataCollectionCore extends ABMLClass { // if it is a linked object let connectedFields = obj.connectFields( - (f) => f.datasourceLink && f.datasourceLink.id == data.objectId + (f) => f.datasourceLink && f.datasourceLink.id == data.objectId, ); // update relation data @@ -1510,7 +1787,7 @@ module.exports = class ABDataCollectionCore extends ABMLClass { // ).length > 0 ) { updateRelateVals[f.relationName()] = relateVal.filter( - (v) => (v.id || v[PK] || v) != deleteId + (v) => (v.id || v[PK] || v) != deleteId, ); updateRelateVals[f.columnName] = updateRelateVals[ f.relationName() @@ -1562,6 +1839,137 @@ module.exports = class ABDataCollectionCore extends ABMLClass { this.setStaticCursor(); }, }); + + this.eventAdd({ + emitter: linkDC, + eventName: "cursorStale", + listener: (currentCursor) => { + // cursorStale : the current cursor hasn't CHANGED, but the data + // of that value has changed. + // This is triggered by one of our socket updates that detects + // changes to the cursor data. + + // if don't have .loadAll set, we'll need to update our data: + if (!this.settings?.loadAll) { + // What I do here depends on what my linked DC has set for + // it's .loadConnections (shouldPopulate) value + let dvLink = this.datacollectionLink; + let isMyDataThere = dvLink.shouldPopulate; + if (Array.isArray(isMyDataThere)) { + // if this was an array: it should be an array of columnNames + // of the dvLink that are being populated. + + // the column I'm interested in: + let colName = this.fieldLink.fieldLink.columnName; + + // is it there? + isMyDataThere = isMyDataThere.indexOf(colName) > -1; + } + if (!isMyDataThere) { + // If it didn't populate it's data, then I can't be + // efficient about how to update my data. + // we'll just have to reload: + + // find out how many entries we have already loaded and try to + // load at least that many again.: + let count = 20; + if (this.__dataCollection.count() > count) + count = this.__dataCollection.count(); + if (this.__treeCollection?.count() > count) + count = this.__treeCollection.count(); + + let currCursor = this.__dataCollection.getCursor(); + this.clearAll(); + this.reloadData(0, count).then(() => { + this.__dataCollection.setCursor(currCursor); + this.emit("cursorSelect", currCursor); + }); + } else { + // if the linked DC does have my data populated: + + // the values I currently contain can fall into 1 of 3 categories: + // 1) A value I currently have that I need to Keep + // 2) A value I currently have that I need to remove + // 3) A value I don't have, but need to Add + + // the current value of the cursor should have the ID references + // to what SHOULD be in my display + + // get the current cursor of our linked DC + let linkCursor; + + if (dvLink) { + linkCursor = dvLink.getCursor(); + } + if (!linkCursor) { + // if linkCursor is no longer set, then we should clear() + this.clearAll(); + return; + } + + let PK = this.datasource.PK(); + + // lets start by assuming all the current values in cursor are #3 + // -> all the values into valuesToAdd: + + let colName = this.fieldLink.fieldLink.relationName(); + let valuesToAdd = {}; + let valuesIn = linkCursor[colName] || []; + if (!Array.isArray(valuesIn)) valuesIn = [valuesIn]; + valuesIn = valuesIn.filter((v) => v); + valuesIn.forEach((v) => { + valuesToAdd[v[PK]] = v; + }); + + let valuesToRemove = []; + // step through all the values I currently have + let currValues = this.__dataCollection.find(() => true); + currValues.forEach((value) => { + // if value is in valuesToAdd + if (valuesToAdd[value[PK]]) { + // we already have it so turns out we don't need to add + // it after all + delete valuesToAdd[value[PK]]; + } else { + // the current state of the Link Cursor value doesn't have + // this value, so we need to remove it: + valuesToRemove.push(value[PK]); + } + }); + + // now remove the items we don't want: + this.__dataCollection.remove(valuesToRemove); + + // then we have to ask for the values we need to add: + valuesToAdd = Object.keys(valuesToAdd); // convert to [] + if (valuesToAdd.length > 0) { + let cond = { where: {} }; + cond.where[PK] = valuesToAdd; + // NOTE: we are using the abbreviated condition syntax here. + + // NOTE: staleRefresh() has some buffering capabilities + // that combine multiple calls into 1 more efficient call: + this.model.staleRefresh(cond).then((res) => { + // check to make sure there is data to work with + if (Array.isArray(res.data) && res.data.length) { + res.data.forEach((d) => { + if (!this.__dataCollection.exists(d[PK])) { + this.__dataCollection.add(d); + } + }); + } + }); + } + } + + return; + } + + // Otherwise, we need to refilter our data: + this.refreshLinkCursor(); + this.setStaticCursor(); + }, + }); } // add listeners when cursor of the followed data collection is changed @@ -1585,6 +1993,20 @@ module.exports = class ABDataCollectionCore extends ABMLClass { this.loadData(); }, }); + + this.eventAdd({ + emitter: followDC, + eventName: "cursorStale", + listener: () => { + // cursorStale : the current cursor hasn't CHANGED, but the data + // of that value has changed. + // This is triggered by one of our socket updates that detects + // changes to the cursor data. + + this.clearAll(); + this.loadData(); + }, + }); } } @@ -1602,6 +2024,7 @@ module.exports = class ABDataCollectionCore extends ABMLClass { DC.init(); return new Promise((resolve, reject) => { + /* eslint-disable no-fallthrough */ switch (DC.dataStatus) { // if that DC hasn't started initializing yet, start it! case DC.dataStatusFlag.notInitial: @@ -1609,7 +2032,7 @@ module.exports = class ABDataCollectionCore extends ABMLClass { // no break; // once in the process of initializing - /* eslint-disable no-fallthrough*/ + case DC.dataStatusFlag.initializing: /* eslint-enable no-fallthrough*/ // listen for "initializedData" event from the DC @@ -1635,55 +2058,56 @@ module.exports = class ABDataCollectionCore extends ABMLClass { resolve(); break; } + /* eslint-enable no-fallthrough */ }); } - /** - * @method whereCleanUp() - * Parse through the current where condition and remove any null or - * empty logical blocks. - * @param {obj} curr - * 1) The current where condition in ABQuery Format: - * { - * glue: [AND, OR], - * rules: [ {rule} ] - * } - * or 2) The current {rule} to validate - * { - * key:{string}, - * rule:{string}, - * vlaue:{mixed} - * } - * @return {ABQuery.where} / { Rule } - */ - whereCleanUp(curr) { - if (curr) { - if (curr.glue && curr.rules) { - // this is a logical Block (AND, OR) - // we need to filter the children - let newValue = { glue: curr.glue, rules: [] }; - curr.rules.forEach((r) => { - let cleanRule = this.whereCleanUp(r); - // don't add values that didn't pass - if (cleanRule) { - newValue.rules.push(cleanRule); - } - }); - - // if we have a non empty block, then return it: - if (newValue.rules.length > 0) { - return newValue; - } - - // this isn't really a valid conditional, so null - return null; - } - - // This is a specific rule, that isn't null so: - return curr; - } - return null; - } + // /** + // * @method whereCleanUp() + // * Parse through the current where condition and remove any null or + // * empty logical blocks. + // * @param {obj} curr + // * 1) The current where condition in ABQuery Format: + // * { + // * glue: [AND, OR], + // * rules: [ {rule} ] + // * } + // * or 2) The current {rule} to validate + // * { + // * key:{string}, + // * rule:{string}, + // * vlaue:{mixed} + // * } + // * @return {ABQuery.where} / { Rule } + // */ + // whereCleanUp(curr) { + // if (curr) { + // if (curr.glue && curr.rules) { + // // this is a logical Block (AND, OR) + // // we need to filter the children + // let newValue = { glue: curr.glue, rules: [] }; + // curr.rules.forEach((r) => { + // let cleanRule = this.whereCleanUp(r); + // // don't add values that didn't pass + // if (cleanRule) { + // newValue.rules.push(cleanRule); + // } + // }); + + // // if we have a non empty block, then return it: + // if (newValue.rules.length > 0) { + // return newValue; + // } + + // // this isn't really a valid conditional, so null + // return null; + // } + + // // This is a specific rule, that isn't null so: + // return curr; + // } + // return null; + // } async loadData(start, limit) { // mark data status is initializing @@ -1709,19 +2133,22 @@ module.exports = class ABDataCollectionCore extends ABMLClass { // pull filter conditions let wheres = this.AB.cloneDeep( - this.settings.objectWorkspace.filterConditions ?? null + this.settings.objectWorkspace.filterConditions ?? {} ); // if we pass new wheres with a reload use them instead if (this.__reloadWheres) { wheres = this.__reloadWheres; } + wheres.glue = wheres.glue || "and"; + wheres.rules = wheres.rules || []; const __additionalWheres = { glue: "and", rules: [], }; - if (this.__filterCond) { + // add the filterCond if there are rules to add + if (this.__filterCond?.rules?.length > 0) { __additionalWheres.rules.push(this.__filterCond); } @@ -1780,7 +2207,7 @@ module.exports = class ABDataCollectionCore extends ABMLClass { // remove any null in the .rules // if (wheres?.rules?.filter) wheres.rules = wheres.rules.filter((r) => r); - wheres = this.whereCleanUp(wheres); + wheres = obj.whereCleanUp(wheres); // set query condition var cond = { @@ -1788,9 +2215,7 @@ module.exports = class ABDataCollectionCore extends ABMLClass { // limit: limit || 20, skip: start || 0, sort: sorts, - populate: - this.settings.populate ?? - (this.settings.preventPopulate ? false : true), + populate: this.shouldPopulate, }; //// NOTE: we no longer set a default limit on loadData() but @@ -1866,6 +2291,20 @@ module.exports = class ABDataCollectionCore extends ABMLClass { }); } + /** + * @method shouldPopulate() + * Return our populate status. We now want to query this info outside this + * object. + * @return {bool|Array} + * true/false, or an array of columnNames that are being populated. + */ + get shouldPopulate() { + return ( + this.settings.populate ?? + (this.settings.preventPopulate ? false : true) + ); + } + /** * @method queuedParse() * This is an attempt at loading very large datasets into a Webix DC without locking up @@ -2183,7 +2622,7 @@ module.exports = class ABDataCollectionCore extends ABMLClass { linkVal.filter( (val) => (val[PK] || val.id || val) == - (linkCursor[linkObj.PK()] || linkCursor.id || linkCursor) + (linkCursor[linkObj.PK()] || linkCursor.id || linkCursor), ).length > 0 ); } else { @@ -2248,7 +2687,7 @@ module.exports = class ABDataCollectionCore extends ABMLClass { key: fieldLink.id, rule: fieldLink.alias ? "contains" : "equals", // NOTE: If object is query, then use "contains" because ABOBjectQuery return JSON value: fieldLink.getRelationValue( - dataCollectionLink.__dataCollection.getItem(linkCursorId) + dataCollectionLink.__dataCollection.getItem(linkCursorId), ), }; } @@ -2277,7 +2716,7 @@ module.exports = class ABDataCollectionCore extends ABMLClass { // Set filter of ABObject if (this.__filterDatasource == null) this.__filterDatasource = this.AB.filterComplexNew( - `${this.id}_filterDatasource` + `${this.id}_filterDatasource`, ); if (this.datasource) { @@ -2306,7 +2745,7 @@ module.exports = class ABDataCollectionCore extends ABMLClass { } else { this.__filterDatasource.fieldsLoad([]); this.__filterDatasource.setValue( - DefaultValues.settings.objectWorkspace.filterConditions + DefaultValues.settings.objectWorkspace.filterConditions, ); } @@ -2314,14 +2753,14 @@ module.exports = class ABDataCollectionCore extends ABMLClass { // Apr 29, 2021 Added this code back to validate with DataCollection Filters if (this.__filterDatacollection == null) this.__filterDatacollection = this.AB.filterComplexNew( - `${this.id}_filterDatacollection` + `${this.id}_filterDatacollection`, ); // this.__filterDatacollection.applicationLoad( // this.datasource ? this.datasource.application : null // ); this.__filterDatacollection.fieldsLoad( - this.datasource ? this.datasource.fields() : [] + this.datasource ? this.datasource.fields() : [], ); // if we pass in wheres, then Save that value to our internal .filterConditions @@ -2331,8 +2770,12 @@ module.exports = class ABDataCollectionCore extends ABMLClass { this.settings.objectWorkspace?.filterConditions ?? { glue: "and", rules: [], - } + }, ); + // sanity checks: + // I've learned not to trust: this.settings.objectWorkspace + filter.glue = filter.glue || "and"; + filter.rules = filter.rules || []; // if there is a linkRule, add it to filter let linkRule = this.ruleLinkedData(); // returns a rule if we are linked @@ -2368,25 +2811,25 @@ module.exports = class ABDataCollectionCore extends ABMLClass { } } - if (filter.rules.length > 0) { + if ((filter.rules || []).length > 0 && !this.isCursorFollow) { this.__filterDatacollection.setValue(filter); } else { this.__filterDatacollection.setValue( - DefaultValues.settings.objectWorkspace.filterConditions + DefaultValues.settings.objectWorkspace.filterConditions, ); } // Set filter of user's scope if (this.__filterScope == null) this.__filterScope = this.AB.filterComplexNew( - `${this.id}_filterScope` + `${this.id}_filterScope`, ); if (this.datasource) { let scopeList = (this.userScopes || []).filter( (s) => !s.allowAll && - (s.objectIds || []).indexOf(this.datasource.id) > -1 + (s.objectIds || []).indexOf(this.datasource.id) > -1, ); if (scopeList && scopeList.length > 0) { // this.__filterScope.applicationLoad(this.datasource.application); @@ -2396,12 +2839,12 @@ module.exports = class ABDataCollectionCore extends ABMLClass { let scopeRules = []; scopeList .filter( - (s) => s.filter && s.filter.rules && s.filter.rules.length + (s) => s.filter && s.filter.rules && s.filter.rules.length, ) .forEach((s) => { let sRules = (s.filter.rules || []).filter( (r) => - this.datasource.fields((f) => f.id == r.key).length > 0 + this.datasource.fields((f) => f.id == r.key).length > 0, ); scopeRules = scopeRules.concat(sRules); @@ -2447,7 +2890,7 @@ module.exports = class ABDataCollectionCore extends ABMLClass { */ _dataCollectionNew(/*data*/) { var error = new Error( - "the platform.ABDataCollection._dataCollectionNew() is expected to return a proper DataCollection!" + "the platform.ABDataCollection._dataCollectionNew() is expected to return a proper DataCollection!", ); console.error(error); return null; @@ -2462,7 +2905,7 @@ module.exports = class ABDataCollectionCore extends ABMLClass { */ _treeCollectionNew() { console.error( - "the platform.ABDataCollection._treeCollectionNew() is expected to return a proper TreeCollection!" + "the platform.ABDataCollection._treeCollectionNew() is expected to return a proper TreeCollection!", ); return null; } @@ -2471,7 +2914,7 @@ module.exports = class ABDataCollectionCore extends ABMLClass { // TODO all this does is log "is missing?" if (data === {}) { console.log( - "Platform.ABDataCollection.parseTreeCollection() missing!" + "Platform.ABDataCollection.parseTreeCollection() missing!", ); } } @@ -2667,7 +3110,7 @@ module.exports = class ABDataCollectionCore extends ABMLClass { // clonedDatacollection.__dataCollection = this.__dataCollection.copy(); clonedDatacollection.__filterDatacollection.setValue( - settings.settings.objectWorkspace.filterConditions + settings.settings.objectWorkspace.filterConditions, ); var parseMe = () => { @@ -2676,8 +3119,8 @@ module.exports = class ABDataCollectionCore extends ABMLClass { this.__dataCollection .find({}) .filter((row) => - clonedDatacollection.__filterDatacollection.isValid(row) - ) + clonedDatacollection.__filterDatacollection.isValid(row), + ), ); } if (clonedDatacollection.__treeCollection) { @@ -2685,8 +3128,8 @@ module.exports = class ABDataCollectionCore extends ABMLClass { this.__treeCollection .find({}) .filter((row) => - clonedDatacollection.__filterDatacollection.isValid(row) - ) + clonedDatacollection.__filterDatacollection.isValid(row), + ), ); } }; @@ -2727,7 +3170,7 @@ module.exports = class ABDataCollectionCore extends ABMLClass { if (obj.settings.objectWorkspace.filterConditions?.rules?.length) { obj.settings.objectWorkspace.filterConditions.rules = obj.settings.objectWorkspace.filterConditions.rules.concat( - filters.rules + filters.rules, ); } else { obj.settings.objectWorkspace.filterConditions = filters; @@ -2820,11 +3263,26 @@ module.exports = class ABDataCollectionCore extends ABMLClass { if (!this.isCursorFollow) return null; return (this.AB ?? AB).datacollectionByID( - this.settings.followDatacollectionID + this.settings.followDatacollectionID, ); } get previousCursorId() { return this.__previousCursorId; } -}; + + waitReady() { + // if (this.dataStatus == this.dataStatusFlag.initialized) { + // return Promise.resolve(); + // } + // console.log(`DC[${this.label}] waiting for initializedData.`); + // return new Promise((resolve /*, reject */) => { + // this.once("initializedData", ()=>{ + // resolve(); + // }) + + // }) + + return this.waitForDataCollectionToInitialize(this); + } +}; \ No newline at end of file diff --git a/ABFactoryCore.js b/ABFactoryCore.js index 1525966..ea46d82 100644 --- a/ABFactoryCore.js +++ b/ABFactoryCore.js @@ -703,6 +703,10 @@ class ABFactory extends EventEmitter { return this.objectByID("d84cd351-d96c-490f-9afb-2a0b880ca0ec"); } + objectProcessDefinition() { + return this.objectByID("af91fc75-fb73-4d71-af14-e22832eb5915"); + } + objectProcessForm() { return this.objectByID("d36ae4c8-edef-48d8-bd9c-79a0edcaa067"); } @@ -825,7 +829,7 @@ class ABFactory extends EventEmitter { var newStep = new ABStep(params, this); return newStep; } - return null; + // return null; } // diff --git a/ABHintCore.js b/ABHintCore.js index 9611679..e041a12 100644 --- a/ABHintCore.js +++ b/ABHintCore.js @@ -35,9 +35,8 @@ module.exports = class ABHintCore extends ABMLClass { } } */ - let active = attributes?.settings.hasOwnProperty("active") - ? attributes?.settings?.active - : "1"; + + let active = attributes?.settings?.active ?? "1"; this.id = attributes?.id || ""; this.name = attributes?.name || "New Tutorial"; diff --git a/ABMobileAppCore.js b/ABMobileAppCore.js index 80e1f09..f81c8fd 100644 --- a/ABMobileAppCore.js +++ b/ABMobileAppCore.js @@ -62,7 +62,7 @@ module.exports = class ABMobileAppCore extends ABMLClass { type: this.type || "mobile.application", name: this.name, settings: this.settings, - translations: obj.translations + translations: obj.translations, }; } }; diff --git a/ABModelCore.js b/ABModelCore.js index 1306eba..9b49879 100644 --- a/ABModelCore.js +++ b/ABModelCore.js @@ -215,7 +215,8 @@ module.exports = class ABModelCore { }; return this.request("get", params) .then((numberOfRows) => { - resolve(numberOfRows); + // resolve(numberOfRows); + return numberOfRows; }) .catch((err) => { // TODO: this should be done in platform/ABModel @@ -292,7 +293,7 @@ module.exports = class ABModelCore { var responseHash = { /* id : [{entry}] */ }; - var cond = { where: {} }; + var cond = { where: {}, populate: true }; cond.where[PK] = []; console.log( @@ -343,7 +344,13 @@ module.exports = class ABModelCore { ); } allKeys.forEach((key) => { - var resolve = responseHash[key].resolve; + let entry = responseHash[key]; + let resolve; + if (Array.isArray(entry)) { + resolve = entry[0].resolve; + } else { + resolve = entry.resolve; + } resolve({ data: [] }); delete responseHash[key]; }); @@ -435,7 +442,7 @@ module.exports = class ABModelCore { if (fields.length == 1) { let data = myObj[ - fields[0].replace(/[^a-z0-9\.]/gi, "") + "__relation" + fields[0].replace(/[^a-z0-9.]/gi, "") + "__relation" ]; if (!data) return resolve([]); @@ -458,7 +465,7 @@ module.exports = class ABModelCore { var returnData = {}; fields.forEach((colName) => { returnData[colName] = - myObj[colName.replace(/[^a-z0-9\.]/gi, "") + "__relation"]; + myObj[colName.replace(/[^a-z0-9.]/gi, "") + "__relation"]; }); resolve(returnData); diff --git a/ABObjectCore.js b/ABObjectCore.js index ae0707b..c839443 100644 --- a/ABObjectCore.js +++ b/ABObjectCore.js @@ -724,7 +724,7 @@ module.exports = class ABObjectCore extends ABMLClass { * @return {string} */ urlRestCount() { - return `/app_builder/model/count/${this.id}`; + return `/app_builder/model/${this.id}/count`; } /// @@ -932,6 +932,11 @@ module.exports = class ABObjectCore extends ABMLClass { fields.push(field.columnName); }); } + // Default defining label + else { + const defaultFld = this.fields((f) => f.fieldUseAsLabel())[0]; + if (defaultFld) fields.push(defaultFld.columnName); + } // System requires to include number field values // because they are used on Formula/Calculate fields on client side @@ -1003,5 +1008,55 @@ module.exports = class ABObjectCore extends ABMLClass { return labelData; } -}; + /** + * @method whereCleanUp() + * Parse through the current where condition and remove any null or + * empty logical blocks. + * @param {obj} curr + * 1) The current where condition in ABQuery Format: + * { + * glue: [AND, OR], + * rules: [ {rule} ] + * } + * or 2) The current {rule} to validate + * { + * key:{string}, + * rule:{string}, + * vlaue:{mixed} + * } + * @return {ABQuery.where} / { Rule } + */ + whereCleanUp(curr) { + if (curr) { + if (curr.glue && curr.rules) { + // this is a logical Block (AND, OR) + // we need to filter the children + let newValue = { glue: curr.glue, rules: [] }; + curr.rules.forEach((r) => { + let cleanRule = this.whereCleanUp(r); + // don't add values that didn't pass + if (cleanRule) { + newValue.rules.push(cleanRule); + } + }); + + // if we have a non empty block, then return it: + if (newValue.rules.length > 0) { + return newValue; + } + + // this isn't really a valid conditional, so null + return null; + } + + // This is a specific rule, that isn't null so: + // if it isn't {}, then return it + if (Object.keys(curr).length > 0) return curr; + + // otherwise we skip this as well + return null; + } + return null; + } +}; diff --git a/ABProcessCore.js b/ABProcessCore.js index da2e007..6ec93ef 100644 --- a/ABProcessCore.js +++ b/ABProcessCore.js @@ -408,7 +408,7 @@ module.exports = class ABProcessCore extends ABMLClass { // : values[0] // : null; - var tasksToAsk = this.allPreviousTasks(currElement) + var tasksToAsk = this.allPreviousTasks(currElement); var values = queryPreviousTasks(tasksToAsk, "processData", params, this); return values.length > 0 ? values.length > 1 diff --git a/ABScopeCore.js b/ABScopeCore.js index c8765f0..a56dc60 100644 --- a/ABScopeCore.js +++ b/ABScopeCore.js @@ -49,7 +49,7 @@ module.exports = class ABScopeCore { createdBy: this.createdBy, filter: this.filter, allowAll: this.allowAll || false, - objectIds: this.objects().map((o) => o.id) + objectIds: this.objects().map((o) => o.id), }; } diff --git a/ABViewManagerCore.js b/ABViewManagerCore.js index 5c5871c..81dab9a 100644 --- a/ABViewManagerCore.js +++ b/ABViewManagerCore.js @@ -28,6 +28,7 @@ var AllViews = [ require("../platform/views/ABViewLayout"), require("../platform/views/ABViewList"), require("../platform/views/ABViewMenu"), + require("../platform/views/ABViewOrgChart"), require("../platform/views/ABViewPage"), require("../platform/views/ABViewPDFImporter"), require("../platform/views/ABViewPivot"), diff --git a/FilterComplexCore.js b/FilterComplexCore.js index 20af0ec..d9064db 100644 --- a/FilterComplexCore.js +++ b/FilterComplexCore.js @@ -50,24 +50,32 @@ function getFieldVal(rowData, field) { function getConnectFieldValue(rowData, field) { let connectedVal = ""; - if (rowData) { - let relationName = field.relationName(); - if (rowData[relationName]) { - connectedVal = + const extractVal = (itemData) => { + let val; + const relationName = field.relationName(); + if (itemData[relationName]) { + val = (field.indexField - ? rowData[relationName][field.indexField.columnName] + ? itemData[relationName][field.indexField.columnName] : null) ?? // custom index (field.indexField2 - ? rowData[relationName][field.indexField2.columnName] + ? itemData[relationName][field.indexField2.columnName] : null) ?? // custom index 2 - rowData[relationName].id ?? - rowData[relationName]; + itemData[relationName].id ?? + itemData[relationName]; } else { - let fieldVal = getFieldVal(rowData, field); + let fieldVal = getFieldVal(itemData, field); if (fieldVal != null) { - connectedVal = fieldVal; + val = fieldVal; } } + return val; + }; + + if (Array.isArray(rowData)) { + connectedVal = rowData.map((data) => extractVal(data)); + } else if (rowData) { + connectedVal = extractVal(rowData); } return connectedVal; } @@ -296,7 +304,6 @@ module.exports = class FilterComplexCore extends ABComponent { if (!(compareValue instanceof Date)) compareValue = new Date(compareValue); - switch (rule) { case "less": result = value < compareValue; @@ -310,11 +317,14 @@ module.exports = class FilterComplexCore extends ABComponent { case "greater_or_equal": result = value >= compareValue; break; + case "is_current_date": + result = + value.setHours(0, 0, 0, 0) == compareValue.setHours(0, 0, 0, 0); + break; default: result = this.queryFieldValid(value, rule, compareValue); break; } - return result; } @@ -383,7 +393,11 @@ module.exports = class FilterComplexCore extends ABComponent { result = value == true || value > 0 || value == "true"; break; case "unchecked": - result = value == false || value <= 0 || value == "false" || value == null; + result = + value == false || + value <= 0 || + value == "false" || + value == null; break; default: result = this.queryFieldValid(value, rule, compareValue); @@ -394,42 +408,46 @@ module.exports = class FilterComplexCore extends ABComponent { } userValid(value, rule, compareValue) { - if (!value) return false; + if (!value || !value?.length) return false; let result = false; - // if (Array.isArray(value)) value = [value]; + if (!Array.isArray(value)) value = [value]; + /* eslint-disable no-fallthrough */ switch (rule) { case "is_current_user": - result = value == this.Account.username; + result = + value.filter((v) => (v?.username || v) == this.Account.username) + .length > 0; break; case "is_not_current_user": - result = value != this.Account.username; + result = + value.filter((v) => (v?.username || v) != this.Account.username) + .length > 0; break; case "contain_current_user": - if (!Array.isArray(value)) value = [value]; + compareValue = this.Account.username; + // break; <-- NO BREAK HERE: fall through to "equals" + case "equals": result = - value.filter((v) => (v.username || v) == this.Account.username) - .length > 0; + value.filter((v) => (v?.username || v) == compareValue).length > + 0; break; case "not_contain_current_user": - if (!Array.isArray(value)) value = [value]; + compareValue = this.Account.username; + // break; <-- NO BREAK HERE: fall through to "not_equals" - result = - value.filter((v) => (v.username || v) == this.Account.username) - .length < 1; - break; - case "equals": - result = (value ?? []).indexOf(compareValue) > -1; - break; case "not_equal": - result = (value ?? []).indexOf(compareValue) < 0; + result = + value.filter((v) => (v?.username || v) == compareValue).length < + 1; break; default: result = this.queryFieldValid(value, rule, compareValue); break; } + /* eslint-enable no-fallthrough */ return result; } @@ -559,7 +577,12 @@ module.exports = class FilterComplexCore extends ABComponent { connectedVal; } - let compareValueLowercase = (compareValue || "").toLowerCase(); + // Compare value isn't always a string? + // https://appdev-designs.sentry.io/issues/5056850389/ + let compareValueLowercase = + typeof compareValue === "string" + ? compareValue.toLowerCase?.() + : compareValue; switch (rule) { case "contains": @@ -922,6 +945,7 @@ module.exports = class FilterComplexCore extends ABComponent { greater: this.labels.component.afterCondition, less_or_equal: this.labels.component.onOrBeforeCondition, greater_or_equal: this.labels.component.onOrAfterCondition, + is_current_date: this.labels.component.isCurrentDateCondition, less_current: this.labels.component.beforeCurrentCondition, greater_current: this.labels.component.afterCurrentCondition, less_or_equal_current: @@ -935,14 +959,22 @@ module.exports = class FilterComplexCore extends ABComponent { let result = []; for (let condKey in dateConditions) { - result.push({ - id: condKey, - value: dateConditions[condKey], - batch: "datepicker", - handler: (a, b) => this.dateValid(a, condKey, b), - }); + if (condKey == "is_current_date") { + result.push({ + id: condKey, + value: dateConditions[condKey], + batch: "none", + handler: (a, b) => this.dateValid(a, condKey, b), + }); + } else { + result.push({ + id: condKey, + value: dateConditions[condKey], + batch: "datepicker", + handler: (a, b) => this.dateValid(a, condKey, b), + }); + } } - return result; } @@ -1329,6 +1361,7 @@ module.exports = class FilterComplexCore extends ABComponent { "is_not_empty", "checked", "unchecked", + "is_current_date", ]; const isCompleteRules = (rules = []) => { diff --git a/dataFields/ABFieldAutoIndexCore.js b/dataFields/ABFieldAutoIndexCore.js index d105b39..ff9a4f1 100644 --- a/dataFields/ABFieldAutoIndexCore.js +++ b/dataFields/ABFieldAutoIndexCore.js @@ -150,4 +150,3 @@ module.exports = class ABFieldAutoIndexCore extends ABField { } } }; - diff --git a/dataFields/ABFieldDateCore.js b/dataFields/ABFieldDateCore.js index 73c3f58..f65cb48 100644 --- a/dataFields/ABFieldDateCore.js +++ b/dataFields/ABFieldDateCore.js @@ -398,9 +398,11 @@ module.exports = class ABFieldDateCore extends ABField { // } exportValue(value) { - return value ? this.AB.rules.toDateFormat(value, { - format: "YYYY-MM-DD", - }) : ""; + return value + ? this.AB.rules.toDateFormat(value, { + format: "YYYY-MM-DD", + }) + : ""; // return this.convertToMoment(value).format("YYYY-MM-DD"); } diff --git a/dataFields/ABFieldEmailCore.js b/dataFields/ABFieldEmailCore.js index 7784864..d6cd5c6 100644 --- a/dataFields/ABFieldEmailCore.js +++ b/dataFields/ABFieldEmailCore.js @@ -124,7 +124,8 @@ module.exports = class ABFieldEmailCore extends ABField { */ isValidData(data, validator) { if (data[this.columnName]) { - const Reg = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; + const Reg = + /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; let value = data[this.columnName]; value = String(value).toLowerCase(); diff --git a/dataFields/ABFieldFileCore.js b/dataFields/ABFieldFileCore.js index a8dc454..56956bc 100644 --- a/dataFields/ABFieldFileCore.js +++ b/dataFields/ABFieldFileCore.js @@ -180,7 +180,9 @@ module.exports = class ABFieldFileCore extends ABField { if ("string" === typeof val) { try { myParameter[this.columnName] = JSON.parse(val); - } catch (e) {} + } catch (e) { + /* ignore */ + } } return myParameter; diff --git a/dataFields/ABFieldNumberCore.js b/dataFields/ABFieldNumberCore.js index a4ced28..d86adfa 100644 --- a/dataFields/ABFieldNumberCore.js +++ b/dataFields/ABFieldNumberCore.js @@ -276,7 +276,7 @@ module.exports = class ABFieldNumberCore extends ABField { format(rowData) { if ( - rowData?.[this.columnName] == null || + rowData?.[this.columnName] == null || (rowData[this.columnName] != 0 && rowData[this.columnName] == "") ) return ""; diff --git a/process/ABProcessEngineCore.js b/process/ABProcessEngineCore.js index db55d44..87be18c 100644 --- a/process/ABProcessEngineCore.js +++ b/process/ABProcessEngineCore.js @@ -8,7 +8,7 @@ module.exports = class ABProcessEngineCore { if (!this.instance.jsonDefinition) { this.instance.jsonDefinition = convert.xml2js( this.instance.xmlDefinition, - { compact: true } + { compact: true }, ); } if (!this.instance.hashDiagramObjects) { @@ -36,11 +36,14 @@ module.exports = class ABProcessEngineCore { */ }; - var processDefinitions = this.instance.jsonDefinition[ - "bpmn2:definitions" - ]["bpmn2:process"]; + const jsonDefinition = this.instance.jsonDefinition?.definition ?? this.instance.jsonDefinition ?? this.instance.definition; + const processDefinitions = jsonDefinition?.["bpmn2:definitions"]?.["bpmn2:process"]; - this.setHashDiagramObjects(processDefinitions); + if (processDefinitions) + this.setHashDiagramObjects(processDefinitions); + // else { + // Should LOGGING ? + // } // var typeLookup = [ // { xmlRef: "bpmn2:sequenceFlow", type: "flow" }, @@ -103,7 +106,7 @@ module.exports = class ABProcessEngineCore { */ startTask() { return this.process.elementForDiagramID( - this.instance.context.startTaskID + this.instance.context.startTaskID, ); } @@ -181,4 +184,3 @@ module.exports = class ABProcessEngineCore { }); } }; - diff --git a/process/ABProcessTaskManager.js b/process/ABProcessTaskManager.js index fb6171a..7674bfc 100644 --- a/process/ABProcessTaskManager.js +++ b/process/ABProcessTaskManager.js @@ -50,16 +50,14 @@ AllProcessElements.forEach((ELEMENT) => { switch (ELEMENT.defaults().category) { case "start": case "end": - DEFINITIONTYPES[ - ELEMENT.DiagramReplace().target.eventDefinitionType - ] = ELEMENT.defaults(); + DEFINITIONTYPES[ELEMENT.DiagramReplace().target.eventDefinitionType] = + ELEMENT.defaults(); break; case "gateway": case "task": - DEFINITIONTYPES[ - ELEMENT.DiagramReplace().target.type - ] = ELEMENT.defaults(); + DEFINITIONTYPES[ELEMENT.DiagramReplace().target.type] = + ELEMENT.defaults(); break; } }); @@ -135,4 +133,3 @@ module.exports = { return DEFINITIONTYPES[key]; }, }; - diff --git a/process/tasks/ABProcessElementCore.js b/process/tasks/ABProcessElementCore.js index c173767..144233f 100644 --- a/process/tasks/ABProcessElementCore.js +++ b/process/tasks/ABProcessElementCore.js @@ -238,7 +238,7 @@ module.exports = class ABProcessTaskCore extends ABMLClass { var myDiagramObj = instance.hashDiagramObjects[this.diagramID]; if (!myDiagramObj) { - var error = new Error( + let error = new Error( `Configuration Error: Did not find my definition for dID[${this.diagramID}]` ); this.onError(instance, error); @@ -255,7 +255,7 @@ module.exports = class ABProcessTaskCore extends ABMLClass { // find my possible exits: var exitFlows = myDiagramObj["bpmn2:outgoing"]; if (!exitFlows) { - var error = new Error( + let error = new Error( `Configuration Error: Did not find any outgoing flows for dID[${this.diagramID}]` ); this.AB.notify.builder(error, { task: this }); @@ -288,7 +288,7 @@ module.exports = class ABProcessTaskCore extends ABMLClass { nextTasks.push(targetTask); } } else { - var error = new Error( + let error = new Error( `Configuration Error: No ProcessTask instance for diagramID[${tid}]` ); this.AB.notify.builder(error, { task: this }); diff --git a/process/tasks/ABProcessGatewayExclusiveCore.js b/process/tasks/ABProcessGatewayExclusiveCore.js index 8a44095..c7983d6 100644 --- a/process/tasks/ABProcessGatewayExclusiveCore.js +++ b/process/tasks/ABProcessGatewayExclusiveCore.js @@ -93,7 +93,6 @@ module.exports = class ABProcessGatewayExclusiveCore extends ABProcessElement { //// Process Instance Methods //// - /** * initState() * setup this task's initial state variables diff --git a/process/tasks/ABProcessTaskUserApprovalCore.js b/process/tasks/ABProcessTaskUserApprovalCore.js index 6a9143a..611d91b 100644 --- a/process/tasks/ABProcessTaskUserApprovalCore.js +++ b/process/tasks/ABProcessTaskUserApprovalCore.js @@ -213,6 +213,13 @@ module.exports = class ABProcessTaskUserApprovalCore extends ABProcessElement { myObj ); + // NOTE: We are pretending our response is a type of ABFieldList. But our + // ABField objects no longer allow "." in our columnNames: + // ( https://github.com/digi-serve/appbuilder_class_core/blob/212cf5fa1c1d5c959aa246c730582ed50809ee0f/dataFields/ABFieldCore.js#L262 ) + // But our Process tasks really will be expecting it there so lets put + // it back: + listField.columnName = `${myID}.userFormResponse`; + return [ { key: `${myID}.userFormResponse`, diff --git a/process/tasks/ABProcessTriggerLifecycleCore.js b/process/tasks/ABProcessTriggerLifecycleCore.js index 05be42d..e016fa8 100644 --- a/process/tasks/ABProcessTriggerLifecycleCore.js +++ b/process/tasks/ABProcessTriggerLifecycleCore.js @@ -164,9 +164,16 @@ module.exports = class ABProcessTriggerLifecycle extends ABProcessTrigger { } else if (parts[1] == "uuid") { return myState["data"]["uuid"]; } else { + /// + /// Questioning the validity of this section of code. + /// In order to get here, we tried to find field, and it + /// didn't exist. + /// then we turn around and REPEAT the same attempt + /// and check for field again. + /* // parts[1] should be a field.id - var object = this.AB.objectByID(this.objectID); - var field = object.fields((f) => { + object = this.AB.objectByID(this.objectID); + field = object.fields((f) => { return f.id == parts[1]; })[0]; if (field) { @@ -177,6 +184,7 @@ module.exports = class ABProcessTriggerLifecycle extends ABProcessTrigger { return myState["data"][field.columnName]; } } + */ } } } diff --git a/ql/ABQLRootObjectCore.js b/ql/ABQLRootObjectCore.js index a5f01fe..198a912 100644 --- a/ql/ABQLRootObjectCore.js +++ b/ql/ABQLRootObjectCore.js @@ -32,7 +32,7 @@ class ABQLObjectCore extends ABQL { /// /// Instance Methods /// - initObject(attributes) { + initObject(/* attributes */) { if (!this.object && this.params) { const objNameDef = this.parameterDefinitions.find((pDef) => { return pDef.type === "objectName"; @@ -44,7 +44,8 @@ class ABQLObjectCore extends ABQL { } if (!this.object) { - this.warningMessage("has no object set.", { + // This function exists on platform_web but not platform_service + this.warningMessage?.("has no object set.", { objectID: this.objectID, }); } diff --git a/version b/version index 2f1a5aa..0d3ad67 100644 --- a/version +++ b/version @@ -1 +1 @@ -2.1.17 +2.2.10 diff --git a/views/ABViewCSVImporterCore.js b/views/ABViewCSVImporterCore.js index bea0f2a..3eab255 100644 --- a/views/ABViewCSVImporterCore.js +++ b/views/ABViewCSVImporterCore.js @@ -108,7 +108,7 @@ module.exports = class ABViewCSVImporterCore extends ABViewWidget { get RecordRule() { let object = this.datacollection?.datasource; - if (!object) return; + if (!object) return null; if (this._recordRule == null) { this._recordRule = new ABRecordRule(); diff --git a/views/ABViewDocxBuilderCore.js b/views/ABViewDocxBuilderCore.js index a4f29ea..b8fc33d 100644 --- a/views/ABViewDocxBuilderCore.js +++ b/views/ABViewDocxBuilderCore.js @@ -71,7 +71,8 @@ module.exports = class ABViewDocxBuilderCore extends ABViewWidget { // TODO: Convert this to use ABFactory.urlFileUpload() or a ABFieldFile // to get the URL: - const object = this.datacollection.datasource; + // support uploading template when more than one data source is selected + const object = this.datacollections[0].datasource; // NOTE: file-upload API needs to have the id of ANY field. const field = object ? object.fields()[0] : null; diff --git a/views/ABViewFormButtonCore.js b/views/ABViewFormButtonCore.js index c65f18d..4bd0061 100644 --- a/views/ABViewFormButtonCore.js +++ b/views/ABViewFormButtonCore.js @@ -93,7 +93,7 @@ module.exports = class ABViewFormButtonCore extends ABView { this.unTranslate(this.settings, this.settings, labels); this.settings.includeSave = JSON.parse( - this.settings.includeSave && + (this.settings?.includeSave ?? true) && ABViewFormButtonPropertyComponentDefaults.includeSave ); this.settings.includeCancel = JSON.parse( diff --git a/views/ABViewFormCheckboxCore.js b/views/ABViewFormCheckboxCore.js index 46fda15..4b47dea 100644 --- a/views/ABViewFormCheckboxCore.js +++ b/views/ABViewFormCheckboxCore.js @@ -34,4 +34,3 @@ module.exports = class ABViewFormCheckboxCore extends ABViewFormItem { return []; } }; - diff --git a/views/ABViewFormConnectCore.js b/views/ABViewFormConnectCore.js index b8e8fe4..2b9c3f5 100644 --- a/views/ABViewFormConnectCore.js +++ b/views/ABViewFormConnectCore.js @@ -69,4 +69,3 @@ module.exports = class ABViewFormConnectCore extends ABViewFormItem { return []; } }; - diff --git a/views/ABViewFormCore.js b/views/ABViewFormCore.js index e24d66e..73afd9d 100644 --- a/views/ABViewFormCore.js +++ b/views/ABViewFormCore.js @@ -236,4 +236,3 @@ module.exports = class ABViewFormCore extends ABViewContainer { return SubmitRules.process({ data: rowData, form: this }); } }; - diff --git a/views/ABViewFormDatepickerCore.js b/views/ABViewFormDatepickerCore.js index 7ac3b48..7f0e79f 100644 --- a/views/ABViewFormDatepickerCore.js +++ b/views/ABViewFormDatepickerCore.js @@ -40,4 +40,3 @@ module.exports = class ABViewFormDatepickerCore extends ABViewFormItem { return []; } }; - diff --git a/views/ABViewFormNumberCore.js b/views/ABViewFormNumberCore.js index fcfc329..e889b59 100644 --- a/views/ABViewFormNumberCore.js +++ b/views/ABViewFormNumberCore.js @@ -74,4 +74,3 @@ module.exports = class ABViewFormNumberCore extends ABViewFormItem { return []; } }; - diff --git a/views/ABViewFormSelectMultipleCore.js b/views/ABViewFormSelectMultipleCore.js index 716b6ec..55268a3 100644 --- a/views/ABViewFormSelectMultipleCore.js +++ b/views/ABViewFormSelectMultipleCore.js @@ -36,4 +36,3 @@ module.exports = class ABViewFormSelectMultipleCore extends ABViewFormItem { return []; } }; - diff --git a/views/ABViewFormSelectSingleCore.js b/views/ABViewFormSelectSingleCore.js index 30bd2ea..75330a5 100644 --- a/views/ABViewFormSelectSingleCore.js +++ b/views/ABViewFormSelectSingleCore.js @@ -36,4 +36,3 @@ module.exports = class ABViewFormSelectSingleCore extends ABViewFormItem { return []; } }; - diff --git a/views/ABViewOrgChartCore.js b/views/ABViewOrgChartCore.js new file mode 100644 index 0000000..2387e86 --- /dev/null +++ b/views/ABViewOrgChartCore.js @@ -0,0 +1,140 @@ +const ABViewWidget = require("../../platform/views/ABViewWidget"); + +const ABViewOrgChartPropertyComponentDefaults = { + datacollectionID: "", + fields: "", + direction: "t2b", + depth: 99, + color: "#00BCD4", + // visibleLevel: 2, + pan: 1, + zoom: 1, + height: 0, + export: 0, + exportFilename: "", +}; + +const ABViewOrgChartDefaults = { + key: "orgchart", // {string} unique key for this view + icon: "sitemap", // {string} fa-[icon] reference for this view + labelKey: "OrgChart", // {string} the multilingual label key for the class label +}; + +module.exports = class ABViewOrgChartCore extends ABViewWidget { + constructor(values, application, parent, defaultValues) { + super( + values, + application, + parent, + defaultValues || ABViewOrgChartDefaults + ); + } + + static common() { + return ABViewOrgChartDefaults; + } + + static defaultValues() { + return ABViewOrgChartPropertyComponentDefaults; + } + + /// + /// Instance Methods + /// + + /** + * @method fromValues() + * + * initialze this object with the given set of values. + * @param {obj} values + */ + fromValues(values) { + super.fromValues(values); + + this.settings.datacollectionID = + this.settings.datacollectionID ?? + ABViewOrgChartPropertyComponentDefaults.datacollectionID; + + this.settings.fields = + this.settings.fields ?? ABViewOrgChartPropertyComponentDefaults.fields; + + this.settings.direction = + this.settings.direction ?? + ABViewOrgChartPropertyComponentDefaults.direction; + + this.settings.depth = parseInt( + this.settings.depth ?? ABViewOrgChartPropertyComponentDefaults.depth + ); + + this.settings.color = + this.settings.color ?? ABViewOrgChartPropertyComponentDefaults.color; + + this.settings.pan = JSON.parse( + this.settings.pan ?? ABViewOrgChartPropertyComponentDefaults.pan + ); + + this.settings.zoom = JSON.parse( + this.settings.zoom ?? ABViewOrgChartPropertyComponentDefaults.zoom + ); + + this.settings.height = parseInt( + this.settings.height ?? ABViewOrgChartPropertyComponentDefaults.height + ); + + this.settings.export = JSON.parse( + this.settings.export ?? ABViewOrgChartPropertyComponentDefaults.export + ); + + this.settings.exportFilename = + this.settings.exportFilename ?? + ABViewOrgChartPropertyComponentDefaults.exportFilename; + } + + get datacollection() { + const datacollectionID = (this.settings || {}).datacollectionID; + + return this.AB.datacollectionByID(datacollectionID); + } + + getValueFields(object) { + // OrgChart supports only one parent node. + return ( + object?.connectFields( + (f) => f.linkType() == "many" && f.linkViaType() == "one" + ) ?? [] + ); + } + + /** + * @function valueFields() + * Return IDs of connect field for each layer of OrgChart, starting from the top to the bottom. + * + * @return {Array} + * + */ + valueFields() { + let fieldValues = (this.settings?.fields ?? "").split(","); + if (!Array.isArray(fieldValues)) fieldValues = [fieldValues]; + + const result = []; + + let obj = this.datacollection?.datasource; + fieldValues.forEach((fId) => { + if (!fId) return; + + const field = obj?.fieldByID?.(fId); + if (!field) return; + + result.push(field); + obj = field.datasourceLink; + }); + + return result; + } + + // descriptionField() { + // return this.valueField()?.datasourceLink?.fieldByID?.( + // this.settings.columnDescription + // ); + // } +}; diff --git a/views/ABViewTabCore.js b/views/ABViewTabCore.js index 4463e93..2907c93 100644 --- a/views/ABViewTabCore.js +++ b/views/ABViewTabCore.js @@ -55,7 +55,7 @@ module.exports = class ABViewTabCore extends ABViewWidget { this.settings.stackTabs = parseInt(this.settings.stackTabs); this.settings.darkTheme = parseInt(this.settings.darkTheme); this.settings.sidebarWidth = parseInt(this.settings.sidebarWidth); - this.settings.sidebarPos = this.settings.sidebarPos; + // this.settings.sidebarPos = this.settings.sidebarPos; this.settings.iconOnTop = parseInt(this.settings.iconOnTop); }