diff --git a/AppBuilder/ABFactory.js b/AppBuilder/ABFactory.js index e31735e2..fb4b9ce3 100644 --- a/AppBuilder/ABFactory.js +++ b/AppBuilder/ABFactory.js @@ -130,6 +130,8 @@ class ABFactory extends ABFactoryCore { } }; + this.performance = performance; + this.UISettings = UISettings; this.Validation = { diff --git a/AppBuilder/core b/AppBuilder/core index b151db79..a45f2151 160000 --- a/AppBuilder/core +++ b/AppBuilder/core @@ -1 +1 @@ -Subproject commit b151db79e95155571132a8a68579c459800ea577 +Subproject commit a45f21514a1be50256f217d60491847438f52af6 diff --git a/AppBuilder/platform/ABModelApiNetsuite.js b/AppBuilder/platform/ABModelApiNetsuite.js new file mode 100644 index 00000000..8b845425 --- /dev/null +++ b/AppBuilder/platform/ABModelApiNetsuite.js @@ -0,0 +1,60 @@ +// +// ABModelAPINetsuite +// +// Represents the Data interface for a connection to Netsuite. + +const ABModel = require("./ABModel"); + +module.exports = class ABModelAPINetsuite extends ABModel { + /// + /// Instance Methods + /// + + /** + * @method normalizeData() + * For a Netsuite object, there are additional steps we need to handle + * to normalize our data. + */ + normalizeData(data) { + super.normalizeData(data); + + if (!Array.isArray(data)) { + data = [data]; + } + + var boolFields = this.object.fields((f) => f.key == "boolean"); + let allFields = this.object.fields(); + + data.forEach((d) => { + // Netsuite sometimes keeps keys all lowercase + // which might not match up with what it told us in the meta-catalog + // which we need: + for (var i = 0; i < allFields.length; i++) { + let actualColumn = allFields[i].columnName; + let lcColumn = actualColumn.toLowerCase(); + + if ( + typeof d[actualColumn] == "undefined" && + typeof d[lcColumn] != "undefined" + ) { + d[actualColumn] = d[lcColumn]; + delete d[lcColumn]; + } + } + + // Netsuite Booleans are "T" or "F" + boolFields.forEach((bField) => { + let val = d[bField.columnName]; + // just how many ways can a DB indicate True/False? + if (typeof val == "string") { + val = val.toLowerCase(); + + if (val === "t") val = true; + else val = false; + + d[bField.columnName] = val; + } + }); + }); + } +}; diff --git a/AppBuilder/platform/ABObjectApi.js b/AppBuilder/platform/ABObjectApi.js index ebfaecb8..a99384ca 100644 --- a/AppBuilder/platform/ABObjectApi.js +++ b/AppBuilder/platform/ABObjectApi.js @@ -69,4 +69,12 @@ module.exports = class ABObjectApi extends ABObjectApiCore { async save() { return await super.save(true); } + + migrateCreate() { + return Promise.resolve(); + } + + migrateDrop() { + return Promise.resolve(); + } }; diff --git a/AppBuilder/platform/ABObjectApiNetsuite.js b/AppBuilder/platform/ABObjectApiNetsuite.js new file mode 100644 index 00000000..4fabb341 --- /dev/null +++ b/AppBuilder/platform/ABObjectApiNetsuite.js @@ -0,0 +1,72 @@ +const ABObjectApiNetsuiteCore = require("../core/ABObjectApiNetsuiteCore"); + +module.exports = class ABObjectApiNetsuite extends ABObjectApiNetsuiteCore { + constructor(attributes, AB) { + super(attributes, AB); + } + + async getDbInfo() { + /* + // Data format: + { + "definitionId": "f2416a1a-d75c-40f2-8180-bad9b5f8b9cc", + "tableName": "AB_MockupHR_TeamTargetLocation", + "fields": [ + { + "Field": "uuid", + "Type": "varchar(255)", + "Null": "NO", + "Key": "PRI", + "Default": null, + "Extra": "" + }, + { + "Field": "created_at", + "Type": "datetime", + "Null": "YES", + "Key": "", + "Default": null, + "Extra": "" + }, + { + "Field": "updated_at", + "Type": "datetime", + "Null": "YES", + "Key": "", + "Default": null, + "Extra": "" + }, + { + "Field": "properties", + "Type": "text", + "Null": "YES", + "Key": "", + "Default": null, + "Extra": "" + } + ] + } + */ + let PK = this.PK(); + let fieldInfo = []; + this.fields().forEach((f) => { + let field = { + Field: f.columnName, + Type: f.key, + Null: f.settings.required ? "NO" : "YES", + Key: PK == f.columnName ? "PRI" : "", + Default: "", + Extra: "", + }; + fieldInfo.push(field); + }); + + let TableInfo = { + definitionId: this.id, + tableName: this.tableName, + fields: fieldInfo, + }; + + return TableInfo; + } +}; diff --git a/AppBuilder/platform/FilterComplex.js b/AppBuilder/platform/FilterComplex.js index 977e81a9..a34f7b4e 100644 --- a/AppBuilder/platform/FilterComplex.js +++ b/AppBuilder/platform/FilterComplex.js @@ -40,6 +40,13 @@ function _toInternal(cond, fields = []) { }; if (Array.isArray(cond.value)) cond.includes = cond.value; + if ( + cond.rule === "in_query_field" || + cond.rule === "not_in_query_field" + ) { + cond.includes = cond.value.split(":"); + } + // else cond.includes = cond.value?.split?.(/,|:/) ?? []; // if (field?.key == "date" || field?.key == "datetime") { diff --git a/AppBuilder/platform/dataFields/ABField.js b/AppBuilder/platform/dataFields/ABField.js index 2d9bdea4..1fc8dd13 100644 --- a/AppBuilder/platform/dataFields/ABField.js +++ b/AppBuilder/platform/dataFields/ABField.js @@ -160,7 +160,9 @@ module.exports = class ABField extends ABFieldCore { // NOTE: our .migrateXXX() routines expect the object to currently exist // in the DB before we perform the DB operations. So we need to // .migrateDrop() before we actually .objectDestroy() this. - await this.migrateDrop(); + if (!this.object.isAPI) { + await this.migrateDrop(); + } // the server still references an ABField in relationship to it's // ABObject, so we need to destroy the Field 1st, then remove it @@ -231,7 +233,7 @@ module.exports = class ABField extends ABFieldCore { // but not connectObject fields: // ABFieldConnect.migrateXXX() gets called from the UI popupNewDataField // in order to handle the timings of the 2 fields that need to be created - if (!this.isConnection && !skipMigrate) { + if (!this.isConnection && !skipMigrate && !this.object.isAPI) { const fnMigrate = isAdd ? this.migrateCreate() : this.migrateUpdate(); await fnMigrate; } diff --git a/AppBuilder/platform/dataFields/ABFieldConnect.js b/AppBuilder/platform/dataFields/ABFieldConnect.js index f68c83d9..f18fdd15 100644 --- a/AppBuilder/platform/dataFields/ABFieldConnect.js +++ b/AppBuilder/platform/dataFields/ABFieldConnect.js @@ -244,7 +244,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) { @@ -383,7 +383,7 @@ module.exports = class ABFieldConnect extends ABFieldConnectCore { return linkedModel.findAll({ where: where, sort: sort, - populate: false, + populate, }); }; @@ -423,9 +423,14 @@ 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: "uuid", + key: linkedObj.PK(), rule: "equals", value: v, }); diff --git a/AppBuilder/platform/views/ABViewOrgChartTeams.js b/AppBuilder/platform/views/ABViewOrgChartTeams.js new file mode 100644 index 00000000..c2560653 --- /dev/null +++ b/AppBuilder/platform/views/ABViewOrgChartTeams.js @@ -0,0 +1,13 @@ +const ABViewOrgChartTeamsCore = require("../../core/views/ABViewOrgChartTeamsCore"); +const ABViewOrgChartTeamsComponent = require("./viewComponent/ABViewOrgChartTeamsComponent"); + +module.exports = class ABViewOrgChartTeams extends ABViewOrgChartTeamsCore { + /** + * @method component() + * return a UI component based upon this view. + * @return {obj} UI component + */ + component() { + return new ABViewOrgChartTeamsComponent(this); + } +}; diff --git a/AppBuilder/platform/views/viewComponent/ABViewDataSelectComponent.js b/AppBuilder/platform/views/viewComponent/ABViewDataSelectComponent.js index eaac84a5..a68dbaa0 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewDataSelectComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewDataSelectComponent.js @@ -16,10 +16,11 @@ export default class ABViewDataSelectComponent extends ABViewComponent { ui() { const _ui = super.ui([ { - view: "richselect", + view: "combo", id: this.ids.select, on: { onChange: (n, o) => { + if (!o) return; if (n !== o) this.cursorChange(n); }, }, @@ -30,26 +31,25 @@ export default class ABViewDataSelectComponent extends ABViewComponent { return _ui; } - async init(AB) { - await super.init(AB); - this.dc = AB.datacollectionByID(this.settings.dataviewID); - } - async onShow() { - if (!this.dc) return; - await this.dc.waitForDataCollectionToInitialize(this.dc); + super.onShow(); + const dc = this.datacollection; + if (!dc) return; + await dc.waitReady(); const labelField = this.AB.definitionByID( this.settings.labelField )?.columnName; - const options = this.dc + const options = dc .getData() - .map((o) => ({ id: o.id, value: o[labelField] })); - $$(this.ids.select).define("options", options); - $$(this.ids.select).refresh(); - $$(this.ids.select).setValue(this.dc.getCursor().id); + .map((o) => ({ id: o.id, value: o[labelField] })) + .sort((a, b) => (a.value > b.value ? 1 : -1)); + const $select = $$(this.ids.select); + $select.define("options", options); + $select.refresh(); + $select.setValue(dc.getCursor().id); } cursorChange(n) { - this.dc.setCursor(n); + this.datacollection.setCursor(n); } } diff --git a/AppBuilder/platform/views/viewComponent/ABViewGridComponent.js b/AppBuilder/platform/views/viewComponent/ABViewGridComponent.js index ffc89ce9..9f21aab9 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewGridComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewGridComponent.js @@ -267,6 +267,12 @@ export default class ABViewGridComponent extends ABViewComponent { self.toggleUpdateDelete(); } else { if (settings.isEditable) { + // get the field related to this col + const currObject = self.datacollection.datasource; + const selectField = currObject.fields( + (f) => f.columnName === col + )[0]; + // if the colum is not the select item column move on to // the next step to save const state = { @@ -275,7 +281,7 @@ export default class ABViewGridComponent extends ABViewComponent { const editor = { row: row, column: col, - config: null, + config: { fieldID: selectField?.id ?? null }, }; self.onAfterEditStop(state, editor); @@ -1350,6 +1356,8 @@ export default class ABViewGridComponent extends ABViewComponent { return false; } + const CurrentObject = this.datacollection.datasource; + if (editor.config) switch (editor.config.editor) { case "number": @@ -1369,9 +1377,23 @@ export default class ABViewGridComponent extends ABViewComponent { // code block } - if (state.value !== state.old) { + // lets make sure we are comparing things properly: + // reduce newValue and oldValue down to PK if they were objects + let newVal = state.value; + if (newVal) { + newVal = newVal[CurrentObject.PK()] || newVal; + } + let oldVal = state.old; + if (oldVal) { + oldVal = oldVal[CurrentObject.PK()] || oldVal; + } + + // NOTE: != vs !== : + // want to handle when newVal = "3" and oldVal = 3 + // that is why we don't use !== so that we convert the values into + // the same case. + if (newVal != oldVal) { const item = $DataTable?.getItem(editor.row); - const CurrentObject = this.datacollection.datasource; item[editor.column] = state.value; @@ -1379,9 +1401,9 @@ export default class ABViewGridComponent extends ABViewComponent { $DataTable.removeCellCss(item.id, editor.column, "webix_invalid_cell"); //maxlength field - const f = CurrentObject.fieldByID(editor.config.fieldID); + const f = CurrentObject.fieldByID(editor.config?.fieldID); if ( - f.settings.maxLength && + f?.settings.maxLength && state.value.length > f.settings.maxLength ) { this.AB.alert({ diff --git a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js new file mode 100644 index 00000000..3627354c --- /dev/null +++ b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js @@ -0,0 +1,3291 @@ +const ABViewComponent = require("./ABViewComponent").default; +const DC_OFFSET = 20; +const RECORD_LIMIT = 20; +const TEAM_CHART_MAX_DEPTH = 10; // prevent inifinite loop +module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { + constructor(baseView, idBase, ids) { + super( + baseView, + idBase || `ABViewOrgChart_${baseView.id}`, + Object.assign( + { + chartView: "", + // chartDom: "", + chartContent: "", + chartHeader: "", + dataPanel: "", + dataPanelButton: "", + dataPanelPopup: "", + filterButton: "", + filterPopup: "", + filterForm: "", + contentForm: "", + contentFormData: "", + teamForm: "", + teamFormCode: "", + teamFormPopup: "", + teamFormStrategy: "", + teamFormSubmit: "", + teamFormTitle: "", + }, + ids + ) + ); + this._resources = [ + import( + /* webpackPrefetch: true */ + "../../../../js/orgchart-webcomponents.js" + ), + import( + /* webpackPrefetch: true */ + "../../../../styles/orgchart-webcomponents.css" + ), + import( + /* webpackPrefetch: true */ + "../../../../styles/team-widget.css" + ), + ]; + this.__filters = { + inactive: 0, + }; + this._OrgChart = null; + this._resolveInit = null; + this._promiseInit = new Promise((resolve) => { + this._resolveInit = resolve; + }); + this._promisePageData = null; + this._contentDC = null; + this._contentGroupDC = null; + this._contentDisplayDCs = []; + this._dataPanelDCs = []; + this._entityDC = null; + this._chartData = null; + + // DRAG EVENTS + this._fnContentDragEnd = (event) => { + // event.target.style.opacity = "1"; + }; + this._fnContentDragOver = (event) => { + event.preventDefault(); + event.stopPropagation(); + }; + this._fnContentDragStart = (event) => { + event.stopPropagation(); + const $eventTarget = event.target; + const dataset = $eventTarget.dataset; + const dataTransfer = event.dataTransfer; + const data = {}; + switch ($eventTarget.className) { + case "webix_list_item": + data.pk = dataset.pk; + data.contentLinkedFieldID = dataset.contentLinkedFieldId; + break; + default: + data.source = dataset.source; + break; + } + dataTransfer.setData("text/plain", JSON.stringify(data)); + // $eventTarget.style.opacity = "0.5"; + }; + this._fnContentDrop = async (event) => { + const settings = this.view.settings; + const dropContentToCreate = settings.dropContentToCreate === 1; + const nodeObj = this.view.datacollection?.datasource; + const nodeObjPK = nodeObj.PK(); + const contentFieldLink = nodeObj.fieldByID( + settings.contentField + )?.fieldLink; + const contentObj = contentFieldLink?.object; + const contentDateStartFieldColumnName = contentObj?.fieldByID( + settings.contentFieldDateStart + )?.columnName; + const contentDateEndFieldColumnName = contentObj?.fieldByID( + settings.contentFieldDateEnd + )?.columnName; + const contentGroupByField = contentObj?.fieldByID( + settings.contentGroupByField + ); + const contentGroupByFieldColumnName = contentGroupByField?.columnName; + const contentFieldLinkColumnName = contentFieldLink?.columnName; + const contentModel = contentObj?.model(); + + const dataTransfer = event.dataTransfer; + if (dataTransfer.getData("isnode") == 1) return; + event.stopPropagation(); + if (contentFieldLinkColumnName == null) return; + this.busy(); + const $group = event.currentTarget; + const $content = $group.parentElement; + const newGroupDataPK = $group.dataset.pk; + const newNodeDataPK = JSON.parse($content.parentElement.dataset.source) + ._rawData[nodeObjPK]; + let { + source: updatedData, + pk: dataPK, + contentLinkedFieldID, + } = JSON.parse(dataTransfer.getData("text/plain")); + const draggedNodes = []; + let isRefreshed = true; + try { + if (!updatedData) { + // This is a drop from Employee list (new assignment) + const contentLinkedFieldColumnName = + contentObj.fieldByID(contentLinkedFieldID).columnName; + const pendingPromises = []; + const newDate = new Date(); + + // Employee can have multiple assignments but not the same team, so don't close + // existing + const $contentRecords = + $content.getElementsByClassName("team-group-record"); + let isUpdated = false; + for (const $contentRecord of $contentRecords) { + const contentData = JSON.parse($contentRecord.dataset.source); + if (contentData[contentLinkedFieldColumnName] == dataPK) { + this._setUpdatedBy(contentObj, contentData); + if (!isUpdated) { + if ( + contentData[contentGroupByFieldColumnName] == + newGroupDataPK + ) { + isRefreshed = false; + isUpdated = true; + continue; + } else if ( + this._isLessThanDay( + new Date( + contentData[contentDateStartFieldColumnName] + ) + ) + ) { + contentData[contentGroupByFieldColumnName] = + this._parseDataPK(newGroupDataPK); + pendingPromises.push( + contentModel.update( + contentData.id, + this._parseFormValueByType( + contentObj, + contentData, + contentData + ) + ) + ); + draggedNodes.push($contentRecord); + isRefreshed = true; + isUpdated = true; + continue; + } + } + contentData[contentDateEndFieldColumnName] = newDate; + pendingPromises.push( + contentModel.update( + contentData.id, + this._parseFormValueByType( + contentObj, + contentData, + contentData + ) + ) + ); + draggedNodes.push($contentRecord); + isRefreshed = true; + } + } + if (!isUpdated) { + updatedData = {}; + updatedData[contentDateStartFieldColumnName] = newDate; + updatedData[contentLinkedFieldColumnName] = + this._parseDataPK(dataPK); + updatedData[contentFieldLinkColumnName] = + this._parseDataPK(newNodeDataPK); + updatedData[contentGroupByFieldColumnName] = + this._parseDataPK(newGroupDataPK); + const entityDC = this._entityDC; + if (entityDC) { + const entityLink = entityDC.datasource.connectFields( + (f) => f.settings.linkObject === contentObj.id + )[0].id; + const entityCol = + this.AB.definitionByID(entityLink).columnName; + updatedData[entityCol] = this._parseDataPK( + entityDC.getCursor() + ); + } + this._setUpdatedBy(contentObj, updatedData); + pendingPromises.push( + contentModel.create( + this._parseFormValueByType( + contentObj, + updatedData, + updatedData + ) + ), + (async () => { + const $draggedNode = await this._createUIContentRecord( + updatedData, + "grey" + ); + $group + .querySelector(".team-group-content") + .appendChild($draggedNode); + draggedNodes.push($draggedNode); + })() + ); + } + await Promise.all(pendingPromises); + } else { + updatedData = JSON.parse(updatedData); + const $contentRecords = + $content.getElementsByClassName("team-group-record"); + const dataPanelDCs = this._dataPanelDCs; + for (const dataPanelDC of dataPanelDCs) { + const dataPanelLinkedFieldColumnName = + dataPanelDC.datasource.connectFields( + (connectField) => + connectField.datasourceLink === contentObj + )[0].columnName; + for (const $contentRecord of $contentRecords) { + const contentRecord = JSON.parse( + $contentRecord.dataset.source + ); + if ( + (updatedData.id != contentRecord.id && + contentRecord[dataPanelLinkedFieldColumnName] == + updatedData[dataPanelLinkedFieldColumnName]) || + (updatedData.id == contentRecord.id && + updatedData[contentGroupByFieldColumnName] == + $group.dataset.pk) + ) { + this.ready(); + return; + } + } + } + + // This is move form another team node + // Move the child node to the target + const $draggedNode = document.querySelector( + `#${this.contentNodeID(updatedData.id)}` + ); + $draggedNode.parentNode.removeChild($draggedNode); + $group + .querySelector(".team-group-content") + .appendChild($draggedNode); + draggedNodes.push($draggedNode); + delete updatedData["created_at"]; + delete updatedData["updated_at"]; + delete updatedData["properties"]; + this._setUpdatedBy(contentObj, updatedData); + if ( + !dropContentToCreate || + (updatedData[contentFieldLinkColumnName] == newNodeDataPK && + this._isLessThanDay( + new Date(updatedData[contentDateStartFieldColumnName]) + )) + ) { + updatedData[contentFieldLinkColumnName] = newNodeDataPK; + updatedData[contentGroupByFieldColumnName] = newGroupDataPK; + await contentModel.update( + updatedData.id, + this._parseFormValueByType( + contentObj, + updatedData, + updatedData + ) + ); + } else { + const pendingPromises = []; + + // TODO (Guy): Force update Date End with a current date. + updatedData[contentDateEndFieldColumnName] = new Date(); + pendingPromises.push( + contentModel.update( + updatedData.id, + this._parseFormValueByType( + contentObj, + updatedData, + updatedData + ) + ) + ); + updatedData[contentDateStartFieldColumnName] = + updatedData[contentDateEndFieldColumnName]; + delete updatedData["id"]; + delete updatedData["uuid"]; + delete updatedData[contentDateEndFieldColumnName]; + updatedData[contentFieldLinkColumnName] = newNodeDataPK; + updatedData[contentGroupByFieldColumnName] = newGroupDataPK; + pendingPromises.push( + contentModel.create( + this._parseFormValueByType( + contentObj, + updatedData, + updatedData + ) + ) + ); + await Promise.all(pendingPromises); + } + } + } catch (err) { + // TODO (Guy): The update data error. + console.log(err); + } + if (!isRefreshed) { + this.ready(); + return; + } + try { + // TODO (Guy): Logic to not reload dcs. + await Promise.all([ + this._reloadDCData(this.datacollection), + this._reloadDCData(this._contentDC), + ]); + // await this._reloadAllDC(); + } catch (err) { + // TODO (Guy): The reload DCs error. + console.error(err); + } + await this.refresh(); + draggedNodes.forEach(($draggedNode) => { + $draggedNode.remove(); + }); + this.ready(); + }; + this._fnCreateNode = async ($node, data) => { + // remove built in icon + $node.querySelector(".title > i")?.remove(); + + // customize + const $content = $node.children.item(1); + $content.innerHTML = ""; + const contentGroupDC = this._contentGroupDC; + const groupObjPKColumeName = contentGroupDC.datasource.PK(); + await this._waitDCReady(contentGroupDC); + const contentGroupOptions = contentGroupDC.getData(); + const contentGroupOptionsLength = contentGroupOptions.length; + if (data.filteredOut || contentGroupOptionsLength === 0) { + // This node doesn't pass the filter, but it's children do so + // simplify the display. + $content.style.display = "none"; + return; + } + const settings = this.settings; + const $nodeSpacer = element("div", "spacer"); + $content.appendChild($nodeSpacer); + const nodeSpacerStyle = $nodeSpacer.style; + nodeSpacerStyle.backgroundColor = ""; + for (const group of contentGroupOptions) { + const $group = element("div", "team-group-section"); + $content.appendChild($group); + const groupStyle = $group.style; + groupStyle["minHeight"] = `${325 / contentGroupOptionsLength}px`; + + // TODO: should this be a config option + const groupColor = group.name === "Leader" ? "#003366" : "#DDDDDD"; + groupStyle["backgroundColor"] = groupColor; + nodeSpacerStyle.backgroundColor === "" && + (nodeSpacerStyle.backgroundColor = groupColor); + + // TODO: should this be a config option + const groupText = group.name; + $group.setAttribute("data-pk", group[groupObjPKColumeName]); + if (settings.showGroupTitle === 1) { + const $groupTitle = element("div", "team-group-title"); + const groupTitleStyle = $groupTitle.style; + groupTitleStyle["backgroundColor"] = groupColor; + $groupTitle.appendChild(document.createTextNode(groupText)); + $group.appendChild($groupTitle); + } + const $groupContent = element("div", "team-group-content"); + $group.appendChild($groupContent); + if (settings.draggable === 1) { + $group.addEventListener("dragover", this._fnContentDragOver); + $group.addEventListener("drop", this._fnContentDrop); + } + } + const $buttons = element("div", "team-button-section"); + $content.appendChild($buttons); + const $editButton = element("div", "team-button"); + $editButton.append(element("i", "fa fa-pencil")); + const $addButton = element("div", "team-button"); + $addButton.append(element("i", "fa fa-plus")); + $buttons.append($editButton, $addButton); + const dataID = this.teamRecordID(data.id); + const values = this.datacollection.getData((e) => e.id == dataID)[0]; + $addButton.onclick = () => { + this.teamForm("Add", { __parentID: dataID }); + }; + $editButton.onclick = () => this.teamForm("Edit", values); + $node.querySelector(".title").ondblclick = () => + this.teamForm("Edit", values); + if (this.teamCanDelete(values)) { + const $deleteButton = element("div", "team-button"); + $deleteButton.append(element("i", "fa fa-trash")); + $deleteButton.onclick = () => this.teamDelete(values); + $buttons.append($deleteButton); + } + if (this.__filters.inactive == 1) { + const isInactive = data.isInactive; + const activeClass = isInactive ? "is-inactive" : "is-active"; + const $active = element("div", `team-button ${activeClass}`); + const $span = element("span", "active-text"); + $span.innerHTML = isInactive ? "INACTIVE" : "ACTIVE"; + $active.append($span); + $buttons.append($active); + } + }; + this._fnNodeDrop = async (event) => { + const eventDetail = event.detail; + const dragedRecord = JSON.parse( + eventDetail.draggedNode.dataset.source + )._rawData; + dragedRecord[ + // Parent node definition. + this.AB.definitionByID( + this.getSettingField("teamLink").settings.linkColumn + ).columnName + ] = JSON.parse(eventDetail.dropZone.dataset.source)._rawData.id; + const dc = this.datacollection; + this.busy(); + this._setUpdatedBy(dc.datasource, dragedRecord); + try { + await dc.model.update(dragedRecord.id, dragedRecord); + } catch (err) { + // TODO (Guy): The update data error. + console.error(err); + } + try { + // TODO (Guy): Logic to not reload dcs. + await Promise.all([ + this._reloadDCData(dc), + this._reloadDCData(this._contentDC), + ]); + // await this._reloadAllDC(); + } catch (err) { + // TODO (Guy): The reload DCs error. + console.error(err); + } + await this.refresh(); + this.ready(); + }; + this._fnPageContentCallback = ( + contentRecords, + isContentDone, + contentDC, + resolve + ) => { + const linkedContentFieldColumnName = this.AB.definitionByID( + this.getSettingField("contentField").settings.linkColumn + ).columnName; + for (const contentRecord of contentRecords) { + const $teamNode = document.getElementById( + this.teamNodeID(contentRecord[linkedContentFieldColumnName]) + ); + if ($teamNode == null) continue; + this._addContentRecordToGroup($teamNode, contentRecord); + } + + // TODO (Guy): Hardcode data panel DCs for Employee. + $$(this.ids.dataPanel) + ?.getChildViews()[1] + .getChildViews() + .forEach(($childView) => $childView.callEvent("onViewShow")); + if (isContentDone) resolve(); + else + this._callPagingEvent( + contentDC, + this._fnPageContentCallback, + resolve + ); + }; + this._fnPageContentGroupCallback = async ( + contentGroupRecords, + isContentGroupDone, + contentGroupDC, + resolve + ) => { + const teamDC = this.datacollection; + this._fnPageTeamCallback(teamDC.getData(), true, teamDC, () => {}); + if (isContentGroupDone) resolve(); + else + this._callPagingEvent( + contentGroupDC, + this._fnPageContentGroupCallback, + resolve + ); + }; + this._fnPageContentDisplayCallback = ( + contentDisplayRecords, + isContentDisplayDone, + contentDisplayDC, + resolve + ) => { + const contentDC = this._contentDC; + this._fnPageContentCallback( + contentDC.getData(), + true, + contentDC, + () => {} + ); + if (isContentDisplayDone) resolve(); + else + this._callPagingEvent( + contentDisplayDC, + this._fnPageContentDisplayCallback, + resolve + ); + }; + this._fnPageData = async (dc, callback, resolve) => { + await this._waitDCReady(dc); + let records = dc.getData(); + try { + // TODO (Guy): Figure out later why the employee dc which is not reloaded lost the data. + if (records.length < DC_OFFSET) await dc.loadData(); + records = dc.getData(); + if ( + records.length < DC_OFFSET || + (records.length - DC_OFFSET) % RECORD_LIMIT > 0 + ) + throw null; + try { + await dc.loadData( + RECORD_LIMIT * parseInt(records.length / RECORD_LIMIT), + RECORD_LIMIT + ); + } catch {} + if (dc.getData().length === records.length) throw null; + records = dc.getData(); + if ((records.length - DC_OFFSET) % RECORD_LIMIT > 0) throw null; + callback && (await callback(records, false, dc, resolve)); + } catch { + callback && (await callback(records, true, dc, resolve)); + } + }; + this._fnPageTeamCallback = async ( + teamRecords, + isTeamDone, + teamDC, + resolve + ) => { + const contentFieldColumnName = + this.getSettingField("contentField").columnName; + const contentDC = this._contentDC; + const contentRecordPK = contentDC.datasource.PK(); + for (const teamRecord of teamRecords) { + const teamNodeID = this.teamNodeID(teamRecord.id); + let $teamNode = document.getElementById(teamNodeID); + if ($teamNode == null) { + await this.teamAddChild(teamRecord, false); + $teamNode = document.getElementById(teamNodeID); + if ($teamNode == null) continue; + } else await this.teamEdit(teamRecord, false); + const contentRecords = contentDC.getData( + (contentRecord) => + teamRecord[contentFieldColumnName].indexOf( + contentRecord[contentRecordPK] + ) > -1 + ); + for (const contentRecord of contentRecords) + this._addContentRecordToGroup($teamNode, contentRecord); + } + if (isTeamDone) { + await this.pullData(); + resolve(); + } else + this._callPagingEvent( + this.datacollection, + this._fnPageTeamCallback, + resolve + ); + }; + this._fnRefresh = async () => { + this.busy(); + const entityDC = this._entityDC; + const teamDC = this.datacollection; + const contentDC = this._contentDC; + const contentGroupDC = this._contentGroupDC; + await Promise.all([ + teamDC.datacollectionLink != null && this._waitDCPending(teamDC), + contentDC.datacollectionLink != null && + this._waitDCPending(contentDC), + contentGroupDC.datacollectionLink != null && + this._waitDCPending(contentGroupDC), + ...this._contentDisplayDCs + .filter( + (contentDisplayDC) => + contentDisplayDC !== entityDC && + contentDisplayDC !== teamDC && + contentDisplayDC !== contentDC && + contentDisplayDC !== contentGroupDC + ) + .map( + (contentDisplayDC) => + contentDisplayDC.datacollectionLink != null && + this._waitDCPending(contentDisplayDC) + ), + ]); + this.__orgchart?.remove(); + this.__orgchart = null; + await this.refresh(); + this.ready(); + }; + this._fnShowContentForm = (event) => { + const contentDC = this._contentDC; + const contentObj = contentDC.datasource; + const contentModel = contentDC.model; + const settings = this.settings; + const editContentFieldsToCreateNew = + settings.editContentFieldsToCreateNew; + const contentDateStartField = contentObj.fields( + (field) => field.id === settings.contentFieldDateStart + )[0]; + const contentDateStartFieldLabel = contentDateStartField?.label; + const contentDateStartFieldColumnName = + contentDateStartField?.columnName; + const contentDateEndFieldColumnName = this.getSettingField( + "contentFieldDateEnd" + )?.columnName; + const contentDataRecord = JSON.parse( + event.currentTarget.dataset.source || + event.currentTarget.parentElement.dataset.source + ); + const rules = {}; + const labelWidth = 200; + const ids = this.ids; + const contentFormElements = settings.setEditableContentFields.map( + (fieldID) => { + const field = contentObj.fields( + (field) => field.id === fieldID + )[0]; + if (field == null) + return { + view: "label", + label: this.label("Missing Field"), + labelWidth, + }; + const fieldKey = field.key; + const fieldName = field.columnName; + + // TODO (Guy): Add validators. + let invalidMessage = ""; + switch (fieldName) { + case contentDateEndFieldColumnName: + invalidMessage = `The ${field.label} must be later than the ${contentDateStartFieldLabel}.`; + rules[fieldName] = (value) => + value > + $$(ids.contentFormData).getValues()[ + contentDateStartFieldColumnName + ] || + value === "" || + value == null; + break; + default: + rules[fieldName] = () => true; + break; + } + const fieldLabel = field.label; + const settings = field.settings; + switch (fieldKey) { + case "boolean": + return { + view: "checkbox", + name: fieldName, + label: fieldLabel, + labelWidth, + invalidMessage, + }; + case "number": + return { + view: "counter", + name: fieldName, + label: fieldLabel, + labelWidth, + type: "number", + invalidMessage, + }; + case "list": + return { + view: + (settings.isMultiple === 1 && "muticombo") || + "combo", + name: fieldName, + label: fieldLabel, + labelWidth, + options: settings.options.map((option) => ({ + id: option.id, + value: option.text, + })), + invalidMessage, + }; + case "user": + case "connectObject": + const abWebix = this.AB.Webix; + const fieldLinkObj = field.datasourceLink; + + // TODO (Guy): Hardcode for the employee field + if (fieldLabel === "NS Employee Record") + return { + view: "text", + label: "Name", + disabled: true, + labelWidth, + invalidMessage, + on: { + async onViewShow() { + abWebix.extend(this, abWebix.ProgressBar); + this.showProgress({ type: "icon" }); + try { + this.setValue( + fieldLinkObj.displayData( + ( + await fieldLinkObj + .model() + .findAll({ + where: { + glue: "and", + rules: [ + { + key: fieldLinkObj.PK(), + rule: "equals", + value: contentDataRecord[ + fieldName + ], + }, + ], + }, + }) + ).data[0] + ) + ); + this.hideProgress(); + } catch { + // Close popup before response or possily response fail + } + }, + }, + }; + const onViewShow = async function () { + abWebix.extend(this, abWebix.ProgressBar); + this.showProgress({ type: "icon" }); + try { + this.define( + "options", + (await fieldLinkObj.model().findAll()).data.map( + (e) => ({ + id: e.id, + value: fieldLinkObj.displayData(e), + }) + ) + ); + this.refresh(); + this.enable(); + this.hideProgress(); + } catch { + // Close popup before response or possily response fail + } + }; + return field.linkType() === "one" + ? { + view: "combo", + name: fieldName, + label: fieldLabel, + disabled: true, + labelWidth, + invalidMessage, + options: [], + on: { + onViewShow, + }, + } + : { + view: "multicombo", + name: fieldName, + label: fieldLabel, + labelWidth, + stringResult: false, + labelAlign: "left", + invalidMessage, + options: [], + on: { + onViewShow, + }, + }; + case "date": + case "datetime": + return { + view: "datepicker", + name: fieldName, + label: fieldLabel, + stringResult: true, + labelWidth, + invalidMessage, + timepicker: fieldKey === "datetime", + }; + case "file": + case "image": + // TODO (Guy): Add logic + return { + // view: "", + name: fieldName, + label: fieldLabel, + labelWidth, + invalidMessage, + }; + // case "json": + // case "LongText": + // case "string": + // case "email": + default: + return { + view: "text", + name: fieldName, + label: fieldLabel, + labelWidth, + invalidMessage, + }; + } + } + ); + const Webix = AB.Webix; + contentFormElements.push({ + view: "button", + value: this.label("Save"), + css: "webix_primary", + click: async () => { + const $contentFormData = $$(ids.contentFormData); + if (!$contentFormData.validate()) return; + const newFormData = this._parseFormValueByType( + contentObj, + contentDataRecord, + $contentFormData.getValues() + ); + const $contentForm = $$(ids.contentForm); + $contentForm.blockEvent(); + $contentForm.$view.remove(); + $contentForm.destructor(); + if (!this._checkDataIsChanged(contentDataRecord, newFormData)) + return; + const teamDC = this.datacollection; + const contentDC = this._contentDC; + const dataID = newFormData.id; + const $contentNode = document.getElementById( + this.contentNodeID(dataID) + ); + delete newFormData["created_at"]; + delete newFormData["updated_at"]; + delete newFormData["properties"]; + this._setUpdatedBy(contentObj, newFormData); + if ( + !this._isLessThanDay( + new Date( + contentDataRecord[contentDateStartFieldColumnName] + ) + ) + ) { + let isCreated = false; + for (const editContentFieldToCreateNew of editContentFieldsToCreateNew) { + const editContentFieldToCreateNewColumnName = + contentObj.fieldByID( + editContentFieldToCreateNew + )?.columnName; + if ( + !isCreated && + contentDataRecord[ + editContentFieldToCreateNewColumnName + ] != null && + contentDataRecord[ + editContentFieldToCreateNewColumnName + ] !== "" && + JSON.stringify( + newFormData[editContentFieldToCreateNewColumnName] ?? + "" + ) !== + JSON.stringify( + contentDataRecord[ + editContentFieldToCreateNewColumnName + ] + ) + ) { + isCreated = true; + break; + } + } + if (isCreated) { + Webix.confirm({ + title: this.label("Caution: Creating New Assignment"), + ok: this.label("Continue with new assignment"), + cancel: this.label("Cancel"), + text: this.label( + "When you change the Role type or Job title, then the current assignment is closed with the current date and a new assignment is created for this team." + ), + css: "orgchart-teams-edit-content-confirm-popup", + }).then(async () => { + this.busy(); + const pendingPromises = []; + const oldData = {}; + + oldData[contentDateEndFieldColumnName] = new Date(); + this._setUpdatedBy(contentObj, oldData); + pendingPromises.push( + contentModel.update(dataID, oldData) + ); + newFormData[contentDateStartFieldColumnName] = + oldData[contentDateEndFieldColumnName]; + delete newFormData["id"]; + delete newFormData["uuid"]; + delete newFormData[contentDateEndFieldColumnName]; + pendingPromises.push(contentModel.create(newFormData)); + try { + await Promise.all(pendingPromises); + } catch (err) { + // TODO (Guy): The update data error. + console.error(err); + } + try { + // TODO (Guy): Logic to not reload dcs. + await Promise.all([ + this._reloadDCData(teamDC), + this._reloadDCData(contentDC), + ]); + // await this._reloadAllDC(); + } catch (err) { + // TODO (Guy): The reload DCs error. + console.error(err); + } + await this.refresh(); + $contentNode.remove(); + this.ready(); + }); + return; + } + } + if ( + new Date(newFormData[contentDateEndFieldColumnName]) <= + new Date() + ) { + Webix.confirm({ + title: this.label("Caution: Ending Current Assignment"), + ok: this.label("Continue with ending this assignment"), + cancel: this.label("Cancel"), + text: [ + this.label( + "When you provide an End Date, the current assignment is ended when the date = the current date and the assignment will no longer show on this team." + ), + this.label( + "This will put the team member back into the unassigned list box if they have no other active assignments." + ), + ].join("\n"), + css: "orgchart-teams-edit-content-confirm-popup", + }).then(async () => { + this.busy(); + try { + await contentModel.update(dataID, newFormData); + } catch (err) { + // TODO (Guy): The update data error. + console.error(err); + } + try { + // TODO (Guy): Logic to not reload dcs. + await Promise.all([ + this._reloadDCData(teamDC), + this._reloadDCData(contentDC), + ]); + // await this._reloadAllDC(); + } catch (err) { + // TODO (Guy): The reload DCs error. + console.error(err); + } + await this.refresh(); + $contentNode.remove(); + this.ready(); + }); + return; + } + this.busy(); + try { + await contentModel.update(dataID, newFormData); + } catch (err) { + // TODO (Guy): The update data error. + console.error(err); + } + try { + // TODO (Guy): Logic to not reload dcs. + await Promise.all([ + this._reloadDCData(teamDC), + this._reloadDCData(contentDC), + ]); + // await this._reloadAllDC(); + } catch (err) { + // TODO (Guy): The reload DCs error. + console.error(err); + } + await this.refresh(); + $contentNode.remove(); + this.ready(); + }, + }); + Webix.ui({ + view: "popup", + id: ids.contentForm, + close: true, + position: "center", + css: { "border-radius": "10px" }, + body: { + width: 600, + rows: [ + { + view: "toolbar", + css: "webix_dark", + cols: [ + { width: 5 }, + { + view: "label", + label: `${this.label("Edit")} ${contentObj.label}`, + align: "left", + }, + { + view: "icon", + icon: "fa fa-times", + align: "right", + width: 60, + click: () => { + const $contentForm = $$(ids.contentForm); + $contentForm.blockEvent(); + $contentForm.$view.remove(); + $contentForm.destructor(); + }, + }, + ], + }, + { + view: "form", + id: ids.contentFormData, + hidden: true, + elements: contentFormElements, + rules, + }, + ], + }, + on: { + onHide() { + this.$view.remove(); + this.destructor(); + }, + }, + }).show(); + const $contentFormData = $$(ids.contentFormData); + $contentFormData.setValues( + this._convertToFormValueByType(structuredClone(contentDataRecord)) + ); + $contentFormData.show(); + }; + this._fnShowFilterPopup = async (event) => { + const contentDisplayedFieldFilters = + this.settings.contentDisplayedFieldFilters; + const ids = this.ids; + let $popup = $$(ids.filterPopup); + if (!$popup) { + const self = this; + $popup = webix.ui({ + view: "popup", + css: "filter-popup", + id: ids.filterPopup, + body: { + rows: [ + { + view: "form", + borderless: true, + hidden: true, + id: ids.filterForm, + elements: [ + { + view: "text", + label: this.label("Team Name"), + labelWidth: 100, + name: "teamName", + clear: true, + }, + { + view: "combo", + label: this.label("Strategy"), + labelWidth: 100, + options: [], + name: "strategy", + clear: "replace", + on: { + async onViewShow() { + webix.extend(this, webix.ProgressBar); + this.showProgress({ type: "icon" }); + try { + this.define( + "options", + await self.strategyCodeOptions() + ); + this.refresh(); + this.enable(); + this.hideProgress(); + } catch { + // Close popup before response or possily response fail + } + }, + }, + }, + { + view: "checkbox", + name: "inactive", + labelRight: this.label("Show Inactive Teams"), + labelWidth: 0, + }, + ...(() => { + const contentDisplayedFieldFilterViews = []; + for (const contentDisplayedFieldFilterKey in contentDisplayedFieldFilters) { + const [, objID, fieldID, isActive] = + contentDisplayedFieldFilterKey.split("."); + if (isActive == 1) + switch (fieldID) { + // TODO (Guy): Hardcode for the role type filter. + case "96dc0d8d-7fb4-4bb1-8b80-a262aae41eed": + const obj = this.AB.objectByID(objID); + const model = obj.model(); + const fieldColumnName = + obj.fieldByID(fieldID).columnName; + contentDisplayedFieldFilterViews.push( + { + view: "combo", + label: contentDisplayedFieldFilters[ + contentDisplayedFieldFilterKey + ], + labelWidth: 100, + options: [], + name: contentDisplayedFieldFilterKey, + clear: "replace", + on: { + async onViewShow() { + webix.extend( + this, + webix.ProgressBar + ); + this.showProgress({ + type: "icon", + }); + try { + this.define( + "options", + ( + await model.findAll() + ).data.map((e) => ({ + id: e[ + fieldColumnName + ], + value: e[ + fieldColumnName + ], + })) + ); + this.refresh(); + this.enable(); + this.hideProgress(); + } catch { + // Close popup before response or possily response fail + } + }, + }, + } + ); + break; + default: + contentDisplayedFieldFilterViews.push( + { + view: "text", + label: contentDisplayedFieldFilters[ + contentDisplayedFieldFilterKey + ], + labelWidth: 100, + name: contentDisplayedFieldFilterKey, + clear: true, + } + ); + break; + } + } + return contentDisplayedFieldFilterViews; + })(), + { + cols: [ + {}, + { + view: "icon", + icon: "fa fa-check", + css: "filter-apply", + click: () => this.filterApply(), + }, + ], + }, + ], + }, + ], + }, + on: { + onShow() { + $$(ids.filterForm).show(); + }, + onHide() { + $$(ids.filterForm).hide(); + }, + }, + }); + } + $popup.show($$(ids.filterButton).$view); + }; + + // Generate strategy css + const css = [ + "org-chart .strategy-external .title{background:#989898 !important;}", + ]; + const colors = this.settings.strategyColors; + for (let key in colors) { + css.push( + `org-chart .strategy-${key} .title{background:${colors[key]} !important;}` + ); + } + const style = document.createElement("style"); + style.innerHTML = css.join(""); + document.getElementsByTagName("head")[0].appendChild(style); + this.on("pageData", this._fnPageData); + } + + _addContentRecordToGroup($teamNode, contentRecord) { + const contentGroupDC = this._contentGroupDC; + const contentGroupDataPK = + contentRecord[this.getSettingField("contentGroupByField").columnName]; + const contentGroupPKField = contentGroupDC.datasource.PK(); + if ( + contentGroupDC.getData( + (e) => e[contentGroupPKField] == contentGroupDataPK + )[0] == null + ) + return; + const $groupSection = $teamNode.querySelector( + `.team-group-section[data-pk="${contentGroupDataPK}"] > .team-group-content` + ); + if ($groupSection == null) return; + (async () => { + await this._callAfterRender(async () => { + const contentNodeID = this.contentNodeID(contentRecord.id); + let $contentNode = document.getElementById(contentNodeID); + while ($contentNode != null) { + $contentNode.remove(); + $contentNode = document.getElementById(contentNodeID); + } + $groupSection.appendChild( + await this._createUIContentRecord( + contentRecord, + this.settings.strategyColors[ + $teamNode.classList.item(1).replace("strategy-", "") + ] + ) + ); + }); + })(); + } + + _convertToFormValueByType(contentRecord) { + const contentAllFields = this._contentDC.datasource.fields(); + for (const field of contentAllFields) { + const columnName = field.columnName; + const value = contentRecord[columnName]; + switch (field.key) { + case "boolean": + if (value === true) contentRecord[columnName] = 1; + else if (value === false) contentRecord[columnName] = 0; + else { + const parsedValue = parseInt(value); + contentRecord[columnName] = isNaN(parsedValue) + ? 0 + : parsedValue; + } + break; + case "date": + case "datetime": + contentRecord[columnName] = new Date(value); + break; + default: + break; + } + } + return contentRecord; + } + + async _createUIContentRecord(data, color) { + const $ui = element("div", "team-group-record"); + $ui.setAttribute("id", this.contentNodeID(data.id)); + $ui.setAttribute("data-source", JSON.stringify(data)); + $ui.style.borderColor = color; + $ui.addEventListener("dblclick", this._fnShowContentForm); + if (this.settings.draggable === 1) { + $ui.setAttribute("draggable", "true"); + $ui.addEventListener("dragstart", this._fnContentDragStart); + $ui.addEventListener("dragend", this._fnContentDragEnd); + } + + // TODO (Guy): Now we are hardcoding for each display + const hardcodedDisplays = [ + element("div", "display-block"), + element("div", "display-block"), + element("div", "display-block display-block-right"), + ]; + hardcodedDisplays[1].style.width = "50%"; + const $hardcodedSpecialDisplay = element( + "div", + "team-group-record-display" + ); + let currentDataRecords = []; + let currentField = null; + let currentDisplayIndex = 0; + const contentDC = this._contentDC; + const contentObjID = contentDC.datasource.id; + const contentDisplayedFields = this.settings.contentDisplayedFields; + const contentDisplayedFieldsKeys = Object.keys(contentDisplayedFields); + const contentDisplayDCs = this._contentDisplayDCs; + for (let j = 0; j < contentDisplayedFieldsKeys.length; j++) { + const displayedFieldKey = contentDisplayedFieldsKeys[j]; + const [atDisplay, objID] = displayedFieldKey.split("."); + const displayedObj = AB.objectByID(objID); + const displayedFieldID = contentDisplayedFields[displayedFieldKey]; + const displayedField = displayedObj.fieldByID(displayedFieldID); + const displayDC = contentDisplayDCs.find( + (contentDisplayDC) => contentDisplayDC.datasource.id === objID + ); + switch (objID) { + case contentObjID: + currentDataRecords = [data]; + break; + default: + if (currentField == null) break; + if (currentDataRecords.length > 0) { + const currentFieldColumnName = currentField.columnName; + const currentDataPKs = []; + do { + const currentFieldData = + currentDataRecords.pop()[currentFieldColumnName]; + if (Array.isArray(currentFieldData)) { + if (currentFieldData.length > 0) + currentDataPKs.push(...currentFieldData); + } else if (currentFieldData != null) + currentDataPKs.push(currentFieldData); + } while (currentDataRecords.length > 0); + await this._waitDCReady(displayDC); + currentDataRecords = displayDC.getData((r) => { + return currentDataPKs.some((id) => id == r.id); + }); + } + break; + } + if (contentDisplayedFieldsKeys[j + 1]?.split(".")[0] === atDisplay) { + currentField = displayedField; + continue; + } + const $currentDisplay = element("div", "team-group-record-display"); + const displayedFieldColumnName = displayedField.columnName; + // TODO (Guy): Now we are hardcoding for each display. + // $rowData.appendChild($currentDisplay); + switch (currentDisplayIndex) { + case 0: + hardcodedDisplays[0].appendChild($currentDisplay); + break; + case 1: + let i = 0; + while ( + currentDataRecords.length > 0 && + currentDataRecords[i] != null + ) + if ( + currentDataRecords[i][displayedFieldColumnName] == null || + currentDataRecords[i][displayedFieldColumnName] === "" + ) + currentDataRecords.splice(i, 1); + else i++; + if (currentDataRecords.length === 0) + hardcodedDisplays[2].style.display = "none"; + hardcodedDisplays[2].appendChild($currentDisplay); + break; + case 2: + hardcodedDisplays[1].appendChild($hardcodedSpecialDisplay); + $hardcodedSpecialDisplay.appendChild($currentDisplay); + break; + case 3: + $hardcodedSpecialDisplay.appendChild($currentDisplay); + break; + default: + hardcodedDisplays[1].appendChild($currentDisplay); + break; + } + currentDisplayIndex++; + const contentDisplayedFieldTypePrefix = `${displayedFieldKey}.${displayedFieldID}`; + const contentDisplayedFieldMappingDataObj = + JSON.parse( + this.settings.contentDisplayedFieldMappingData?.[ + contentDisplayedFieldTypePrefix + ] || null + ) || {}; + if ( + this.settings.contentDisplayedFieldTypes[ + `${contentDisplayedFieldTypePrefix}.0` + ] != null + ) + $currentDisplay.style.display = "none"; + switch ( + this.settings.contentDisplayedFieldTypes[ + `${contentDisplayedFieldTypePrefix}.1` + ] + ) { + case "icon": + // TODO (Guy): Add logic. + break; + case "image": + while (currentDataRecords.length > 0) { + const currentDataRecordValue = + currentDataRecords.pop()[displayedFieldColumnName]; + const $img = document.createElement("img"); + $currentDisplay.appendChild($img); + $img.setAttribute( + "src", + contentDisplayedFieldMappingDataObj[ + currentDataRecordValue + ] ?? currentDataRecordValue + ); + } + break; + case "svg": + while (currentDataRecords.length > 0) { + const currentDataRecord = currentDataRecords.pop(); + const currentDataRecordID = currentDataRecord.id; + const currentDataRecordValue = + currentDataRecord[displayedFieldColumnName]; + const SVG_NS = "http://www.w3.org/2000/svg"; + const X_LINK_NS = "http://www.w3.org/1999/xlink"; + const $svg = document.createElementNS(SVG_NS, "svg"); + $currentDisplay.appendChild($svg); + $svg.setAttribute("viewBox", "0 0 6 6"); + $svg.setAttribute("fill", "none"); + $svg.setAttribute("xmlns", SVG_NS); + $svg.setAttribute("xmlns:xlink", X_LINK_NS); + const $rect = document.createElementNS(SVG_NS, "rect"); + const $defs = document.createElementNS(SVG_NS, "defs"); + $svg.append($rect, $defs); + $rect.setAttribute("width", "6"); + $rect.setAttribute("height", "6"); + const patternID = `display-svg.pattern.${currentDataRecordID}`; + $rect.setAttribute("fill", `url(#${patternID})`); + const $pattern = document.createElementNS(SVG_NS, "pattern"); + const $image = document.createElementNS(SVG_NS, "image"); + $defs.append($pattern, $image); + $pattern.id = patternID; + $pattern.setAttributeNS( + null, + "patternContentUnits", + "objectBoundingBox" + ); + $pattern.setAttribute("width", "1"); + $pattern.setAttribute("height", "1"); + const imageID = `display-svg.image.${currentDataRecordID}`; + $image.id = imageID; + $image.setAttribute("width", "512"); + $image.setAttribute("height", "512"); + $image.setAttributeNS( + X_LINK_NS, + "xlink:href", + contentDisplayedFieldMappingDataObj[ + currentDataRecordValue + ] ?? currentDataRecordValue + ); + const $use = document.createElementNS(SVG_NS, "use"); + $pattern.appendChild($use); + $use.setAttributeNS(X_LINK_NS, "xlink:href", `#${imageID}`); + $use.setAttribute("transform", "scale(0.002)"); + } + break; + default: + while (currentDataRecords.length > 0) { + const currentDataRecordValue = + currentDataRecords.pop()[displayedFieldColumnName]; + $currentDisplay.appendChild( + document.createTextNode( + contentDisplayedFieldMappingDataObj[ + currentDataRecordValue + ] ?? currentDataRecordValue + ) + ); + } + + // TODO (Guy): Hardcode limit text. + if (currentDisplayIndex - 1 === 1) + $currentDisplay.textContent = + $currentDisplay.textContent.slice(0, 35); + break; + } + currentField = null; + } + + // TODO (Guy): Now we are hardcoding for each display. + const hardcodedDisplaysLength = hardcodedDisplays.length; + for (let i = 0; i < hardcodedDisplaysLength; i++) { + const $hardcodedDisplay = hardcodedDisplays[i]; + $ui.appendChild($hardcodedDisplay); + const children = $hardcodedDisplay.children; + let isShown = false; + let j = 0; + let child, grandChildren, grandChildrenLength; + switch (i) { + case 1: + child = children.item(j); + grandChildren = child.children; + grandChildrenLength = grandChildren.length; + for (; j < grandChildrenLength; j++) + if (grandChildren[j].style.display !== "none") { + isShown = true; + break; + } + if (isShown) continue; + child.style.display = "none"; + j = 1; + break; + default: + break; + } + const childrenLength = children.length; + const hardcodedDisplayStyle = $hardcodedDisplay.style; + for (; j < childrenLength; j++) + if (children.item(j).style.display !== "none") { + isShown = true; + break; + } + !isShown && (hardcodedDisplayStyle.display = "none"); + } + const $editIcon = element("div", "team-group-record-edit-icon"); + $editIcon.appendChild(element("i", "fa fa-pencil")); + $editIcon.addEventListener("click", this._fnShowContentForm); + $ui.appendChild($editIcon); + return $ui; + } + + async _callAfterRender(callback, ...params) { + await new Promise((resolve, reject) => { + requestAnimationFrame(() => { + requestAnimationFrame(async () => { + try { + await callback(...params); + resolve(); + } catch (err) { + reject(err); + } + }); + }); + }); + } + + _callPagingEvent(dc, callback, resolve) { + this.emit("pageData", dc, callback, resolve); + } + + _checkDataIsChanged(olaValues, newValues) { + // TODO (Guy): Check array in the future. + for (const key in newValues) + if (JSON.stringify(newValues[key]) !== JSON.stringify(olaValues[key])) + return true; + return false; + } + + _isLessThanDay(date) { + return Math.abs(new Date() - date) / 36e5 < 24; + } + + _initDC(dc) { + dc.init(); + if (dc.dataStatus === dc.dataStatusFlag.notInitial) dc.loadData(); + } + + _pageData() { + if (this._promisePageData != null) return; + let resolvePageData = null; + this._promisePageData = new Promise((resolve) => { + resolvePageData = resolve; + }); + const entityDC = this._entityDC; + const teamDC = this.datacollection; + const contentDC = this._contentDC; + const contentGroupDC = this._contentGroupDC; + (async () => { + try { + await Promise.all([ + new Promise((resolve) => { + this._callPagingEvent( + teamDC, + this._fnPageTeamCallback, + resolve + ); + }), + new Promise((resolve) => { + this._callPagingEvent( + contentDC, + this._fnPageContentCallback, + resolve + ); + }), + new Promise((resolve) => { + this._callPagingEvent( + contentGroupDC, + this._fnPageContentGroupCallback, + resolve + ); + }), + ...this._contentDisplayDCs + .filter( + (contentDisplayDC) => + contentDisplayDC !== entityDC && + contentDisplayDC !== teamDC && + contentDisplayDC !== contentDC && + contentDisplayDC !== contentGroupDC + ) + .map( + (contentDisplayDC) => + new Promise((resolve) => { + this._callPagingEvent( + contentDisplayDC, + this._fnPageContentDisplayCallback, + resolve + ); + }) + ), + ]); + } catch (err) { + // TODO (Guy): The paging error. + console.error(err); + } + resolvePageData(); + this._promisePageData = null; + })(); + } + + _parseDataPK(dataPK) { + const intDataPk = parseInt(dataPK); + return ( + ((isNaN(intDataPk) || intDataPk.toString().length !== dataPK.length) && + dataPK) || + intDataPk + ); + } + + _parseFormValueByType(obj, oldFormData, newFormData) { + const allFields = obj.fields(); + for (const field of allFields) { + const fieldKey = field.key; + const columnName = field.columnName; + const oldValue = oldFormData?.[columnName]; + const newValue = newFormData[columnName]; + switch (fieldKey) { + case "date": + if (oldValue === undefined && newValue == null) + delete newFormData[columnName]; + else { + newFormData[columnName] = new Date(newValue); + if (isNaN(newFormData[columnName])) + delete newFormData[columnName]; + else + newFormData[columnName] = `${newFormData[ + columnName + ].getFullYear()}-${String( + newFormData[columnName].getMonth() + 1 + ).padStart(2, "0")}-${String( + newFormData[columnName].getDate() + ).padStart(2, "0")}`; + } + break; + case "datetime": + if (oldValue === undefined && newValue == null) + delete newFormData[columnName]; + try { + newValue instanceof Date && + (newFormData[columnName] = newValue.toISOString()); + } catch { + delete newFormData[columnName]; + } + break; + case "connectObject": + delete newFormData[`${columnName}__relation`]; + if (field.linkType() === "one") { + if (oldValue === undefined && newFormData[columnName] == null) + delete newFormData[columnName]; + else + switch (typeof oldValue) { + case "number": + newFormData[columnName] = parseInt(newValue) || null; + break; + case "string": + newFormData[columnName] = + newValue?.toString() || null; + break; + default: + break; + } + } + // TODO (Guy): Many logic in the future. Now we don't have an array data changed. + else delete newFormData[columnName]; + break; + default: + if (newValue == null || newValue === "") + if (oldValue === undefined) { + delete newFormData[columnName]; + break; + } else if (oldValue === "") { + newFormData[columnName] = ""; + break; + } + switch (fieldKey) { + case "boolean": + switch (typeof oldValue) { + case "number": + newFormData[columnName] = newValue; + break; + case "string": + newFormData[columnName] = newValue === 1 ? "1" : "0"; + break; + default: + newFormData[columnName] = newValue == 1; + break; + } + break; + case "number": + const paredNewValue = parseInt(newValue); + if (isNaN(parseInt(newValue))) { + if (oldValue === undefined) + delete newFormData[columnName]; + else newFormData[columnName] = oldValue; + break; + } + switch (typeof oldValue) { + case "string": + newFormData[columnName] = paredNewValue.toString(); + break; + default: + newFormData[columnName] = paredNewValue; + break; + } + break; + case "string": + newFormData[columnName] = newValue?.toString() || ""; + break; + default: + break; + } + break; + } + } + return newFormData; + } + + async _reloadAllDC() { + const entityDC = this._entityDC; + const teamDC = this.datacollection; + const contentDC = this._contentDC; + const contentGroupDC = this._contentGroupDC; + await Promise.all([ + this._reloadDCData(teamDC), + ...this._dataPanelDCs.map((dataPanelDC) => + this._reloadDCData(dataPanelDC) + ), + this._reloadDCData(contentDC), + ...this._contentDisplayDCs + .filter( + (contentDisplayDC) => + contentDisplayDC !== entityDC && + contentDisplayDC !== teamDC && + contentDisplayDC !== contentDC && + contentDisplayDC !== contentGroupDC + ) + .map((contentDisplayDC) => this._reloadDCData(contentDisplayDC)), + ]); + } + + async _reloadDCData(dc) { + await this._promisePageData; + if (dc.dataStatus === dc.dataStatusFlag.initializing) + await this._waitDCReady(dc); + dc.clearAll(); + this._initDC(dc); + await this._waitDCReady(dc); + } + + _setUpdatedBy(obj, values) { + values[ + // TODO (Guy): This should be the ABDesigner setting. + obj.fields( + (field) => + field.columnName.indexOf("_last_upd_by_in_app") > -1 || + field.columnName.indexOf("_update_in_app") > -1 + )[0].columnName + ] = this.AB.Account.email(); + } + + _showDataPanel() { + let $panel = $$(this.ids.dataPanelPopup); + if (!$panel) { + $panel = this.AB.Webix.ui({ + id: this.ids.dataPanelPopup, + view: "popup", + width: 250, + body: this._uiDataPanel(), + css: "data-panel-popup", + modal: true, + }); + } + const $dpButtonWebix = $$(this.ids.dataPanelButton).$view; + const $dpButtonElem = $dpButtonWebix.querySelector(".data-panel-button"); + // Ensure the popup will stay to the right when resizing + if (!this._resizeObserver) { + this._resizeObserver = new ResizeObserver(([e]) => { + // Hide the panel when the widget is hidden (ex. switched to another App) + if (e.contentRect.width == 0 && e.contentRect.height == 0) { + return $panel.hide(); + } + $panel.show($dpButtonElem, { x: -30, y: -35 }); + }); + } + this._resizeObserver.observe($dpButtonWebix); + $panel.show($dpButtonElem, { x: -30, y: -35 }); + $$(this.ids.dataPanel) + .getChildViews()[1] + .getChildViews() + .forEach(($childView) => $childView.callEvent("onViewShow")); + } + + _showOrgChart() { + const settings = this.settings; + const AB = this.AB; + const draggable = settings.draggable === 1; + const ids = this.ids; + const chartData = this._chartData; + if (chartData == null) { + this.__orgchart = null; + return; + } + const orgchart = new this._OrgChart({ + data: AB.cloneDeep(chartData), + direction: settings.direction, + // depth: settings.depth, + // chartContainer: `#${ids.chartDom}`, + pan: true, // settings.pan == 1, + zoom: false, // settings.zoom == 1, + draggable, + // visibleLevel: settings.visibleLevel, + parentNodeSymbol: false, + exportButton: settings.export, + exportFilename: settings.exportFilename, + createNode: this._fnCreateNode, + nodeContent: "description", + }); + + // On drop update the parent (dropZone) of the node + if (draggable) + orgchart.addEventListener("nodedropped.orgchart", this._fnNodeDrop); + if (this.__orgchart != null) { + const oldOrgchart = this.__orgchart; + orgchart.dataset.panStart = oldOrgchart.dataset.panStart; + orgchart.setAttribute("style", oldOrgchart.getAttribute("style")); + oldOrgchart.remove(); + } + $$(ids.chartContent).$view.appendChild((this.__orgchart = orgchart)); + } + + _uiDataPanel() { + const self = this; + const _dataPanelDCs = self._dataPanelDCs; + const dataPanelDCs = self.settings.dataPanelDCs; + const contentObjID = this._contentDC?.datasource?.id; + const cells = []; + for (const key in dataPanelDCs) { + const [tabIndex, dataPanelDCID] = key.split("."); + + // TODO (Guy): Hardcode data panel DCs for Employee. + // const _dataPanelDC = _dataPanelDCs.find( + // (dataPanelDC) => dataPanelDC.id === dataPanelDCID + // ); + const _dataPanelDC = self._contentDisplayDCs.find( + (contentDisplayDC) => + contentDisplayDC.datasource.id === + _dataPanelDCs.find( + (dataPanelDC) => dataPanelDC.id === dataPanelDCID + ).datasource.id + ); + const contentDC = this._contentDC; + const header = dataPanelDCs[key]; + if (_dataPanelDC == null) + cells.push({ + header, + body: { + view: "list", + css: { overflow: "auto", "max-height": "90%" }, + data: [], + }, + }); + else { + const panelObj = _dataPanelDC.datasource; + cells.push({ + header, + body: { + view: "list", + template: (data) => + `