From 5d68620ecdca50fcf1a257838a6caa757214cd89 Mon Sep 17 00:00:00 2001 From: nh758 <7259@pm.me> Date: Mon, 9 Sep 2024 13:05:09 +0700 Subject: [PATCH 001/129] add complex team widget and display multiple levels --- .../platform/views/ABViewOrgChartTeams.js | 13 + .../ABViewOrgChartTeamsComponent.js | 244 ++++++++++++++++++ 2 files changed, 257 insertions(+) create mode 100644 AppBuilder/platform/views/ABViewOrgChartTeams.js create mode 100644 AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js 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/ABViewOrgChartTeamsComponent.js b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js new file mode 100644 index 00000000..61ee13da --- /dev/null +++ b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js @@ -0,0 +1,244 @@ +const ABViewComponent = require("./ABViewComponent").default; + +module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { + constructor(baseView, idBase, ids) { + super( + baseView, + idBase || `ABViewOrgChart_${baseView.id}`, + Object.assign( + { + chartView: "", + chartDom: "", + }, + ids + ) + ); + } + + ui() { + const ids = this.ids; + const _ui = super.ui([ + { + view: "template", + template: `
`, + css: { + position: "relative", + }, + }, + ]); + + delete _ui.type; + + return _ui; + } + + async init(AB, accessLevel) { + await super.init(AB, accessLevel); + + const $chartView = $$(this.ids.chartView); + if ($chartView) + this.AB.Webix.extend($chartView, this.AB.Webix.ProgressBar); + } + + async loadOrgChartJs() { + this.busy(); + + const [orgChartLoader] = await Promise.all([ + import( + /* webpackPrefetch: true */ + "../../../../js/orgchart-webcomponents.js" + ), + import( + /* webpackPrefetch: true */ + "../../../../styles/orgchart-webcomponents.css" + ), + ]); + + this.OrgChart = orgChartLoader.default; + + this.ready(); + } + + async onShow() { + super.onShow(); + + this.busy(); + await this.loadOrgChartJs(); + await this.pullData(); + this.displayOrgChart(); + this.ready(); + } + + async displayOrgChart() { + const baseView = this.view; + const chartData = this.AB.cloneDeep(this.chartData); + + const orgchart = new this.OrgChart({ + data: chartData, + direction: baseView.settings.direction, + // depth: baseView.settings.depth, + pan: baseView.settings.pan, + zoom: baseView.settings.zoom, + // visibleLevel: baseView.settings.visibleLevel, + + exportButton: baseView.settings.export, + exportFilename: baseView.settings.exportFilename, + + // ajaxURLs: { + // children: function (nodeData) { + // console.info("nodeData: ", nodeData); + // return null; + // }, + // }, + nodeContent: "description", + }); + + const chartDom = document.querySelector(`#${this.ids.chartDom}`); + if (chartDom) { + chartDom.textContent = ""; + chartDom.innerHTML = ""; + chartDom.appendChild(orgchart); + } + + setTimeout(() => { + this._setColor(); + }, 1); + } + + async pullData() { + const view = this.view; + const dc = view.datacollection; + + const cursor = dc?.getCursor(); + if (!cursor) return null; + + // TODO refactor props to only support one field (self linking) + const valueField = view.valueFields()[0].columnName; + // const descriptionField = view.descriptionField?.(); + + const chartData = this.chartData; + (chartData.name = cursor.Name), (chartData.description = "..."); + // description: + // descriptionField?.format?.(f) ?? + // f[descriptionField?.columnName] ?? + // "", + chartData._rawData = cursor; + + function pullChildData(node) { + node.children = []; + node._rawData[valueField].forEach((id) => { + const childData = dc.getData((e) => e.id === id)[0]; + const child = { + name: childData.Name, + description: "...", + _rawData: childData, + }; + if (childData[valueField].length > 0) { + pullChildData(child); + } + node.children.push(child); + }); + return; + } + + pullChildData(chartData); + // + // let parentChartData = [chartData]; + // let currChildren; + // + // valueFields.forEach((field) => { + // let _tempParentChartData = []; + // + // parentChartData.forEach(async (chartItem) => { + // if (!chartItem) return; + // + // const rawData = chartItem?._rawData; + // currChildren = rawData?.[field?.relationName()]; + // + // // Pull data from the server + // if (!currChildren) { + // const objLink = field.object; + // const where = { + // glue: "and", + // rules: [], + // }; + // where.rules.push({ + // key: objLink.PK(), + // rule: "equals", + // value: rawData[objLink.PK()], + // }); + // const returnData = await objLink + // .model() + // .findAll({ where, populate: true }); + // chartItem._rawData = returnData?.data[0]; + // currChildren = chartItem._rawData?.[field?.relationName()]; + // + // this.displayOrgChart(); + // } + // + // chartItem.children = []; + // if (currChildren?.length) { + // currChildren.forEach((childData) => { + // chartItem.children.push({ + // name: field.datasourceLink.displayData(childData), + // description: "", + // _rawData: childData, + // }); + // }); + // } + // + // _tempParentChartData = _tempParentChartData.concat( + // chartItem.children + // ); + // }); + // + // parentChartData = _tempParentChartData; + // }); + } + + get chartData() { + if (this._chartData == null) { + this._chartData = {}; + } + return this._chartData; + } + + _setColor() { + const view = this.view; + let doms = document.querySelectorAll(`org-chart`); + doms.forEach((dom) => { + dom.style.backgroundImage = "none"; + }); + + doms = document.querySelectorAll(` + org-chart .verticalNodes>td::before, + org-chart .verticalNodes ul>li::before, + org-chart .verticalNodes ul>li::after, + org-chart .node .content, + org-chart tr.lines .topLine, + org-chart tr.lines .rightLine, + org-chart tr.lines .leftLine`); + doms.forEach((dom) => { + dom.style.borderColor = view.settings.color; + }); + + doms = document.querySelectorAll(` + org-chart tr.lines .downLine, + org-chart .node .title`); + doms.forEach((dom) => { + dom.style.backgroundColor = view.settings.color; + }); + } + + busy() { + const $chartView = $$(this.ids.chartView); + $chartView?.disable?.(); + $chartView?.showProgress?.({ type: "icon" }); + } + + ready() { + const $chartView = $$(this.ids.chartView); + $chartView?.enable?.(); + $chartView?.hideProgress?.(); + } +}; From ca634dcf476fd6d292874eb5e0fe3dbe1e275611 Mon Sep 17 00:00:00 2001 From: nh758 <7259@pm.me> Date: Tue, 10 Sep 2024 11:14:39 +0700 Subject: [PATCH 002/129] use new settings to display node name and select top --- .../ABViewOrgChartTeamsComponent.js | 87 +++++-------------- 1 file changed, 24 insertions(+), 63 deletions(-) diff --git a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js index 61ee13da..1e1bd353 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js @@ -108,92 +108,53 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { async pullData() { const view = this.view; const dc = view.datacollection; + await dc?.waitForDataCollectionToInitialize(dc); + + let topNode = dc?.getCursor(); + if (view.settings.topTeam) { + const topNodeColumn = this.AB.definitionByID( + view.settings.topTeam + ).columnName; + const topFromFeild = dc.getData((e) => e[topNodeColumn] === 1)[0]; + topNode = topFromFeild ? topFromFeild : topNode; + } + if (!topNode) return null; - const cursor = dc?.getCursor(); - if (!cursor) return null; + const teamLink = this.AB.definitionByID( + view.settings.teamLink + )?.columnName; - // TODO refactor props to only support one field (self linking) - const valueField = view.valueFields()[0].columnName; - // const descriptionField = view.descriptionField?.(); + const teamName = this.AB.definitionByID( + view.settings.teamName + )?.columnName; const chartData = this.chartData; - (chartData.name = cursor.Name), (chartData.description = "..."); + chartData.name = topNode[teamName] ?? ""; + chartData.description = "..."; // description: // descriptionField?.format?.(f) ?? // f[descriptionField?.columnName] ?? // "", - chartData._rawData = cursor; + chartData._rawData = topNode; function pullChildData(node) { + console.log(node.name, node._rawData[teamLink]); node.children = []; - node._rawData[valueField].forEach((id) => { + node._rawData[teamLink].forEach((id) => { const childData = dc.getData((e) => e.id === id)[0]; const child = { - name: childData.Name, + name: childData[teamName], description: "...", _rawData: childData, }; - if (childData[valueField].length > 0) { + if (childData[teamLink].length > 0) { pullChildData(child); } node.children.push(child); }); return; } - pullChildData(chartData); - // - // let parentChartData = [chartData]; - // let currChildren; - // - // valueFields.forEach((field) => { - // let _tempParentChartData = []; - // - // parentChartData.forEach(async (chartItem) => { - // if (!chartItem) return; - // - // const rawData = chartItem?._rawData; - // currChildren = rawData?.[field?.relationName()]; - // - // // Pull data from the server - // if (!currChildren) { - // const objLink = field.object; - // const where = { - // glue: "and", - // rules: [], - // }; - // where.rules.push({ - // key: objLink.PK(), - // rule: "equals", - // value: rawData[objLink.PK()], - // }); - // const returnData = await objLink - // .model() - // .findAll({ where, populate: true }); - // chartItem._rawData = returnData?.data[0]; - // currChildren = chartItem._rawData?.[field?.relationName()]; - // - // this.displayOrgChart(); - // } - // - // chartItem.children = []; - // if (currChildren?.length) { - // currChildren.forEach((childData) => { - // chartItem.children.push({ - // name: field.datasourceLink.displayData(childData), - // description: "", - // _rawData: childData, - // }); - // }); - // } - // - // _tempParentChartData = _tempParentChartData.concat( - // chartItem.children - // ); - // }); - // - // parentChartData = _tempParentChartData; - // }); } get chartData() { From 2b0f4113a8ac7496dde77c6fac02484b352cc53d Mon Sep 17 00:00:00 2001 From: nh758 <7259@pm.me> Date: Tue, 10 Sep 2024 12:47:58 +0700 Subject: [PATCH 003/129] add drag & drop editting --- .../ABViewOrgChartTeamsComponent.js | 44 +++++++++++++++---- 1 file changed, 35 insertions(+), 9 deletions(-) diff --git a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js index 1e1bd353..928120fd 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js @@ -72,13 +72,15 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { async displayOrgChart() { const baseView = this.view; const chartData = this.AB.cloneDeep(this.chartData); + const draggable = baseView.settings.draggable == 1; const orgchart = new this.OrgChart({ data: chartData, direction: baseView.settings.direction, // depth: baseView.settings.depth, - pan: baseView.settings.pan, - zoom: baseView.settings.zoom, + pan: baseView.settings.pan == 1, + zoom: baseView.settings.zoom == 1, + draggable, // visibleLevel: baseView.settings.visibleLevel, exportButton: baseView.settings.export, @@ -93,6 +95,26 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { nodeContent: "description", }); + if (draggable) { + // On drop update the parent (dropZone) of the node + orgchart.addEventListener("nodedropped.orgchart", (event) => { + const dragNode = JSON.parse( + event.detail.draggedNode.dataset.source + ); + const dropNode = JSON.parse(event.detail.dropZone.dataset.source); + const dragRecord = dragNode._rawData; + const dropID = dropNode._rawData.id; + + const linkField = this.getSettingField("teamLink"); + const parent = this.AB.definitionByID( + linkField.settings.linkColumn + ); + dragRecord[parent.columnName] = dropID; + + this.datacollection.model.update(dragRecord.id, dragRecord); + }); + } + const chartDom = document.querySelector(`#${this.ids.chartDom}`); if (chartDom) { chartDom.textContent = ""; @@ -120,13 +142,8 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { } if (!topNode) return null; - const teamLink = this.AB.definitionByID( - view.settings.teamLink - )?.columnName; - - const teamName = this.AB.definitionByID( - view.settings.teamName - )?.columnName; + const teamLink = this.getSettingField("teamLink").columnName; + const teamName = this.getSettingField("teamName").columnName; const chartData = this.chartData; chartData.name = topNode[teamName] ?? ""; @@ -164,6 +181,15 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { return this._chartData; } + /** + * Get the ABField from settings + * @param {string} setting key in this.view.settings - should be an id for an + * ABField + */ + getSettingField(setting) { + return this.AB.definitionByID(this.view.settings[setting]); + } + _setColor() { const view = this.view; let doms = document.querySelectorAll(`org-chart`); From a14c7341c5c863168417f71655465942d6bde761 Mon Sep 17 00:00:00 2001 From: nh758 <7259@pm.me> Date: Wed, 11 Sep 2024 11:53:46 +0700 Subject: [PATCH 004/129] wip styling --- .../ABViewOrgChartTeamsComponent.js | 31 +++--- styles/orgchart-webcomponents.css | 102 +++++++++--------- styles/team-widget.css | 80 ++++++++++++++ 3 files changed, 149 insertions(+), 64 deletions(-) create mode 100644 styles/team-widget.css diff --git a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js index 928120fd..c0fb7b54 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js @@ -52,6 +52,10 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { /* webpackPrefetch: true */ "../../../../styles/orgchart-webcomponents.css" ), + import( + /* webpackPrefetch: true */ + "../../../../styles/team-widget.css" + ), ]); this.OrgChart = orgChartLoader.default; @@ -78,20 +82,25 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { data: chartData, direction: baseView.settings.direction, // depth: baseView.settings.depth, - pan: baseView.settings.pan == 1, - zoom: baseView.settings.zoom == 1, + chartContainer: `#${this.ids.chartDom}`, + pan: true, // baseView.settings.pan == 1, + zoom: true, //baseView.settings.zoom == 1, draggable, // visibleLevel: baseView.settings.visibleLevel, exportButton: baseView.settings.export, exportFilename: baseView.settings.exportFilename, + createNode: ($node /*, data*/) => { + // remove built in icon + $node.querySelector(".title > i")?.remove(); + // customize + const $content = $node.querySelector(".content"); + $content.innerHTML = ""; + const $leaderSection = document.createElement("div"); + $leaderSection.classList.add("team-leader-section"); + $content.append($leaderSection); + }, - // ajaxURLs: { - // children: function (nodeData) { - // console.info("nodeData: ", nodeData); - // return null; - // }, - // }, nodeContent: "description", }); @@ -147,11 +156,6 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { const chartData = this.chartData; chartData.name = topNode[teamName] ?? ""; - chartData.description = "..."; - // description: - // descriptionField?.format?.(f) ?? - // f[descriptionField?.columnName] ?? - // "", chartData._rawData = topNode; function pullChildData(node) { @@ -191,6 +195,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { } _setColor() { + return; const view = this.view; let doms = document.querySelectorAll(`org-chart`); doms.forEach((dom) => { diff --git a/styles/orgchart-webcomponents.css b/styles/orgchart-webcomponents.css index ad263773..e1c4ab88 100644 --- a/styles/orgchart-webcomponents.css +++ b/styles/orgchart-webcomponents.css @@ -32,7 +32,7 @@ org-chart { -moz-user-select: none; -ms-user-select: none; user-select: none; - background-image: linear-gradient(90deg, rgba(200, 0, 0, 0.15) 10%, rgba(0, 0, 0, 0) 10%), linear-gradient(rgba(200, 0, 0, 0.15) 10%, rgba(0, 0, 0, 0) 10%); + /* background-image: linear-gradient(90deg, rgba(200, 0, 0, 0.15) 10%, rgba(0, 0, 0, 0) 10%), linear-gradient(rgba(200, 0, 0, 0.15) 10%, rgba(0, 0, 0, 0) 10%); */ background-size: 10px 10px; border: 1px dashed transparent; padding: 20px; @@ -163,29 +163,29 @@ org-chart td { padding: 0; } -org-chart tr.lines .topLine { - border-top: 2px solid rgba(217, 83, 79, 0.8); -} - -org-chart tr.lines .rightLine { - border-right: 1px solid rgba(217, 83, 79, 0.8); - float: none; - border-radius: 0; -} - -org-chart tr.lines .leftLine { - border-left: 1px solid rgba(217, 83, 79, 0.8); - float: none; - border-radius: 0; -} - -org-chart tr.lines .downLine { - background-color: rgba(217, 83, 79, 0.8); - margin: 0 auto; - height: 20px; - width: 2px; - float: none; -} +/* org-chart tr.lines .topLine { */ +/* border-top: 2px solid rgba(217, 83, 79, 0.8); */ +/* } */ +/**/ +/* org-chart tr.lines .rightLine { */ +/* border-right: 1px solid rgba(217, 83, 79, 0.8); */ +/* float: none; */ +/* border-radius: 0; */ +/* } */ +/**/ +/* org-chart tr.lines .leftLine { */ +/* border-left: 1px solid rgba(217, 83, 79, 0.8); */ +/* float: none; */ +/* border-radius: 0; */ +/* } */ +/**/ +/* org-chart tr.lines .downLine { */ +/* background-color: rgba(217, 83, 79, 0.8); */ +/* margin: 0 auto; */ +/* height: 20px; */ +/* width: 2px; */ +/* float: none; */ +/* } */ /* node styling */ org-chart .node { @@ -242,19 +242,19 @@ org-chart .node.allowedDrop { border-color: rgba(68, 157, 68, 0.9); } -org-chart .node .title { - text-align: center; - font-size: 12px; - font-weight: bold; - height: 20px; - line-height: 20px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - background-color: rgba(217, 83, 79, 0.8); - color: #fff; - border-radius: 4px 4px 0 0; -} +/* org-chart .node .title { */ +/* text-align: center; */ +/* font-size: 12px; */ +/* font-weight: bold; */ +/* height: 20px; */ +/* line-height: 20px; */ +/* overflow: hidden; */ +/* text-overflow: ellipsis; */ +/* white-space: nowrap; */ +/* background-color: rgba(217, 83, 79, 0.8); */ +/* color: #fff; */ +/* border-radius: 4px 4px 0 0; */ +/* } */ org-chart.b2t .node .title { -ms-transform: rotate(-180deg); @@ -297,20 +297,20 @@ org-chart .node .title .symbol { margin-left: 2px; } -org-chart .node .content { - width: 100%; - height: 20px; - font-size: 11px; - line-height: 18px; - border: 1px solid rgba(217, 83, 79, 0.8); - border-radius: 0 0 4px 4px; - text-align: center; - background-color: #fff; - color: #333; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} +/* org-chart .node .content { */ +/* width: 100%; */ +/* height: 20px; */ +/* font-size: 11px; */ +/* line-height: 18px; */ +/* border: 1px solid rgba(217, 83, 79, 0.8); */ +/* border-radius: 0 0 4px 4px; */ +/* text-align: center; */ +/* background-color: #fff; */ +/* color: #333; */ +/* overflow: hidden; */ +/* text-overflow: ellipsis; */ +/* white-space: nowrap; */ +/* } */ org-chart.b2t .node .content { -ms-transform: rotate(180deg); diff --git a/styles/team-widget.css b/styles/team-widget.css new file mode 100644 index 00000000..1fcf374a --- /dev/null +++ b/styles/team-widget.css @@ -0,0 +1,80 @@ +@import url('https://fonts.googleapis.com/css2?family=Jomhuria&display=swap'); + +org-chart .node { + display: inline-block; + position: relative; + margin: 5px; + padding: 5px; + border: 2px dashed transparent; + text-align: center; + width: 164px; +} + +org-chart .node .title { + text-overflow: ellipsis; + white-space: nowrap; + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + padding: 6px 69px; + gap: 10px; + background: #EF3340; + border-radius: 15px; + width: 154px; + height: 32px; + font-family: 'Jomhuria', sans-serif; + font-style: normal; + font-weight: 400; + font-size: 24px; + line-height: 24px; + color: #FFFFFF; + flex: none; + order: 0; + flex-grow: 0; + position: relative; + z-index: 1; +} + +org-chart .node .content { + position: relative; + top: -15px; + width: 154px; + min-height: 60px; + background: #EEEEEE; + border-radius: 0 0 15px 15px; +} + +org-chart tr.lines .topLine { + border-top: 2px solid black; +} + +org-chart tr.lines .rightLine { + border-right: 1px solid black; + float: none; + border-radius: 0; +} + +org-chart tr.lines .leftLine { + border-left: 1px solid black; + float: none; + border-radius: 0; +} + +org-chart tr.lines .downLine { + background-color: black; + margin: 0 auto; + height: 20px; + width: 2px; + float: none; +} + + +.team-leader-section { + width: 154px; + min-height: 35px; + left: 0px; + top: 14px; + background: #1A3E72; +} + From 85670b2faa05c967f7c0335db4ce8f2c1a8fce52 Mon Sep 17 00:00:00 2001 From: nh758 <7259@pm.me> Date: Wed, 25 Sep 2024 16:28:26 +0700 Subject: [PATCH 005/129] add new teams --- .../ABViewOrgChartTeamsComponent.js | 200 +++++++++++++++--- 1 file changed, 168 insertions(+), 32 deletions(-) diff --git a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js index c0fb7b54..7d35db51 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js @@ -9,6 +9,9 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { { chartView: "", chartDom: "", + teamForm: "", + teamFormPopup: "", + teamFormTitle: "", }, ids ) @@ -87,10 +90,11 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { zoom: true, //baseView.settings.zoom == 1, draggable, // visibleLevel: baseView.settings.visibleLevel, - + parentNodeSymbol: false, exportButton: baseView.settings.export, exportFilename: baseView.settings.exportFilename, createNode: ($node /*, data*/) => { + $node.onclick = (e) => this.nodeClick(e); // remove built in icon $node.querySelector(".title > i")?.remove(); // customize @@ -104,6 +108,8 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { nodeContent: "description", }); + this.__orgchart = orgchart; + if (draggable) { // On drop update the parent (dropZone) of the node orgchart.addEventListener("nodedropped.orgchart", (event) => { @@ -129,11 +135,8 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { chartDom.textContent = ""; chartDom.innerHTML = ""; chartDom.appendChild(orgchart); + this.toolbarUi(chartDom); } - - setTimeout(() => { - this._setColor(); - }, 1); } async pullData() { @@ -156,26 +159,30 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { const chartData = this.chartData; chartData.name = topNode[teamName] ?? ""; + chartData.id = this.teamNodeID(topNode.id); chartData._rawData = topNode; - function pullChildData(node) { + const maxDepth = 10; // prevent inifinite loop + function pullChildData(node, prefixFn, depth = 0) { + if (depth >= maxDepth) return; console.log(node.name, node._rawData[teamLink]); node.children = []; node._rawData[teamLink].forEach((id) => { const childData = dc.getData((e) => e.id === id)[0]; const child = { name: childData[teamName], + id: prefixFn(id), description: "...", _rawData: childData, }; if (childData[teamLink].length > 0) { - pullChildData(child); + pullChildData(child, prefixFn, depth + 1); } node.children.push(child); }); return; } - pullChildData(chartData); + pullChildData(chartData, this.teamNodeID); } get chartData() { @@ -194,32 +201,161 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { return this.AB.definitionByID(this.view.settings[setting]); } - _setColor() { - return; - const view = this.view; - let doms = document.querySelectorAll(`org-chart`); - doms.forEach((dom) => { - dom.style.backgroundImage = "none"; - }); + nodeClick(event) { + // if (this.tool === "add") { + const recordID = this.teamRecordID(event.currentTarget.id); + this.teamForm("Add", { __parentID: recordID }); + // this.addChildNode(event); + // } + } - doms = document.querySelectorAll(` - org-chart .verticalNodes>td::before, - org-chart .verticalNodes ul>li::before, - org-chart .verticalNodes ul>li::after, - org-chart .node .content, - org-chart tr.lines .topLine, - org-chart tr.lines .rightLine, - org-chart tr.lines .leftLine`); - doms.forEach((dom) => { - dom.style.borderColor = view.settings.color; - }); + async teamAddChild(values) { + const { id } = await this.datacollection.model.create(values); - doms = document.querySelectorAll(` - org-chart tr.lines .downLine, - org-chart .node .title`); - doms.forEach((dom) => { - dom.style.backgroundColor = view.settings.color; - }); + const linkField = this.AB.definitionByID( + this.getSettingField("teamLink").settings.linkColumn + ).columnName; + const nameField = this.getSettingField("teamName").columnName; + const parent = document.querySelector( + `#${this.teamNodeID(values[linkField])}` + ); + const hasChild = parent.parentNode.colSpan > 1; + const newChild = { + name: values[nameField], + id: this.teamNodeID(id), + relationship: hasChild ? "110" : "100", + }; + // Need to add differently if the node already has child nodes + if (hasChild) { + const sibling = this.closest(parent, (el) => el.nodeName === "TABLE") + .querySelector(".nodes") + .querySelector(".node"); + this.__orgchart.addSiblings(sibling, { siblings: [newChild] }); + } else { + this.__orgchart.addChildren(parent, { + children: [newChild], + }); + } + } + + teamForm(mode, values) { + let $teamFormPopup = $$(this.ids.teamFormPopup); + const linkField = this.AB.definitionByID( + this.getSettingField("teamLink").settings.linkColumn + ).columnName; + if (!$teamFormPopup) { + const nameField = this.getSettingField("teamName"); + $teamFormPopup = webix.ui({ + view: "popup", + id: this.ids.teamFormPopup, + close: true, + position: "center", + body: { + rows: [ + { + view: "toolbar", + id: "myToolbar", + cols: [ + { + view: "label", + label: `Edit Team`, + align: "left", + id: this.ids.teamFormTitle, + }, + { + view: "button", + value: "X", + align: "right", + click: () => $teamFormPopup.hide(), + }, + ], + }, + { + view: "form", + id: this.ids.teamForm, + elements: [ + { + view: "text", + label: nameField.label ?? nameField.columnName, + name: nameField.columnName, + }, + { view: "text", name: "id", hidden: true }, + { view: "text", name: linkField, hidden: true }, + { + view: "button", + value: "Add", + css: "webix_primary", + click: () => { + const values = $$(this.ids.teamForm).getValues(); + if (values.id) { + //TODO + } else { + this.teamAddChild(values); + } + $teamFormPopup.hide(); + }, + }, + ], + }, + ], + }, + }); + } + if (values.__parentID) { + values[linkField] = values.__parentID; + delete values.__parentID; + } + $$(this.ids.teamFormTitle).define("label", `${mode} Team`); + $$(this.ids.teamForm).setValues(values); + $teamFormPopup.show(); + } + + /** + * generate a id for the team dom node based on it's record id + * @param {string} id record id + */ + teamNodeID(id) { + return `teamnode_${id}`; + } + + /** + * extract the record id from the team dom node id + * @param {string} id dom node id + */ + teamRecordID(id) { + return id.split("_")[1]; + } + + /** + * Create toolbar ui + * @param {HTMLElement} dom node + */ + toolbarUi(dom) { + const toolbar = document.createElement("div"); + toolbar.classList.add("team-chart-toolbar"); + const button = document.createElement("button"); + button.classList.add("team-chart-toolbar"); + button.textContent = "add"; + button.onclick(() => (this.tool = "add")); + toolbar.appendChild(button); + dom.appendChild(toolbar); + } + + // UTIL + + /** + * Recursively finds the closest ancestor element that matches the provided function. + * @param {Element} el - The starting element. + * @param {Function} fn - The function to test against. + * @return {Element|null} The closest matching ancestor element or null if no match is found. + */ + closest(el, fn) { + return ( + el && + (fn(el) && el !== document.querySelector(`#${this.ids.chartDom}`) + ? el + : this.closest(el.parentNode, fn)) + ); } busy() { From 3a8f21cdda7874310a61efde253a56abe4cdcc3f Mon Sep 17 00:00:00 2001 From: nh758 <7259@pm.me> Date: Fri, 27 Sep 2024 11:27:49 +0700 Subject: [PATCH 006/129] add ability to edit teams --- .../ABViewOrgChartTeamsComponent.js | 84 +++++++++++++++---- 1 file changed, 69 insertions(+), 15 deletions(-) diff --git a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js index 7d35db51..4e9065a3 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js @@ -11,7 +11,9 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { chartDom: "", teamForm: "", teamFormPopup: "", + teamFormSubmit: "", teamFormTitle: "", + teamFormInactive: "", }, ids ) @@ -156,6 +158,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { const teamLink = this.getSettingField("teamLink").columnName; const teamName = this.getSettingField("teamName").columnName; + const teamInactive = this.getSettingField("teamInactive").columnName; const chartData = this.chartData; chartData.name = topNode[teamName] ?? ""; @@ -165,10 +168,12 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { const maxDepth = 10; // prevent inifinite loop function pullChildData(node, prefixFn, depth = 0) { if (depth >= maxDepth) return; - console.log(node.name, node._rawData[teamLink]); node.children = []; node._rawData[teamLink].forEach((id) => { const childData = dc.getData((e) => e.id === id)[0]; + // Don't show inactive teams + // @TODO this should be a default filter option + if (childData[teamInactive]) return; const child = { name: childData[teamName], id: prefixFn(id), @@ -180,6 +185,10 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { } node.children.push(child); }); + // sort children alphabetically + node.children = node.children.sort((a, b) => + a.name > b.name ? 1 : -1 + ); return; } pullChildData(chartData, this.teamNodeID); @@ -202,11 +211,13 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { } nodeClick(event) { - // if (this.tool === "add") { const recordID = this.teamRecordID(event.currentTarget.id); - this.teamForm("Add", { __parentID: recordID }); - // this.addChildNode(event); - // } + if (this.tool === "add") { + this.teamForm("Add", { __parentID: recordID }); + } else if (this.tool === "edit") { + const [values] = this.datacollection.getData((e) => e.id === recordID); + this.teamForm("Edit", values); + } } async teamAddChild(values) { @@ -238,8 +249,36 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { } } + teamCanInactivate(values) { + const isInactive = this.getSettingField("teamInactive").columnName; + if (values[isInactive]) return true; // Allow activating inactive teams + const canInactive = this.getSettingField("teamCanInactivate").columnName; + if (!values[canInactive]) return false; + const children = this.getSettingField("teamLink").columnName; + if (values[children].length > 0) return false; + // @TODO check for active assignment + // if (hasActiveAssignment) return false; + return true; + } + + teamEdit(values) { + this.datacollection.model.update(values.id, values).catch((err) => { + //TODO + }); + const nodeID = this.teamNodeID(values.id); + const node = document.querySelector(`#${nodeID}`); + const inactive = this.getSettingField("teamInactive").columnName; + // @TODO this will need to check against active filters + if (values[inactive]) { + this.__orgchart.removeNodes(node); + } + const nameCol = this.getSettingField("teamName").columnName; + node.querySelector(".title").innerHTML = values[nameCol]; + } + teamForm(mode, values) { let $teamFormPopup = $$(this.ids.teamFormPopup); + const inactive = this.getSettingField("teamInactive").columnName; const linkField = this.AB.definitionByID( this.getSettingField("teamLink").settings.linkColumn ).columnName; @@ -279,18 +318,25 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { label: nameField.label ?? nameField.columnName, name: nameField.columnName, }, + { + view: "switch", + id: this.ids.teamFormInactive, + name: inactive, + label: "Inactive", + }, { view: "text", name: "id", hidden: true }, { view: "text", name: linkField, hidden: true }, { view: "button", + id: this.ids.teamFormSubmit, value: "Add", css: "webix_primary", click: () => { const values = $$(this.ids.teamForm).getValues(); - if (values.id) { - //TODO - } else { + if (this.tool === "add") { this.teamAddChild(values); + } else if (this.tool === "edit") { + this.teamEdit(values); } $teamFormPopup.hide(); }, @@ -306,7 +352,15 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { delete values.__parentID; } $$(this.ids.teamFormTitle).define("label", `${mode} Team`); + $$(this.ids.teamFormTitle).refresh(); + $$(this.ids.teamFormSubmit).setValue(mode); $$(this.ids.teamForm).setValues(values); + this.teamCanInactivate(values) + ? $$(this.ids.teamFormInactive).enable() + : $$(this.ids.teamFormInactive).disable(); + if (mode === "Edit") { + // Check if we can inactivate + } $teamFormPopup.show(); } @@ -333,16 +387,16 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { toolbarUi(dom) { const toolbar = document.createElement("div"); toolbar.classList.add("team-chart-toolbar"); - const button = document.createElement("button"); - button.classList.add("team-chart-toolbar"); - button.textContent = "add"; - button.onclick(() => (this.tool = "add")); - toolbar.appendChild(button); + ["add", "edit", "delete"].forEach((m) => { + const button = document.createElement("button"); + button.classList.add("team-chart-button"); + button.textContent = m; + button.onclick = () => (this.tool = m); + toolbar.appendChild(button); + }); dom.appendChild(toolbar); } - // UTIL - /** * Recursively finds the closest ancestor element that matches the provided function. * @param {Element} el - The starting element. From 6453843d1f4fa6e7fbb0c44671181e6dc9adc78d Mon Sep 17 00:00:00 2001 From: nh758 <7259@pm.me> Date: Fri, 27 Sep 2024 12:15:56 +0700 Subject: [PATCH 007/129] add ability to delete teams --- .../ABViewOrgChartTeamsComponent.js | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js index 4e9065a3..a1959eb9 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js @@ -217,6 +217,9 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { } else if (this.tool === "edit") { const [values] = this.datacollection.getData((e) => e.id === recordID); this.teamForm("Edit", values); + } else if (this.tool === "delete") { + const [values] = this.datacollection.getData((e) => e.id === recordID); + this.teamDelete(values); } } @@ -261,6 +264,34 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { return true; } + teamCanDelete(values) { + const canInactive = this.getSettingField("teamCanInactivate").columnName; + if (!values[canInactive]) return false; + const children = this.getSettingField("teamLink").columnName; + if (values[children].length > 0) return false; + // @TODO check for any assignment + // if (hasAssignment) return false; + return true; + } + + teamDelete(values) { + if (!this.teamCanDelete(values)) { + this.AB.Webix.message({ + text: "This team cannot be deleted", + type: "error", + expire: 1001, + }); + return; + } + this.AB.Webix.confirm({ + text: "This can't be undone, are you sure?", + }).then(() => { + this.datacollection.model.delete(values.id); + const nodeID = this.teamNodeID(values.id); + this.__orgchart.removeNodes(document.querySelector(`#${nodeID}`)); + }); + } + teamEdit(values) { this.datacollection.model.update(values.id, values).catch((err) => { //TODO From 7b82d5e8da38bd710d875f7920147737625c150c Mon Sep 17 00:00:00 2001 From: nh758 <7259@pm.me> Date: Mon, 30 Sep 2024 11:06:03 +0700 Subject: [PATCH 008/129] move actions to buttons on the team node --- .../ABViewOrgChartTeamsComponent.js | 61 +++++++++++------ styles/team-widget.css | 67 ++++++++++++++++++- 2 files changed, 106 insertions(+), 22 deletions(-) diff --git a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js index a1959eb9..5e520f46 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js @@ -95,16 +95,36 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { parentNodeSymbol: false, exportButton: baseView.settings.export, exportFilename: baseView.settings.exportFilename, - createNode: ($node /*, data*/) => { - $node.onclick = (e) => this.nodeClick(e); + createNode: ($node, { id }) => { // remove built in icon $node.querySelector(".title > i")?.remove(); // customize const $content = $node.querySelector(".content"); $content.innerHTML = ""; - const $leaderSection = document.createElement("div"); - $leaderSection.classList.add("team-leader-section"); - $content.append($leaderSection); + const $leaderSection = element("div", "team-leader-section"); + const $memberSection = element("div", "team-member-section"); + const $buttons = element("div", "team-button-section"); + 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(id); + const values = this.datacollection.getData( + (e) => e.id === dataID + )[0]; + $addButton.onclick = () => { + this.teamForm("Add", { __parentID: dataID }); + }; + $editButton.onclick = () => 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); + } + $content.append($leaderSection, $memberSection, $buttons); }, nodeContent: "description", @@ -210,19 +230,6 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { return this.AB.definitionByID(this.view.settings[setting]); } - nodeClick(event) { - const recordID = this.teamRecordID(event.currentTarget.id); - if (this.tool === "add") { - this.teamForm("Add", { __parentID: recordID }); - } else if (this.tool === "edit") { - const [values] = this.datacollection.getData((e) => e.id === recordID); - this.teamForm("Edit", values); - } else if (this.tool === "delete") { - const [values] = this.datacollection.getData((e) => e.id === recordID); - this.teamDelete(values); - } - } - async teamAddChild(values) { const { id } = await this.datacollection.model.create(values); @@ -364,10 +371,10 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { css: "webix_primary", click: () => { const values = $$(this.ids.teamForm).getValues(); - if (this.tool === "add") { - this.teamAddChild(values); - } else if (this.tool === "edit") { + if (values.id) { this.teamEdit(values); + } else { + this.teamAddChild(values); } $teamFormPopup.hide(); }, @@ -455,3 +462,15 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { $chartView?.hideProgress?.(); } }; + +/** + * Creates a new HTML element with the given type and classes. + * @param {string} type - The type of the HTML element to create. + * @param {string} classes - A space-separated list of classes to add to the element. + * @returns {Element} The newly created HTML element. + */ +function element(type, classes) { + const elem = document.createElement(type); + elem.classList.add(...classes.split(" ")); + return elem; +} diff --git a/styles/team-widget.css b/styles/team-widget.css index 1fcf374a..d2b4fd86 100644 --- a/styles/team-widget.css +++ b/styles/team-widget.css @@ -41,8 +41,16 @@ org-chart .node .content { top: -15px; width: 154px; min-height: 60px; - background: #EEEEEE; border-radius: 0 0 15px 15px; + display: flex; + flex-direction: column; + align-items: flex-end; + flex-wrap: nowrap; + gap: 4px; + margin: 0 auto; + background: #dddddd; + overflow: hidden; + box-shadow: 0 4px 4px 0 rgba(0, 0, 0, 0.25); } org-chart tr.lines .topLine { @@ -76,5 +84,62 @@ org-chart tr.lines .downLine { left: 0px; top: 14px; background: #1A3E72; + display: flex; + align-self: stretch; + flex-wrap: nowrap; + gap: 5px; + /* position: relative; */ +} + +.team-member-section { + display: flex; + flex-direction: column; + align-items: flex-end; + flex-wrap: nowrap; + gap: 4px; + /* position: relative; */ + width: 154px; + min-height: 21px; +} + +.team-button-section { + display: flex; + flex-direction: row; + padding: 3px; + gap: 6px; + justify-content: flex-end; +} + +.team-button { + display: flex; + align-items: center; + justify-content: center; + gap: 2px; + width: 28px; + height: 10px; + padding: 2px 4px; + background: #1A3E72; + border: none; + border-radius: 5px; + cursor: pointer; + font-size: 8px; + color: white; +} + +.team-chart-toolbar { + display: flex; + flex-direction: row; + align-items: flex-start; + padding: 17px 24px; + gap: 10px; + + width: 254px; + background: #FFFFFF; + box-shadow: 0px -1px 4px rgba(0, 0, 0, 0.25); + border-radius: 20px; +} + +.team-chart-button { + } From e85743837f06b39ad16dd9de4c4a7e397e3b0ff5 Mon Sep 17 00:00:00 2001 From: nh758 <7259@pm.me> Date: Thu, 3 Oct 2024 13:33:04 +0700 Subject: [PATCH 009/129] add unit tests --- .../ABViewOrgChartTeamsComponent.js | 19 +- .../viewComponent/ABViewOrgChartTeams.test.js | 234 ++++++++++++++++++ 2 files changed, 235 insertions(+), 18 deletions(-) create mode 100644 test/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeams.test.js diff --git a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js index 5e520f46..05d49e43 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js @@ -290,7 +290,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { }); return; } - this.AB.Webix.confirm({ + return this.AB.Webix.confirm({ text: "This can't be undone, are you sure?", }).then(() => { this.datacollection.model.delete(values.id); @@ -418,23 +418,6 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { return id.split("_")[1]; } - /** - * Create toolbar ui - * @param {HTMLElement} dom node - */ - toolbarUi(dom) { - const toolbar = document.createElement("div"); - toolbar.classList.add("team-chart-toolbar"); - ["add", "edit", "delete"].forEach((m) => { - const button = document.createElement("button"); - button.classList.add("team-chart-button"); - button.textContent = m; - button.onclick = () => (this.tool = m); - toolbar.appendChild(button); - }); - dom.appendChild(toolbar); - } - /** * Recursively finds the closest ancestor element that matches the provided function. * @param {Element} el - The starting element. diff --git a/test/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeams.test.js b/test/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeams.test.js new file mode 100644 index 00000000..0ed54548 --- /dev/null +++ b/test/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeams.test.js @@ -0,0 +1,234 @@ +import { assert } from "chai"; +import sinon from "sinon"; +import ABFactory from "../../../../../AppBuilder/ABFactory"; +import ABViewOrgChartTeams from "../../../../../AppBuilder/platform/views/ABViewOrgChartTeams"; +import ABViewOrgChartTeamsComponent from "../../../../../AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent"; + +describe("ABViewDetailCheckboxComponent item widget", function () { + let sandbox; + let selectorStub; + let modelCreate; + let teamChart; + + beforeEach(function () { + sandbox = sinon.createSandbox(); + const AB = new ABFactory(); + const application = AB.applicationNew({}); + sinon.stub(AB, "definitionByID").returns({}); + const view = new ABViewOrgChartTeams({}, application); + teamChart = new ABViewOrgChartTeamsComponent(view); + modelCreate = sinon.fake.resolves({ id: "new" }); + teamChart.datacollection = { model: {} }; + teamChart.datacollection.model.create = modelCreate; + sinon.replace(teamChart, "getSettingField", (n) => ({ + columnName: n, + settings: {}, + })); + teamChart.__orgchart = { + addSiblings: sinon.fake(), + addChildren: sinon.fake(), + removeNodes: sinon.fake(), + }; + selectorStub = sinon.stub(document, "querySelector"); + }); + + afterEach(function () { + selectorStub.restore(); + sandbox.restore(); + }); + + it(".pullData prepares data for org-chart", async function () { + const dc = {}; + sinon.stub(teamChart.view, "datacollection").get(() => dc); + assert.equal(teamChart.view.datacollection, dc); + dc.waitForDataCollectionToInitialize = sinon.fake.resolves(); + dc.getCursor = sinon.fake.returns({ + id: "1", + teamName: "One", + teamLink: ["2", "3", "7"], + }); + const data = (id, teamName, teamLink = []) => [ + { + id, + teamName, + teamLink, + __rawData: { teamLink }, + teamInactive: false, + }, + ]; + dc.getData = sinon + .stub() + .onCall(0) + .returns(data("2", "Two")) + .onCall(1) + .returns(data("3", "Three", ["4", "6"])) + .onCall(2) + .returns(data("4", "Four", ["5"])) + .onCall(3) + .returns(data("5", "Five")) + .onCall(4) + .returns(data("6", "Six")) + .onCall(5) + .returns(data("7", "Seven")); + await teamChart.pullData(); + const expected = (i, n) => ({ id: `teamnode_${i}`, name: n }); + assert(dc.getData.callCount, 6); + // Check expected data strucutre, note: the calls will process children + // before siblings. Also siblings get sorted alphabetically by name so we + // expect: 1 ---- 7 + // \-- 3 --- 4 - 5 + // \- 2 \- 11 + assert.include(teamChart.chartData, expected(1, "One")); + assert.include(teamChart.chartData.children[2], expected(2, "Two")); + assert.include(teamChart.chartData.children[1], expected(3, "Three")); + assert.include( + teamChart.chartData.children[1].children[0], + expected(4, "Four") + ); + assert.include( + teamChart.chartData.children[1].children[0].children[0], + expected(5, "Five") + ); + assert.include( + teamChart.chartData.children[1].children[1], + expected(6, "Six") + ); + assert.include(teamChart.chartData.children[0], expected(7, "Seven")); + }); + + describe(".teamAddChild", function () { + const values = { teamName: "Test" }; + + before(function () {}); + + beforeEach(function () { + sinon.stub(teamChart, "closest").returns({ + querySelector: function () { + return this; + }, + }); + }); + + it("adds a team node as child", async function () { + selectorStub.returns({ parentNode: { colSpan: 0 } }); + await teamChart.teamAddChild(values); + assert(modelCreate.calledOnce); + assert(teamChart.__orgchart.addChildren.calledOnce); + assert.deepEqual( + teamChart.__orgchart.addChildren.lastArg.children[0], + { relationship: "100", name: "Test", id: "teamnode_new" } + ); + }); + + it("adds a team node as sibling", async function () { + selectorStub.returns({ parentNode: { colSpan: 2 } }); + await teamChart.teamAddChild(values); + assert(modelCreate.calledOnce); + assert(teamChart.__orgchart.addSiblings.calledOnce); + assert.deepEqual( + teamChart.__orgchart.addSiblings.lastArg.siblings[0], + { relationship: "110", name: "Test", id: "teamnode_new" } + ); + }); + }); + + it(".teamCanInactivate", function () { + const tests = [ + { + values: { + teamInactive: false, + teamCanInactivate: true, + teamLink: [], + }, + expected: true, + }, + { + values: { + teamInactive: false, + teamCanInactivate: false, + teamLink: [], + }, + expected: false, + }, + { + values: { + teamInactive: true, + teamCanInactivate: false, + teamLink: [], + }, + expected: true, + }, + { + values: { + teamInactive: false, + teamCanInactivate: true, + teamLink: [1], + }, + expected: false, + }, + ]; + tests.forEach((t, i) => { + const result = teamChart.teamCanInactivate(t.values); + assert.equal(result, t.expected, `case ${i + 1}`); + }); + }); + + it(".teamCanDelete", function () { + const tests = [ + { + values: { + teamCanInactivate: true, + teamLink: [], + }, + expected: true, + }, + { + values: { + teamCanInactivate: false, + teamLink: [], + }, + expected: false, + }, + { + values: { + teamCanInactivate: true, + teamLink: [1], + }, + expected: false, + }, + ]; + tests.forEach((t, i) => { + const result = teamChart.teamCanDelete(t.values); + assert.equal(result, t.expected, `case ${i + 1}`); + }); + }); + + it(".teamDelete - calls model.delete & updates ui", async function () { + const canDeleteFake = sinon.fake.returns(true); + sinon.replace(teamChart, "teamCanDelete", canDeleteFake); + const values = { id: "delete" }; + teamChart.AB.Webix.confirm = sinon.fake.resolves(); + teamChart.datacollection.model.delete = sinon.fake.resolves(); + await teamChart.teamDelete(values); + assert(canDeleteFake.calledOnceWith(values)); + assert(teamChart.datacollection.model.delete.calledOnceWith(values.id)); + assert(teamChart.__orgchart.removeNodes.calledOnce); + }); + + it(".teamEdit - calls model.update", function () { + teamChart.datacollection.model.update = sinon.fake.resolves(); + selectorStub.returnsThis(); + + const values = { id: "update", teamName: "update" }; + teamChart.teamEdit(values); + assert( + teamChart.datacollection.model.update.calledOnceWith("update", values) + ); + }); + + it(".teamNodeID/teamRecordID can insert & extract ID", function () { + const id = "c43f40d9-6d6a-40d8-adaf-7c61a54b439e"; + const result = teamChart.teamRecordID(teamChart.teamNodeID(id)); + assert.equal(id, result); + }); +}); From 2bf6e8593f49a3bc97892d3be52905cefb02fcd1 Mon Sep 17 00:00:00 2001 From: nh758 <7259@pm.me> Date: Fri, 4 Oct 2024 14:47:54 +0700 Subject: [PATCH 010/129] update core --- AppBuilder/core | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AppBuilder/core b/AppBuilder/core index cce4c226..bb5cdd8f 160000 --- a/AppBuilder/core +++ b/AppBuilder/core @@ -1 +1 @@ -Subproject commit cce4c226d3a41f66bb0132a80c1bbb15944c3a9b +Subproject commit bb5cdd8fe6fc7d411ee801370f8f4e67d40ea036 From 963d6b4ac17bc5461257d1d81662393eddd0f661 Mon Sep 17 00:00:00 2001 From: Johnny Date: Fri, 11 Oct 2024 15:40:49 -0500 Subject: [PATCH 011/129] [wip] initial Netsuite object definitions --- AppBuilder/core | 2 +- AppBuilder/platform/ABModelApiNetsuite.js | 86 ++++++++++++++++++++++ AppBuilder/platform/ABObjectApiNetsuite.js | 7 ++ AppBuilder/platform/dataFields/ABField.js | 6 +- 4 files changed, 98 insertions(+), 3 deletions(-) create mode 100644 AppBuilder/platform/ABModelApiNetsuite.js create mode 100644 AppBuilder/platform/ABObjectApiNetsuite.js diff --git a/AppBuilder/core b/AppBuilder/core index cce4c226..5ab94be6 160000 --- a/AppBuilder/core +++ b/AppBuilder/core @@ -1 +1 @@ -Subproject commit cce4c226d3a41f66bb0132a80c1bbb15944c3a9b +Subproject commit 5ab94be69df29277b89c70e8c01c44b366282706 diff --git a/AppBuilder/platform/ABModelApiNetsuite.js b/AppBuilder/platform/ABModelApiNetsuite.js new file mode 100644 index 00000000..5d07bb42 --- /dev/null +++ b/AppBuilder/platform/ABModelApiNetsuite.js @@ -0,0 +1,86 @@ +// +// ABModelAPINetsuite +// +// Represents the Data interface for a connection to Netsuite. + +const ABModel = require("./ABModel"); + +module.exports = class ABModelAPINetsuite extends ABModel { + /// + /// Instance Methods + /// + + /** + * @method findAll + * performs a data find with the provided condition. + */ + async findAll(cond = {}) { + // cond.isNetsuite = true; + + // the server side handler will decode the cond obj + // with any specific info needed that isn't part + // of this Obj's data. + + // include those parameters here in cond, eg: + // cond.url = this.object?.request?.url; + + return super.findAll(cond); + } + + /** + * @method batchCreate + * update model values on the server. + */ + batchCreate(values) { + const error = new Error( + "ABObjectApi.ABModelAPINetsuite.batchCreate() does not be implemented." + ); + return Promise.reject(error); + } + + /** + * @method create + * update model values on the server. + */ + // async create(values) { + // const error = new Error( + // "ABObjectApi.ABModelAPINetsuite.create() does not be implemented." + // ); + // return Promise.reject(error); + // } + + /** + * @method delete + * remove this model instance from the server + * @param {integer|UUID} id the .id of the instance to remove. + * @return {Promise} + */ + delete(id) { + const error = new Error( + "ABObjectApi.ABModelAPINetsuite.delete() does not be implemented." + ); + return Promise.reject(error); + } + + /** + * @method update + * update model values on the server. + */ + update(id, values) { + const error = new Error( + "ABObjectApi.ABModelAPINetsuite.update() does not be implemented." + ); + return Promise.reject(error); + } + + /** + * @method batchUpdate + * update value to many rows on the server. + */ + batchUpdate({ rowIds, values }) { + const error = new Error( + "ABObjectApi.ABModelAPINetsuite.batchUpdate() does not be implemented." + ); + return Promise.reject(error); + } +}; diff --git a/AppBuilder/platform/ABObjectApiNetsuite.js b/AppBuilder/platform/ABObjectApiNetsuite.js new file mode 100644 index 00000000..02df1187 --- /dev/null +++ b/AppBuilder/platform/ABObjectApiNetsuite.js @@ -0,0 +1,7 @@ +const ABObjectApiNetsuiteCore = require("../core/ABObjectApiNetsuiteCore"); + +module.exports = class ABObjectApiNetsuite extends ABObjectApiNetsuiteCore { + constructor(attributes, AB) { + super(attributes, AB); + } +}; 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; } From 772201b3bb800184ea6435ec9752c61d225a5e9a Mon Sep 17 00:00:00 2001 From: guyyoo Date: Tue, 15 Oct 2024 15:06:14 +0700 Subject: [PATCH 012/129] Add node content and display --- .../ABViewOrgChartTeamsComponent.js | 204 +++++++++++++++++- 1 file changed, 197 insertions(+), 7 deletions(-) diff --git a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js index 928120fd..9b82427c 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js @@ -71,10 +71,196 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { async displayOrgChart() { const baseView = this.view; - const chartData = this.AB.cloneDeep(this.chartData); - const draggable = baseView.settings.draggable == 1; - + const AB = this.AB; + const chartData = AB.cloneDeep(this.chartData); + const settings = baseView.settings; + const draggable = settings.draggable === 1; + const nodeObj = baseView.datacollection?.datasource; + const nodeObjPK = nodeObj.PK(); + const contentField = nodeObj.fieldByID(settings.contentField); + const contentFieldColumnName = contentField.columnName; + const contentFieldLink = contentField.fieldLink; + const contentObj = contentFieldLink.object; + const contentObjPK = contentObj.PK(); + const contentDataRecordPKs = []; + const getContentDataRecordPKs = (node) => { + const contentFieldData = node._rawData[contentFieldColumnName]; + if (Array.isArray(contentFieldData)) + contentDataRecordPKs.push(...contentFieldData); + else contentDataRecordPKs.push(contentFieldData); + const children = node.children || []; + for (const child of children) getContentDataRecordPKs(child); + }; + getContentDataRecordPKs(chartData); + const contentDataRecords = ( + await contentObj.model().findAll({ + where: { + glue: "and", + rules: [ + { + key: contentObjPK, + rule: "in", + value: contentDataRecordPKs, + }, + ], + }, + populate: true, + }) + ).data; + const contentGroupByField = contentObj.fieldByID( + settings.contentGroupByField + ); + const contentGroupOptions = contentGroupByField.settings.options; + const contentGroupOptionsLength = contentGroupOptions.length; + const contentGroupByFieldColumnName = contentGroupByField.columnName; + const contentFieldLinkColumnName = contentFieldLink.columnName; + const contentObjID = contentObj.id; + const contentDisplayedFields = settings.contentDisplayedFields; + const contentDisplayedFieldsKeys = Object.keys(contentDisplayedFields); const orgchart = new this.OrgChart({ + createNode: async ($node, data) => { + const nodeWidth = 300; + const nodeStyle = $node.style; + nodeStyle["width"] = `${nodeWidth}px`; + const $contentNode = $node.children.item(1); + $contentNode.innerHTML = ""; + const contentNodeStyle = $contentNode.style; + + // Team content buckets + contentNodeStyle["height"] = `${ + // TODO (Guy): Fix the number later. + contentGroupOptionsLength * 150 + }px`; + const groupBorderWidth = 5; + contentNodeStyle["width"] = `${nodeWidth - 2 * groupBorderWidth}px`; + const averageHeight = 100 / contentGroupOptionsLength; + const currentNodeDataRecordPK = data._rawData[nodeObjPK]; + for (const group of contentGroupOptions) { + const $group = document.createElement("div"); + $contentNode.appendChild($group); + const groupStyle = $group.style; + groupStyle["height"] = `${averageHeight}%`; + groupStyle["borderStyle"] = "solid"; + groupStyle["borderWidth"] = `${groupBorderWidth}px`; + const groupColor = group.hex; + groupStyle["borderColor"] = groupColor; + const $groupTitle = document.createElement("div"); + const groupTitleStyle = $groupTitle.style; + groupTitleStyle["backgroundColor"] = groupColor; + groupTitleStyle["height"] = "20%"; + const groupText = group.text; + $groupTitle.appendChild(document.createTextNode(groupText)); + $group.appendChild($groupTitle); + const $groupContent = document.createElement("div"); + $group.appendChild($groupContent); + const groupContentStyle = $groupContent.style; + groupContentStyle["overflow"] = "auto"; + groupContentStyle["height"] = "80%"; + let contentDataRecordIndex = 0; + while ( + contentDataRecordIndex < contentDataRecords.length && + contentDataRecords.length > 0 + ) { + const contentDataRecord = + contentDataRecords[contentDataRecordIndex]; + if ( + contentDataRecord[contentFieldLinkColumnName] !== + currentNodeDataRecordPK || + contentDataRecord[contentGroupByFieldColumnName] !== + groupText + ) { + contentDataRecordIndex++; + continue; + } + contentDataRecords.splice(contentDataRecordIndex, 1); + const contentDataRecordPK = contentDataRecord[contentObjPK]; + const rowDataID = `${currentNodeDataRecordPK}.${contentDataRecordPK}`; + const $rowData = document.createElement("div"); + $groupContent.appendChild($rowData); + $rowData.setAttribute("id", rowDataID); + draggable && $rowData.setAttribute("draggable", "true"); + const rowDataStyle = $rowData.style; + rowDataStyle["borderStyle"] = "solid"; + rowDataStyle["borderColor"] = "black"; + let currentDataRecords = []; + let currentField = null; + for (let j = 0; j < contentDisplayedFieldsKeys.length; j++) { + const displayedFieldKey = contentDisplayedFieldsKeys[j]; + const [atDisplay, objID] = displayedFieldKey.split("."); + const displayedObj = AB.objectByID(objID); + const displayedObjPK = displayedObj.PK(); + const displayedField = displayedObj.fieldByID( + contentDisplayedFields[displayedFieldKey] + ); + switch (objID) { + case contentObjID: + currentDataRecords = [contentDataRecord]; + 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); + currentDataRecords = ( + await displayedObj.model().findAll({ + where: { + glue: "and", + rules: [ + { + key: displayedObj.PK(), + rule: "in", + value: currentDataPKs, + }, + ], + }, + populate: true, + }) + ).data; + } + break; + } + if ( + contentDisplayedFieldsKeys[j + 1]?.split(".")[0] === + atDisplay + ) { + currentField = displayedField; + continue; + } + const $currentDisplay = document.createElement("div"); + $rowData.appendChild($currentDisplay); + const displayedFieldColumnName = displayedField.columnName; + while (currentDataRecords.length > 0) { + const $currentDisplayData = + document.createElement("div"); + $currentDisplay.appendChild($currentDisplayData); + const currentDataRecord = currentDataRecords.pop(); + $currentDisplayData.setAttribute( + "id", + `${atDisplay}.${rowDataID}.${currentDataRecord[displayedObjPK]}` + ); + $currentDisplayData.appendChild( + document.createTextNode( + currentDataRecord[displayedFieldColumnName] + ) + ); + } + currentField = null; + } + } + } + }, data: chartData, direction: baseView.settings.direction, // depth: baseView.settings.depth, @@ -94,13 +280,13 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { // }, nodeContent: "description", }); - if (draggable) { // On drop update the parent (dropZone) of the node orgchart.addEventListener("nodedropped.orgchart", (event) => { const dragNode = JSON.parse( event.detail.draggedNode.dataset.source ); + debugger; const dropNode = JSON.parse(event.detail.dropZone.dataset.source); const dragRecord = dragNode._rawData; const dropID = dropNode._rawData.id; @@ -114,8 +300,13 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { this.datacollection.model.update(dragRecord.id, dragRecord); }); } - const chartDom = document.querySelector(`#${this.ids.chartDom}`); + const orgchartStyle = orgchart.style; + orgchartStyle["overflow"] = "auto"; + let $currentNode = chartDom; + while ($currentNode.style["height"] === "") + $currentNode = $currentNode.parentNode; + orgchartStyle["height"] = $currentNode.style["height"]; if (chartDom) { chartDom.textContent = ""; chartDom.innerHTML = ""; @@ -155,10 +346,10 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { chartData._rawData = topNode; function pullChildData(node) { - console.log(node.name, node._rawData[teamLink]); node.children = []; node._rawData[teamLink].forEach((id) => { const childData = dc.getData((e) => e.id === id)[0]; + if (childData == null) return; const child = { name: childData[teamName], description: "...", @@ -169,7 +360,6 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { } node.children.push(child); }); - return; } pullChildData(chartData); } From d1c025edff9f8ec03cfcaf797a1362e4c20f8a96 Mon Sep 17 00:00:00 2001 From: Johnny Date: Tue, 15 Oct 2024 23:26:22 -0500 Subject: [PATCH 013/129] [wip] base netsuite objects perform CRUD operations --- AppBuilder/platform/ABModelApiNetsuite.js | 114 +++++++----------- .../viewComponent/ABViewGridComponent.js | 8 +- 2 files changed, 51 insertions(+), 71 deletions(-) diff --git a/AppBuilder/platform/ABModelApiNetsuite.js b/AppBuilder/platform/ABModelApiNetsuite.js index 5d07bb42..8b845425 100644 --- a/AppBuilder/platform/ABModelApiNetsuite.js +++ b/AppBuilder/platform/ABModelApiNetsuite.js @@ -11,76 +11,50 @@ module.exports = class ABModelAPINetsuite extends ABModel { /// /** - * @method findAll - * performs a data find with the provided condition. + * @method normalizeData() + * For a Netsuite object, there are additional steps we need to handle + * to normalize our data. */ - async findAll(cond = {}) { - // cond.isNetsuite = true; - - // the server side handler will decode the cond obj - // with any specific info needed that isn't part - // of this Obj's data. - - // include those parameters here in cond, eg: - // cond.url = this.object?.request?.url; - - return super.findAll(cond); - } - - /** - * @method batchCreate - * update model values on the server. - */ - batchCreate(values) { - const error = new Error( - "ABObjectApi.ABModelAPINetsuite.batchCreate() does not be implemented." - ); - return Promise.reject(error); - } - - /** - * @method create - * update model values on the server. - */ - // async create(values) { - // const error = new Error( - // "ABObjectApi.ABModelAPINetsuite.create() does not be implemented." - // ); - // return Promise.reject(error); - // } - - /** - * @method delete - * remove this model instance from the server - * @param {integer|UUID} id the .id of the instance to remove. - * @return {Promise} - */ - delete(id) { - const error = new Error( - "ABObjectApi.ABModelAPINetsuite.delete() does not be implemented." - ); - return Promise.reject(error); - } - - /** - * @method update - * update model values on the server. - */ - update(id, values) { - const error = new Error( - "ABObjectApi.ABModelAPINetsuite.update() does not be implemented." - ); - return Promise.reject(error); - } - - /** - * @method batchUpdate - * update value to many rows on the server. - */ - batchUpdate({ rowIds, values }) { - const error = new Error( - "ABObjectApi.ABModelAPINetsuite.batchUpdate() does not be implemented." - ); - return Promise.reject(error); + 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/views/viewComponent/ABViewGridComponent.js b/AppBuilder/platform/views/viewComponent/ABViewGridComponent.js index ffc89ce9..876e09cd 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); From e4941d344cceef3f29928eefc70a84ff7a6f2f90 Mon Sep 17 00:00:00 2001 From: Johnny Date: Thu, 17 Oct 2024 22:02:26 -0500 Subject: [PATCH 014/129] [wip] make sure API objects don't trigger DB migrations --- AppBuilder/platform/ABObjectApi.js | 8 ++++++++ 1 file changed, 8 insertions(+) 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(); + } }; From 2c07c2ff612e5c82df7e44da9681f7b22ea2a5e0 Mon Sep 17 00:00:00 2001 From: nh758 <7259@pm.me> Date: Mon, 7 Oct 2024 15:35:49 +0700 Subject: [PATCH 015/129] fix edit checkbox field in grid --- .../platform/views/viewComponent/ABViewGridComponent.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/AppBuilder/platform/views/viewComponent/ABViewGridComponent.js b/AppBuilder/platform/views/viewComponent/ABViewGridComponent.js index 20f25de8..5ac74ac5 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewGridComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewGridComponent.js @@ -1379,9 +1379,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({ From c2d247a0c6a24ea28b83679b88fe9df906322a67 Mon Sep 17 00:00:00 2001 From: nh758 <7259@pm.me> Date: Mon, 21 Oct 2024 11:06:10 +0700 Subject: [PATCH 016/129] style node by startegy & edit strategy --- .../ABViewOrgChartTeamsComponent.js | 49 +++++++++++++++++-- styles/team-widget.css | 4 -- 2 files changed, 46 insertions(+), 7 deletions(-) diff --git a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js index 05d49e43..8fa4d5ec 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js @@ -72,6 +72,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { super.onShow(); this.busy(); + this.generateStrategyCss(); await this.loadOrgChartJs(); await this.pullData(); this.displayOrgChart(); @@ -157,7 +158,6 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { chartDom.textContent = ""; chartDom.innerHTML = ""; chartDom.appendChild(orgchart); - this.toolbarUi(chartDom); } } @@ -179,10 +179,15 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { const teamLink = this.getSettingField("teamLink").columnName; const teamName = this.getSettingField("teamName").columnName; const teamInactive = this.getSettingField("teamInactive").columnName; + const strategy = this.getSettingField("teamStrategy").columnName; + const strategyCode = this.getSettingField("strategyCode").columnName; const chartData = this.chartData; chartData.name = topNode[teamName] ?? ""; chartData.id = this.teamNodeID(topNode.id); + chartData.className = `strategy-${ + topNode[`${strategy}__relation`]?.[strategyCode] + }`; chartData._rawData = topNode; const maxDepth = 10; // prevent inifinite loop @@ -194,12 +199,17 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { // Don't show inactive teams // @TODO this should be a default filter option if (childData[teamInactive]) return; + const strategyClass = `strategy-${ + childData[`${strategy}__relation`]?.[strategyCode] + }`; const child = { name: childData[teamName], id: prefixFn(id), - description: "...", + className: strategyClass, _rawData: childData, }; + if (child.name === "External Support") + child.className = `strategy-external`; if (childData[teamLink].length > 0) { pullChildData(child, prefixFn, depth + 1); } @@ -230,6 +240,21 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { return this.AB.definitionByID(this.view.settings[setting]); } + generateStrategyCss() { + 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); + } + async teamAddChild(values) { const { id } = await this.datacollection.model.create(values); @@ -314,7 +339,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { node.querySelector(".title").innerHTML = values[nameCol]; } - teamForm(mode, values) { + async teamForm(mode, values) { let $teamFormPopup = $$(this.ids.teamFormPopup); const inactive = this.getSettingField("teamInactive").columnName; const linkField = this.AB.definitionByID( @@ -322,6 +347,10 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { ).columnName; if (!$teamFormPopup) { const nameField = this.getSettingField("teamName"); + const [strategyField] = this.datacollection.datasource.fields( + (f) => f.id == this.view.settings["teamStrategy"] + ); + const strategyOptions = await strategyField.getOptions(); $teamFormPopup = webix.ui({ view: "popup", id: this.ids.teamFormPopup, @@ -356,6 +385,13 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { label: nameField.label ?? nameField.columnName, name: nameField.columnName, }, + { + view: "richselect", + label: + strategyField.label ?? strategyField.columnName, + name: strategyField.columnName, + options: strategyOptions.map(fieldToOption), + }, { view: "switch", id: this.ids.teamFormInactive, @@ -457,3 +493,10 @@ function element(type, classes) { elem.classList.add(...classes.split(" ")); return elem; } + +function fieldToOption(f) { + return { + id: f.id, + value: f.text, + }; +} diff --git a/styles/team-widget.css b/styles/team-widget.css index d2b4fd86..3a2772dd 100644 --- a/styles/team-widget.css +++ b/styles/team-widget.css @@ -139,7 +139,3 @@ org-chart tr.lines .downLine { border-radius: 20px; } -.team-chart-button { - -} - From de74242ae10f7fa7c36adaedc7ac3f01a7965a4f Mon Sep 17 00:00:00 2001 From: nh758 <7259@pm.me> Date: Mon, 21 Oct 2024 14:36:05 +0700 Subject: [PATCH 017/129] update core --- AppBuilder/core | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AppBuilder/core b/AppBuilder/core index bb5cdd8f..b3764fe5 160000 --- a/AppBuilder/core +++ b/AppBuilder/core @@ -1 +1 @@ -Subproject commit bb5cdd8fe6fc7d411ee801370f8f4e67d40ea036 +Subproject commit b3764fe5583375adad2fbcf7fb39b5d365f6a486 From 1f2a1821fdb5e2d875d2cc6624f7cb5a7670c029 Mon Sep 17 00:00:00 2001 From: nh758 <7259@pm.me> Date: Mon, 21 Oct 2024 14:37:37 +0700 Subject: [PATCH 018/129] update tests to cover strategy color --- .../viewComponent/ABViewOrgChartTeams.test.js | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/test/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeams.test.js b/test/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeams.test.js index 0ed54548..4b2ee423 100644 --- a/test/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeams.test.js +++ b/test/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeams.test.js @@ -15,7 +15,8 @@ describe("ABViewDetailCheckboxComponent item widget", function () { const AB = new ABFactory(); const application = AB.applicationNew({}); sinon.stub(AB, "definitionByID").returns({}); - const view = new ABViewOrgChartTeams({}, application); + const settings = { strategyColors: { ops: "#111111", slm: "#222222" } }; + const view = new ABViewOrgChartTeams({ settings }, application); teamChart = new ABViewOrgChartTeamsComponent(view); modelCreate = sinon.fake.resolves({ id: "new" }); teamChart.datacollection = { model: {} }; @@ -37,6 +38,23 @@ describe("ABViewDetailCheckboxComponent item widget", function () { sandbox.restore(); }); + it(".generateStrategyCss adds css rules", function () { + teamChart.generateStrategyCss(); + const css = document.getElementsByTagName("style")[0].innerHTML; + assert.include( + css, + "org-chart .strategy-external .title{background:#989898 !important;}" + ); + assert.include( + css, + "org-chart .strategy-ops .title{background:#111111 !important;}" + ); + assert.include( + css, + "org-chart .strategy-slm .title{background:#222222 !important;}" + ); + }); + it(".pullData prepares data for org-chart", async function () { const dc = {}; sinon.stub(teamChart.view, "datacollection").get(() => dc); @@ -46,6 +64,7 @@ describe("ABViewDetailCheckboxComponent item widget", function () { id: "1", teamName: "One", teamLink: ["2", "3", "7"], + teamStrategy__relation: { strategyCode: "test" }, }); const data = (id, teamName, teamLink = []) => [ { @@ -94,6 +113,7 @@ describe("ABViewDetailCheckboxComponent item widget", function () { expected(6, "Six") ); assert.include(teamChart.chartData.children[0], expected(7, "Seven")); + assert.equal(teamChart.chartData.className, "strategy-test"); }); describe(".teamAddChild", function () { From 5bffd4120f16ddac822b43bbf3bedfdef73cd00f Mon Sep 17 00:00:00 2001 From: Johnny Date: Tue, 22 Oct 2024 02:56:57 -0500 Subject: [PATCH 019/129] [fix] reference the PK() instead of hardcoding a `uuid` lookup value --- AppBuilder/core | 2 +- AppBuilder/platform/dataFields/ABFieldConnect.js | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/AppBuilder/core b/AppBuilder/core index 5ab94be6..aac1d2cb 160000 --- a/AppBuilder/core +++ b/AppBuilder/core @@ -1 +1 @@ -Subproject commit 5ab94be69df29277b89c70e8c01c44b366282706 +Subproject commit aac1d2cb67015597a7e5a888fb62924e62340f0b diff --git a/AppBuilder/platform/dataFields/ABFieldConnect.js b/AppBuilder/platform/dataFields/ABFieldConnect.js index f68c83d9..ad3bd226 100644 --- a/AppBuilder/platform/dataFields/ABFieldConnect.js +++ b/AppBuilder/platform/dataFields/ABFieldConnect.js @@ -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, }); From e3a06ba5330fef909ddef3f80db87148fa25bbba Mon Sep 17 00:00:00 2001 From: nh758 <7259@pm.me> Date: Wed, 23 Oct 2024 16:19:33 +0700 Subject: [PATCH 020/129] add team filtering options https://github.com/digi-serve/ab_platform_web/issues/573 --- .../ABViewOrgChartTeamsComponent.js | 117 ++++++++++++++++-- styles/team-widget.css | 24 ++++ 2 files changed, 129 insertions(+), 12 deletions(-) diff --git a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js index 8fa4d5ec..1a51a263 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js @@ -9,6 +9,8 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { { chartView: "", chartDom: "", + filterPopup: "", + filterForm: "", teamForm: "", teamFormPopup: "", teamFormSubmit: "", @@ -96,12 +98,18 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { parentNodeSymbol: false, exportButton: baseView.settings.export, exportFilename: baseView.settings.exportFilename, - createNode: ($node, { id }) => { + createNode: ($node, { id, filteredOut }) => { // remove built in icon $node.querySelector(".title > i")?.remove(); // customize const $content = $node.querySelector(".content"); $content.innerHTML = ""; + if (filteredOut) { + // This node doesn't pass the filter, but it's children do so + // simplify the display. + $content.style.display = "none"; + return; + } const $leaderSection = element("div", "team-leader-section"); const $memberSection = element("div", "team-member-section"); const $buttons = element("div", "team-button-section"); @@ -157,11 +165,12 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { if (chartDom) { chartDom.textContent = ""; chartDom.innerHTML = ""; + this.initFilter(chartDom); chartDom.appendChild(orgchart); } } - async pullData() { + async pullData(filters = {}) { const view = this.view; const dc = view.datacollection; await dc?.waitForDataCollectionToInitialize(dc); @@ -197,28 +206,49 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { node._rawData[teamLink].forEach((id) => { const childData = dc.getData((e) => e.id === id)[0]; // Don't show inactive teams - // @TODO this should be a default filter option - if (childData[teamInactive]) return; - const strategyClass = `strategy-${ - childData[`${strategy}__relation`]?.[strategyCode] - }`; + if (filters?.inactive !== 1 && childData[teamInactive]) return; + const code = childData[`${strategy}__relation`]?.[strategyCode]; + const strategyClass = `strategy-${code}`; const child = { name: childData[teamName], id: prefixFn(id), className: strategyClass, _rawData: childData, }; + // Apply filters (match using or) + if (filters.strategy || filters.teamName) { + child.filteredOut = true; + if (filters.strategy !== "" && filters.strategy === code) { + child.filteredOut = false; + } + if ( + filters.teamName !== "" && + child.name + .toLowerCase() + .includes(filters.teamName.toLowerCase()) + ) { + child.filteredOut = false; + } + } if (child.name === "External Support") child.className = `strategy-external`; if (childData[teamLink].length > 0) { pullChildData(child, prefixFn, depth + 1); } - node.children.push(child); + // If this node is filtered we still need it if it has children + // that pass + if (!child.filteredOut || child.children?.length > 0) { + node.children.push(child); + } }); - // sort children alphabetically - node.children = node.children.sort((a, b) => - a.name > b.name ? 1 : -1 - ); + if (node.children.length === 0) { + delete node.children; + } else { + // sort children alphaetically + node.children = node.children.sort((a, b) => + a.name > b.name ? 1 : -1 + ); + } return; } pullChildData(chartData, this.teamNodeID); @@ -231,6 +261,69 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { return this._chartData; } + initFilter(domNode) { + const filterButton = document.createElement("button"); + filterButton.innerHTML = ` Filter`; + filterButton.classList.add("filter-button"); + filterButton.onclick = () => this.filterWindow(filterButton); + domNode.append(filterButton); + } + + async filterWindow(buttonNode) { + let $popup = $$(this.ids.filterPopup); + if (!$popup) { + const [strategyField] = this.datacollection.datasource.fields( + (f) => f.id == this.view.settings["teamStrategy"] + ); + const strategyOptions = await strategyField.getOptions(); + + $popup = webix.ui({ + view: "popup", + css: "filter-popup", + id: this.ids.filterPopup, + body: { + view: "form", + id: this.ids.filterForm, + elements: [ + { + view: "text", + label: this.label("Team Name"), + labelWidth: 90, + name: "teamName", + clear: true, + }, + { + view: "combo", + label: this.label("Strategy"), + labelWidth: 90, + options: strategyOptions.map((f) => f.text), + name: "strategy", + clear: "replace", + }, + { + view: "checkbox", + name: "inactive", + labelRight: this.label("Show Inactive Teams"), + labelWidth: 0, + }, + { + view: "button", + label: this.label("Apply"), + click: () => this.filterApply(), + }, + ], + }, + }); + } + $popup.show(buttonNode); + } + + filterApply() { + $$(this.ids.filterPopup).hide(); + const filters = $$(this.ids.filterForm).getValues(); + this.pullData(filters).then(() => this.displayOrgChart()); + } + /** * Get the ABField from settings * @param {string} setting key in this.view.settings - should be an id for an diff --git a/styles/team-widget.css b/styles/team-widget.css index 3a2772dd..7fcbbec1 100644 --- a/styles/team-widget.css +++ b/styles/team-widget.css @@ -139,3 +139,27 @@ org-chart tr.lines .downLine { border-radius: 20px; } +.filter-button { + border-radius: 10px; + background: #FFF; + box-shadow: 0px 4px 4px 0px rgba(0, 0, 0, 0.25); + color: #2F27CE; + font-family: Roboto; + font-size: 20px; + font-style: normal; + font-weight: 400; + line-height: normal; + padding: 7px 15px; + position: absolute; +} + +.filter-button:hover { + background: #2F27CE; + color: #FFF; +} + +.filter-popup { + border-radius: 10px; + background: #FFF; + box-shadow: 0px 4px 4px 0px rgba(0, 0, 0, 0.25); +} From b6e443e770f837461d01d32803c1812d756a519c Mon Sep 17 00:00:00 2001 From: nh758 <7259@pm.me> Date: Thu, 24 Oct 2024 14:45:41 +0700 Subject: [PATCH 021/129] test the team filter --- .../ABViewOrgChartTeamsComponent.js | 40 ++++++++++--------- .../viewComponent/ABViewOrgChartTeams.test.js | 30 ++++++++++++++ 2 files changed, 52 insertions(+), 18 deletions(-) diff --git a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js index 1a51a263..5fa9b515 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js @@ -200,6 +200,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { chartData._rawData = topNode; const maxDepth = 10; // prevent inifinite loop + const self = this; function pullChildData(node, prefixFn, depth = 0) { if (depth >= maxDepth) return; node.children = []; @@ -211,29 +212,15 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { const strategyClass = `strategy-${code}`; const child = { name: childData[teamName], - id: prefixFn(id), + id: self.teamNodeID(id), className: strategyClass, _rawData: childData, }; - // Apply filters (match using or) - if (filters.strategy || filters.teamName) { - child.filteredOut = true; - if (filters.strategy !== "" && filters.strategy === code) { - child.filteredOut = false; - } - if ( - filters.teamName !== "" && - child.name - .toLowerCase() - .includes(filters.teamName.toLowerCase()) - ) { - child.filteredOut = false; - } - } + child.filteredOut = self.filterTeam(filters, child, code); if (child.name === "External Support") child.className = `strategy-external`; if (childData[teamLink].length > 0) { - pullChildData(child, prefixFn, depth + 1); + pullChildData(child, depth + 1); } // If this node is filtered we still need it if it has children // that pass @@ -251,7 +238,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { } return; } - pullChildData(chartData, this.teamNodeID); + pullChildData(chartData); } get chartData() { @@ -324,6 +311,23 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { this.pullData(filters).then(() => this.displayOrgChart()); } + filterTeam(filters, team, code) { + // Apply filters (match using or) + if (filters.strategy || filters.teamName) { + let filter = true; + if (filters.strategy !== "" && filters.strategy === code) { + filter = false; + } + if ( + filters.teamName !== "" && + team.name.toLowerCase().includes(filters.teamName.toLowerCase()) + ) { + filter = false; + } + return filter; + } + } + /** * Get the ABField from settings * @param {string} setting key in this.view.settings - should be an id for an diff --git a/test/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeams.test.js b/test/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeams.test.js index 4b2ee423..319463ab 100644 --- a/test/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeams.test.js +++ b/test/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeams.test.js @@ -116,6 +116,36 @@ describe("ABViewDetailCheckboxComponent item widget", function () { assert.equal(teamChart.chartData.className, "strategy-test"); }); + it(".filterTeam()", function () { + [ + { + filters: {}, + result: undefined, + }, + { + filters: { strategy: "OPS", teamName: "" }, + result: false, + }, + { + filters: { strategy: "SLM", teamName: "" }, + result: true, + }, + { + filters: { strategy: "SLM", teamName: "X" }, + result: false, + }, + { + filters: { teamName: "Y" }, + result: true, + }, + ].forEach((c) => { + assert.equal( + teamChart.filterTeam(c.filters, { name: "Team X" }, "OPS"), + c.result + ); + }); + }); + describe(".teamAddChild", function () { const values = { teamName: "Test" }; From 4d743fe09de35cd783499ce1326d2509cddb434e Mon Sep 17 00:00:00 2001 From: nh758 <7259@pm.me> Date: Thu, 24 Oct 2024 16:14:22 +0700 Subject: [PATCH 022/129] apply strategy style on add/edit team --- .../ABViewOrgChartTeamsComponent.js | 27 +++++++++++++------ 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js index 5fa9b515..c300556a 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js @@ -201,7 +201,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { const maxDepth = 10; // prevent inifinite loop const self = this; - function pullChildData(node, prefixFn, depth = 0) { + function pullChildData(node, depth = 0) { if (depth >= maxDepth) return; node.children = []; node._rawData[teamLink].forEach((id) => { @@ -352,7 +352,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { document.getElementsByTagName("head")[0].appendChild(style); } - async teamAddChild(values) { + async teamAddChild(values, strategy) { const { id } = await this.datacollection.model.create(values); const linkField = this.AB.definitionByID( @@ -367,6 +367,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { name: values[nameField], id: this.teamNodeID(id), relationship: hasChild ? "110" : "100", + className: `strategy-${strategy.text}`, }; // Need to add differently if the node already has child nodes if (hasChild) { @@ -375,9 +376,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { .querySelector(".node"); this.__orgchart.addSiblings(sibling, { siblings: [newChild] }); } else { - this.__orgchart.addChildren(parent, { - children: [newChild], - }); + this.__orgchart.addChildren(parent, { children: [newChild] }); } } @@ -421,12 +420,20 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { }); } - teamEdit(values) { + teamEdit(values, strategy) { this.datacollection.model.update(values.id, values).catch((err) => { //TODO }); const nodeID = this.teamNodeID(values.id); const node = document.querySelector(`#${nodeID}`); + const currentStrategy = node.classList.value.match(/strategy-\S+/)[0]; + const newStrategy = `strategy-${strategy.text}`; + if (currentStrategy !== newStrategy) { + node.classList.remove(currentStrategy); + node.classList.add(newStrategy); + } + + console.log(node.classList); const inactive = this.getSettingField("teamInactive").columnName; // @TODO this will need to check against active filters if (values[inactive]) { @@ -504,10 +511,14 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { css: "webix_primary", click: () => { const values = $$(this.ids.teamForm).getValues(); + const strategy = strategyOptions.find( + (f) => + f.id === values[strategyField.columnName] + ); if (values.id) { - this.teamEdit(values); + this.teamEdit(values, strategy); } else { - this.teamAddChild(values); + this.teamAddChild(values, strategy); } $teamFormPopup.hide(); }, From f333d0aa5440f035d5c9435666ebe77c775f717e Mon Sep 17 00:00:00 2001 From: nh758 <7259@pm.me> Date: Fri, 25 Oct 2024 10:41:57 +0700 Subject: [PATCH 023/129] fix unit tests --- .../ABViewOrgChartTeamsComponent.js | 35 +++++++++++++------ styles/team-widget.css | 20 +++++++++++ .../viewComponent/ABViewOrgChartTeams.test.js | 20 ++++++++--- 3 files changed, 60 insertions(+), 15 deletions(-) diff --git a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js index c300556a..dcf80746 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js @@ -20,6 +20,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { ids ) ); + this.__filters = {}; } ui() { @@ -98,7 +99,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { parentNodeSymbol: false, exportButton: baseView.settings.export, exportFilename: baseView.settings.exportFilename, - createNode: ($node, { id, filteredOut }) => { + createNode: ($node, { id, filteredOut, isInactive }) => { // remove built in icon $node.querySelector(".title > i")?.remove(); // customize @@ -133,6 +134,14 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { $deleteButton.onclick = () => this.teamDelete(values); $buttons.append($deleteButton); } + if (this.__filters.inactive && this.__filters.inactive === 1) { + 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); + } $content.append($leaderSection, $memberSection, $buttons); }, @@ -170,7 +179,8 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { } } - async pullData(filters = {}) { + async pullData() { + const filters = this.__filters; const view = this.view; const dc = view.datacollection; await dc?.waitForDataCollectionToInitialize(dc); @@ -197,6 +207,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { chartData.className = `strategy-${ topNode[`${strategy}__relation`]?.[strategyCode] }`; + chartData.isInactive = topNode[teamInactive]; chartData._rawData = topNode; const maxDepth = 10; // prevent inifinite loop @@ -214,6 +225,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { name: childData[teamName], id: self.teamNodeID(id), className: strategyClass, + isInactive: childData[teamInactive], _rawData: childData, }; child.filteredOut = self.filterTeam(filters, child, code); @@ -307,8 +319,8 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { filterApply() { $$(this.ids.filterPopup).hide(); - const filters = $$(this.ids.filterForm).getValues(); - this.pullData(filters).then(() => this.displayOrgChart()); + this.__filters = $$(this.ids.filterForm).getValues(); + this.pullData().then(() => this.displayOrgChart()); } filterTeam(filters, team, code) { @@ -369,6 +381,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { relationship: hasChild ? "110" : "100", className: `strategy-${strategy.text}`, }; + // Need to add differently if the node already has child nodes if (hasChild) { const sibling = this.closest(parent, (el) => el.nodeName === "TABLE") @@ -426,17 +439,19 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { }); const nodeID = this.teamNodeID(values.id); const node = document.querySelector(`#${nodeID}`); - const currentStrategy = node.classList.value.match(/strategy-\S+/)[0]; + const currentStrategy = node.classList?.value?.match(/strategy-\S+/)[0]; const newStrategy = `strategy-${strategy.text}`; if (currentStrategy !== newStrategy) { - node.classList.remove(currentStrategy); - node.classList.add(newStrategy); + node.classList?.remove(currentStrategy); + node.classList?.add(newStrategy); } - console.log(node.classList); const inactive = this.getSettingField("teamInactive").columnName; - // @TODO this will need to check against active filters - if (values[inactive]) { + if ( + this.__filters.inactive && + this.__filters.inactive === 0 && + values[inactive] + ) { this.__orgchart.removeNodes(node); } const nameCol = this.getSettingField("teamName").columnName; diff --git a/styles/team-widget.css b/styles/team-widget.css index 7fcbbec1..990594d4 100644 --- a/styles/team-widget.css +++ b/styles/team-widget.css @@ -108,6 +108,7 @@ org-chart tr.lines .downLine { padding: 3px; gap: 6px; justify-content: flex-end; + margin-right: 5px; } .team-button { @@ -163,3 +164,22 @@ org-chart tr.lines .downLine { background: #FFF; box-shadow: 0px 4px 4px 0px rgba(0, 0, 0, 0.25); } + +.active-text { + color: #ffffff; + font-family: 'Jomhuria', sans-serif; + height: 6.5px; + font-size: 8px; + font-weight: 400; + text-align: center; +} + +.is-active { + background: #4bc90f; + cursor: default; +} + +.is-inactive { + background: grey; + cursor: default; +} diff --git a/test/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeams.test.js b/test/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeams.test.js index 319463ab..3bf1ba91 100644 --- a/test/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeams.test.js +++ b/test/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeams.test.js @@ -161,23 +161,33 @@ describe("ABViewDetailCheckboxComponent item widget", function () { it("adds a team node as child", async function () { selectorStub.returns({ parentNode: { colSpan: 0 } }); - await teamChart.teamAddChild(values); + await teamChart.teamAddChild(values, { text: "1" }); assert(modelCreate.calledOnce); assert(teamChart.__orgchart.addChildren.calledOnce); assert.deepEqual( teamChart.__orgchart.addChildren.lastArg.children[0], - { relationship: "100", name: "Test", id: "teamnode_new" } + { + relationship: "100", + name: "Test", + id: "teamnode_new", + className: "strategy-1", + } ); }); it("adds a team node as sibling", async function () { selectorStub.returns({ parentNode: { colSpan: 2 } }); - await teamChart.teamAddChild(values); + await teamChart.teamAddChild(values, { text: "1" }); assert(modelCreate.calledOnce); assert(teamChart.__orgchart.addSiblings.calledOnce); assert.deepEqual( teamChart.__orgchart.addSiblings.lastArg.siblings[0], - { relationship: "110", name: "Test", id: "teamnode_new" } + { + relationship: "110", + name: "Test", + id: "teamnode_new", + className: "strategy-1", + } ); }); }); @@ -270,7 +280,7 @@ describe("ABViewDetailCheckboxComponent item widget", function () { selectorStub.returnsThis(); const values = { id: "update", teamName: "update" }; - teamChart.teamEdit(values); + teamChart.teamEdit(values, {}); assert( teamChart.datacollection.model.update.calledOnceWith("update", values) ); From b75d0272cd01a7b57c77007c144402f717193bde Mon Sep 17 00:00:00 2001 From: guyyoo Date: Mon, 28 Oct 2024 10:41:15 +0700 Subject: [PATCH 024/129] WIP --- AppBuilder/platform/ql/ABQL.js | 12 +- .../ABViewOrgChartTeamsComponent.js | 200 +++++++++++------- styles/team-widget.css | 164 +++++++------- 3 files changed, 217 insertions(+), 159 deletions(-) diff --git a/AppBuilder/platform/ql/ABQL.js b/AppBuilder/platform/ql/ABQL.js index 09350273..270ade15 100644 --- a/AppBuilder/platform/ql/ABQL.js +++ b/AppBuilder/platform/ql/ABQL.js @@ -589,13 +589,11 @@ class ABQL extends ABQLCore { id: this.ids.shorthand, view: "button", label: displayLabel, - on: { - onItemClick: () => { - Filter.popUp($$(this.ids.shorthand), null, { - pos: "center", - }); - }, - }, + click() { + Filter.popUp(this.$view, null, { + pos: "center", + }); + } }, // have a hidden field to contain the condition // value we will parse out later diff --git a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js index 0717cd74..1cb5cce1 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js @@ -74,7 +74,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { this.busy(); await this.loadOrgChartJs(); await this.pullData(); - this.displayOrgChart(); + await this.displayOrgChart(); this.ready(); } @@ -84,7 +84,10 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { const chartData = AB.cloneDeep(this.chartData); const settings = baseView.settings; const draggable = settings.draggable === 1; - const nodeObj = baseView.datacollection?.datasource; + const dropContentToCreate = settings.$dropContentToCreate === 1; + const nodeDC = baseView.datacollection; + const nodeModel = baseView.datacollection.model; + const nodeObj = nodeDC?.datasource; const nodeObjPK = nodeObj.PK(); const contentField = nodeObj.fieldByID(settings.contentField); const contentFieldColumnName = contentField.columnName; @@ -111,6 +114,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { rule: "in", value: contentDataRecordPKs, }, + JSON.parse(settings.contentFieldFilter), ], }, populate: true, @@ -126,45 +130,90 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { const contentObjID = contentObj.id; const contentDisplayedFields = settings.contentDisplayedFields; const contentDisplayedFieldsKeys = Object.keys(contentDisplayedFields); + const contentModel = contentObj.model(); const orgchart = new this.OrgChart({ + data: chartData, + direction: baseView.settings.direction, + // depth: baseView.settings.depth, + chartContainer: `#${this.ids.chartDom}`, + pan: true, // baseView.settings.pan == 1, + zoom: true, //baseView.settings.zoom == 1, + draggable, + // visibleLevel: baseView.settings.visibleLevel, + parentNodeSymbol: false, + exportButton: baseView.settings.export, + exportFilename: baseView.settings.exportFilename, createNode: async ($node, data) => { - const nodeWidth = 300; - const nodeStyle = $node.style; - nodeStyle["width"] = `${nodeWidth}px`; + // remove built in icon + $node.querySelector(".title > i")?.remove(); + + // customize const $contentNode = $node.children.item(1); $contentNode.innerHTML = ""; - const contentNodeStyle = $contentNode.style; - - // Team content buckets - contentNodeStyle["height"] = `${ - // TODO (Guy): Fix the number later. - contentGroupOptionsLength * 150 - }px`; - const groupBorderWidth = 5; - contentNodeStyle["width"] = `${nodeWidth - 2 * groupBorderWidth}px`; - const averageHeight = 100 / contentGroupOptionsLength; + const averageHeight = 80 / contentGroupOptionsLength; const currentNodeDataRecordPK = data._rawData[nodeObjPK]; + const $nodeSpacer = element("div", "spacer"); + $contentNode.appendChild($nodeSpacer); + $nodeSpacer.style.backgroundColor = contentGroupOptions[0].hex; for (const group of contentGroupOptions) { - const $group = document.createElement("div"); + const $group = element("div", "team-group-section"); $contentNode.appendChild($group); const groupStyle = $group.style; groupStyle["height"] = `${averageHeight}%`; - groupStyle["borderStyle"] = "solid"; - groupStyle["borderWidth"] = `${groupBorderWidth}px`; const groupColor = group.hex; - groupStyle["borderColor"] = groupColor; - const $groupTitle = document.createElement("div"); - const groupTitleStyle = $groupTitle.style; - groupTitleStyle["backgroundColor"] = groupColor; - groupTitleStyle["height"] = "20%"; + groupStyle["backgroundColor"] = groupColor; + const $groupTitle = element("div", "team-group-title"); + // const groupTitleStyle = $groupTitle.style; + // groupTitleStyle["backgroundColor"] = groupColor; + // groupTitleStyle["height"] = "20%"; const groupText = group.text; - $groupTitle.appendChild(document.createTextNode(groupText)); - $group.appendChild($groupTitle); - const $groupContent = document.createElement("div"); + // $groupTitle.setAttribute( + // "id", + // `${currentNodeDataRecordPK}.${groupText}` + // ); + // $groupTitle.appendChild(document.createTextNode(groupText)); + // $group.appendChild($groupTitle); + const $groupContent = element("div", "team-group-content"); $group.appendChild($groupContent); - const groupContentStyle = $groupContent.style; - groupContentStyle["overflow"] = "auto"; - groupContentStyle["height"] = "80%"; + if (draggable) { + $group.addEventListener("drop", async (event) => { + const elementID = event.dataTransfer.getData("element-id"); + if (elementID.includes("teamnode")) return; + const draggedContentDataRecord = JSON.parse( + document.getElementById(elementID).dataset.source + ); + if (dropContentToCreate) { + // Trigger a process manager. + await contentModel.update( + draggedContentDataRecord.id, + draggedContentDataRecord + ); + delete draggedContentDataRecord["id"]; + delete draggedContentDataRecord["uuid"]; + delete draggedContentDataRecord["created_at"]; + delete draggedContentDataRecord["updated_at"]; + draggedContentDataRecord[contentFieldLinkColumnName] = + currentNodeDataRecordPK; + draggedContentDataRecord[ + contentGroupByFieldColumnName + ] = groupText; + await contentModel.create(draggedContentDataRecord); + } else { + draggedContentDataRecord[contentFieldLinkColumnName] = + currentNodeDataRecordPK; + draggedContentDataRecord[ + contentGroupByFieldColumnName + ] = groupText; + await contentModel.update( + draggedContentDataRecord.id, + draggedContentDataRecord + ); + } + + // TODO (Guy): This is refreshing the whole chart. + await this.onShow(); + }); + } let contentDataRecordIndex = 0; while ( contentDataRecordIndex < contentDataRecords.length && @@ -182,15 +231,30 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { continue; } contentDataRecords.splice(contentDataRecordIndex, 1); + for (const key in contentDataRecord) + key.includes("__relation") && + delete contentDataRecord[key]; const contentDataRecordPK = contentDataRecord[contentObjPK]; const rowDataID = `${currentNodeDataRecordPK}.${contentDataRecordPK}`; - const $rowData = document.createElement("div"); + const $rowData = element("div", "team-group-record"); + $rowData.setAttribute( + "data-source", + JSON.stringify(contentDataRecord) + ); $groupContent.appendChild($rowData); $rowData.setAttribute("id", rowDataID); - draggable && $rowData.setAttribute("draggable", "true"); + if (draggable) { + $rowData.setAttribute("draggable", "true"); + $rowData.addEventListener("dragstart", (e) => { + e.dataTransfer.setData("element-id", e.target.id); + e.target.style.opacity = "0.5"; + }); + $rowData.addEventListener("dragend", (e) => { + e.target.style.opacity = "1"; + }); + } const rowDataStyle = $rowData.style; - rowDataStyle["borderStyle"] = "solid"; - rowDataStyle["borderColor"] = "black"; + rowDataStyle["borderColor"] = "#EF3340"; let currentDataRecords = []; let currentField = null; for (let j = 0; j < contentDisplayedFieldsKeys.length; j++) { @@ -247,12 +311,17 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { currentField = displayedField; continue; } - const $currentDisplay = document.createElement("div"); + const $currentDisplay = element( + "div", + "team-group-record-display" + ); $rowData.appendChild($currentDisplay); const displayedFieldColumnName = displayedField.columnName; while (currentDataRecords.length > 0) { - const $currentDisplayData = - document.createElement("div"); + const $currentDisplayData = element( + "div", + "team-group-record-display-data" + ); $currentDisplay.appendChild($currentDisplayData); const currentDataRecord = currentDataRecords.pop(); $currentDisplayData.setAttribute( @@ -269,33 +338,15 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { } } } - }, - data: chartData, - direction: baseView.settings.direction, - // depth: baseView.settings.depth, - chartContainer: `#${this.ids.chartDom}`, - pan: true, // baseView.settings.pan == 1, - zoom: true, //baseView.settings.zoom == 1, - draggable, - // visibleLevel: baseView.settings.visibleLevel, - parentNodeSymbol: false, - exportButton: baseView.settings.export, - exportFilename: baseView.settings.exportFilename, - createNode: ($node, { id }) => { - // remove built in icon - $node.querySelector(".title > i")?.remove(); - // customize - const $content = $node.querySelector(".content"); - $content.innerHTML = ""; - const $leaderSection = element("div", "team-leader-section"); - const $memberSection = element("div", "team-member-section"); + // const $leaderSection = element("div", "team-leader-section"); + // const $memberSection = element("div", "team-member-section"); const $buttons = element("div", "team-button-section"); 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(id); + const dataID = this.teamRecordID(data.id); const values = this.datacollection.getData( (e) => e.id === dataID )[0]; @@ -303,14 +354,14 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { this.teamForm("Add", { __parentID: dataID }); }; $editButton.onclick = () => 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); } - $content.append($leaderSection, $memberSection, $buttons); + // $contentNode.append($leaderSection, $memberSection, $buttons); + $contentNode.appendChild($buttons); }, nodeContent: "description", @@ -320,12 +371,12 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { if (draggable) { // On drop update the parent (dropZone) of the node - orgchart.addEventListener("nodedropped.orgchart", (event) => { - const dragNode = JSON.parse( - event.detail.draggedNode.dataset.source - ); - debugger; - const dropNode = JSON.parse(event.detail.dropZone.dataset.source); + orgchart.addEventListener("nodedropped.orgchart", async (event) => { + const eventDetail = event.detail; + const $draggedNode = eventDetail.draggedNode; + if (!$draggedNode.getAttribute("id").includes("teamnode")) return; + const dragNode = JSON.parse($draggedNode.dataset.source); + const dropNode = JSON.parse(eventDetail.dropZone.dataset.source); const dragRecord = dragNode._rawData; const dropID = dropNode._rawData.id; @@ -334,22 +385,21 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { linkField.settings.linkColumn ); dragRecord[parent.columnName] = dropID; - - this.datacollection.model.update(dragRecord.id, dragRecord); + await nodeModel.update(dragRecord.id, dragRecord); }); } const chartDom = document.querySelector(`#${this.ids.chartDom}`); - const orgchartStyle = orgchart.style; - orgchartStyle["overflow"] = "auto"; - let $currentNode = chartDom; - while ($currentNode.style["height"] === "") - $currentNode = $currentNode.parentNode; - orgchartStyle["height"] = $currentNode.style["height"]; + // const orgchartStyle = orgchart.style; + // orgchartStyle["overflow"] = "auto"; + // let $currentNode = chartDom; + // while ($currentNode.style["height"] === "") + // $currentNode = $currentNode.parentNode; + // orgchartStyle["height"] = $currentNode.style["height"]; if (chartDom) { chartDom.textContent = ""; chartDom.innerHTML = ""; chartDom.appendChild(orgchart); - this.toolbarUi(chartDom); + // this.toolbarUi(chartDom); } } @@ -372,7 +422,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { const teamName = this.getSettingField("teamName").columnName; const teamInactive = this.getSettingField("teamInactive").columnName; - const chartData = this.chartData; + const chartData = (this._chartData = {}); chartData.name = topNode[teamName] ?? ""; chartData.id = this.teamNodeID(topNode.id); chartData._rawData = topNode; diff --git a/styles/team-widget.css b/styles/team-widget.css index d2b4fd86..a9839856 100644 --- a/styles/team-widget.css +++ b/styles/team-widget.css @@ -1,105 +1,117 @@ -@import url('https://fonts.googleapis.com/css2?family=Jomhuria&display=swap'); +@import url("https://fonts.googleapis.com/css2?family=Jomhuria&display=swap"); org-chart .node { - display: inline-block; - position: relative; - margin: 5px; - padding: 5px; - border: 2px dashed transparent; - text-align: center; - width: 164px; + position: relative; + border: 2px dashed transparent; + text-align: center; + justify-content: center; + width: 325px !important; + height: 250px; + font-family: "Jomhuria", sans-serif; + font-style: normal; + font-weight: 200; + font-size: 16px; + color: #000000; +} + +org-chart .node .spacer { + border: none; + width: 100%; + height: 10%; } org-chart .node .title { - text-overflow: ellipsis; - white-space: nowrap; + position: relative; display: flex; flex-direction: row; justify-content: center; align-items: center; - padding: 6px 69px; - gap: 10px; - background: #EF3340; + background: #ef3340; border-radius: 15px; - width: 154px; - height: 32px; - font-family: 'Jomhuria', sans-serif; - font-style: normal; + width: 100%; + height: 15%; font-weight: 400; - font-size: 24px; - line-height: 24px; - color: #FFFFFF; - flex: none; - order: 0; - flex-grow: 0; - position: relative; + font-size: 32px; + color: #ffffff; z-index: 1; } org-chart .node .content { position: relative; - top: -15px; - width: 154px; - min-height: 60px; + top: -7.5%; + width: 100%; + height: 85%; border-radius: 0 0 15px 15px; display: flex; flex-direction: column; - align-items: flex-end; - flex-wrap: nowrap; - gap: 4px; margin: 0 auto; background: #dddddd; - overflow: hidden; box-shadow: 0 4px 4px 0 rgba(0, 0, 0, 0.25); } org-chart tr.lines .topLine { - border-top: 2px solid black; + border-top: 2px solid black; } org-chart tr.lines .rightLine { - border-right: 1px solid black; - float: none; - border-radius: 0; + border-right: 1px solid black; + float: none; + border-radius: 0; } org-chart tr.lines .leftLine { - border-left: 1px solid black; - float: none; - border-radius: 0; + border-left: 1px solid black; + float: none; + border-radius: 0; } org-chart tr.lines .downLine { - background-color: black; - margin: 0 auto; - height: 20px; - width: 2px; - float: none; + background-color: black; + margin: 0 auto; + height: 20px; + width: 2px; + float: none; } +.team-group-section { + width: 100%; + padding: 2%; +} -.team-leader-section { - width: 154px; - min-height: 35px; - left: 0px; - top: 14px; - background: #1A3E72; - display: flex; - align-self: stretch; - flex-wrap: nowrap; - gap: 5px; - /* position: relative; */ +.team-group-title { + width: 100%; } -.team-member-section { +.team-group-content { + height: 80%; + width: 100%; display: flex; flex-direction: column; - align-items: flex-end; - flex-wrap: nowrap; - gap: 4px; - /* position: relative; */ - width: 154px; - min-height: 21px; + gap: 5%; + overflow: auto; +} + +.team-group-record { + width: 100%; + border-width: 1%; + border-style: solid; + border-radius: 5px; + background: #ffffff; + display: flex; + flex-direction: row; + justify-content: center; +} + +.team-group-record-display { + +} + +.team-group-record-display-data { + +} + +.team-group-record:hover { + background-color: yellow; } .team-button-section { @@ -111,19 +123,19 @@ org-chart tr.lines .downLine { } .team-button { - display: flex; - align-items: center; - justify-content: center; - gap: 2px; - width: 28px; - height: 10px; - padding: 2px 4px; - background: #1A3E72; - border: none; - border-radius: 5px; - cursor: pointer; - font-size: 8px; - color: white; + display: flex; + align-items: center; + justify-content: center; + gap: 2px; + width: 28px; + height: 10px; + padding: 2px 4px; + background: #1a3e72; + border: none; + border-radius: 5px; + cursor: pointer; + font-size: 8px; + color: white; } .team-chart-toolbar { @@ -134,12 +146,10 @@ org-chart tr.lines .downLine { gap: 10px; width: 254px; - background: #FFFFFF; + background: #ffffff; box-shadow: 0px -1px 4px rgba(0, 0, 0, 0.25); border-radius: 20px; } .team-chart-button { - } - From 36b1e0ec091f7d7bd46d238888f3445e9224bc01 Mon Sep 17 00:00:00 2001 From: guyyoo Date: Tue, 29 Oct 2024 13:23:34 +0700 Subject: [PATCH 025/129] Fix the dropToCreate bug --- .../views/viewComponent/ABViewOrgChartTeamsComponent.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js index 669e3378..125ba837 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js @@ -88,7 +88,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { const chartData = AB.cloneDeep(this.chartData); const settings = baseView.settings; const draggable = settings.draggable === 1; - const dropContentToCreate = settings.$dropContentToCreate === 1; + const dropContentToCreate = settings.dropContentToCreate === 1; const nodeDC = baseView.datacollection; const nodeModel = baseView.datacollection.model; const nodeObj = nodeDC?.datasource; @@ -221,7 +221,9 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { } // TODO (Guy): This is refreshing the whole chart. - await this.onShow(); + setTimeout(async () => { + await this.onShow(); + }, 1000); }); } let contentDataRecordIndex = 0; From f9a5d464c6ba612007c1734d590d883f2b5981f4 Mon Sep 17 00:00:00 2001 From: guyyoo Date: Wed, 30 Oct 2024 11:11:15 +0700 Subject: [PATCH 026/129] WIP --- .../ABViewOrgChartTeamsComponent.js | 39 ++++++++++++++----- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js index 125ba837..e03cfbc5 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js @@ -135,11 +135,12 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { const contentDisplayedFields = settings.contentDisplayedFields; const contentDisplayedFieldsKeys = Object.keys(contentDisplayedFields); const contentModel = contentObj.model(); + const ids = this.ids; const orgchart = new this.OrgChart({ data: chartData, direction: baseView.settings.direction, // depth: baseView.settings.depth, - chartContainer: `#${this.ids.chartDom}`, + chartContainer: `#${ids.chartDom}`, pan: true, // baseView.settings.pan == 1, zoom: true, //baseView.settings.zoom == 1, draggable, @@ -192,6 +193,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { const draggedContentDataRecord = JSON.parse( document.getElementById(elementID).dataset.source ); + orgchart.innerHTML = ""; if (dropContentToCreate) { // Trigger a process manager. await contentModel.update( @@ -406,18 +408,37 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { await nodeModel.update(dragRecord.id, dragRecord); }); } - const chartDom = document.querySelector(`#${this.ids.chartDom}`); - // const orgchartStyle = orgchart.style; - // orgchartStyle["overflow"] = "auto"; - // let $currentNode = chartDom; - // while ($currentNode.style["height"] === "") - // $currentNode = $currentNode.parentNode; - // orgchartStyle["height"] = $currentNode.style["height"]; + const chartDom = document.querySelector(`#${ids.chartDom}`); if (chartDom) { chartDom.textContent = ""; chartDom.innerHTML = ""; this.initFilter(chartDom); - chartDom.appendChild(orgchart); + const $chartContent = AB.Webix.ui({cols: [{ + view: "template", + },{ + view: "tabview", + width: 450, + cells: [ + { + header: "Placed", + body: { + id: "listView", + view: "list", + // list config + } + }, + { + header: "Unplaced", + body: { + id: "listView", + view: "list", + // list config + } + } + ] + }]}).$view; + $chartContent.children[0].children[0].appendChild(orgchart) + chartDom.appendChild($chartContent); } } From 9b0d982fac5421941322ba53f38df21378af88b7 Mon Sep 17 00:00:00 2001 From: Johnny Date: Sat, 2 Nov 2024 23:49:40 -0500 Subject: [PATCH 027/129] [wip] Netsuite objects respond to an Object Info request --- AppBuilder/platform/ABObjectApiNetsuite.js | 65 ++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/AppBuilder/platform/ABObjectApiNetsuite.js b/AppBuilder/platform/ABObjectApiNetsuite.js index 02df1187..4fabb341 100644 --- a/AppBuilder/platform/ABObjectApiNetsuite.js +++ b/AppBuilder/platform/ABObjectApiNetsuite.js @@ -4,4 +4,69 @@ 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; + } }; From c53c5886de739cb1fd7700a2f84f476e323e03d5 Mon Sep 17 00:00:00 2001 From: Johnny Date: Sat, 2 Nov 2024 23:50:45 -0500 Subject: [PATCH 028/129] [fix] comparison between old and new values to be reduced to PKs --- .../viewComponent/ABViewGridComponent.js | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/AppBuilder/platform/views/viewComponent/ABViewGridComponent.js b/AppBuilder/platform/views/viewComponent/ABViewGridComponent.js index 876e09cd..9376663c 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewGridComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewGridComponent.js @@ -1356,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": @@ -1375,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; From d0cc8d20571a9d2041f250c946de00611fe3ea44 Mon Sep 17 00:00:00 2001 From: Johnny Date: Sat, 2 Nov 2024 23:52:20 -0500 Subject: [PATCH 029/129] [sync] with core --- AppBuilder/core | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AppBuilder/core b/AppBuilder/core index 9a423912..d59ae403 160000 --- a/AppBuilder/core +++ b/AppBuilder/core @@ -1 +1 @@ -Subproject commit 9a42391208bddbd65e1c481630378b3333ed99a0 +Subproject commit d59ae40353c646e9166217f1a7fdaf31b4a79d3e From 183c4474c3625297ddb274242465ff2a7b9c3162 Mon Sep 17 00:00:00 2001 From: guyyoo Date: Mon, 18 Nov 2024 12:40:37 +0700 Subject: [PATCH 030/129] Drag and drop content --- .../ABViewOrgChartTeamsComponent.js | 403 +++++++++++------- 1 file changed, 246 insertions(+), 157 deletions(-) diff --git a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js index e03cfbc5..ea361253 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js @@ -17,8 +17,8 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { teamFormTitle: "", teamFormInactive: "", }, - ids - ) + ids, + ), ); this.__filters = {}; } @@ -87,6 +87,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { const AB = this.AB; const chartData = AB.cloneDeep(this.chartData); const settings = baseView.settings; + const showGroupTitle = settings.showGroupTitle === 1; const draggable = settings.draggable === 1; const dropContentToCreate = settings.dropContentToCreate === 1; const nodeDC = baseView.datacollection; @@ -94,48 +95,129 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { const nodeObj = nodeDC?.datasource; const nodeObjPK = nodeObj.PK(); const contentField = nodeObj.fieldByID(settings.contentField); - const contentFieldColumnName = contentField.columnName; - const contentFieldLink = contentField.fieldLink; - const contentObj = contentFieldLink.object; - const contentObjPK = contentObj.PK(); + const contentFieldColumnName = contentField?.columnName; + const contentFieldLink = contentField?.fieldLink; + const contentObj = contentFieldLink?.object; + const contentObjPK = contentObj?.PK(); const contentDataRecordPKs = []; - const getContentDataRecordPKs = (node) => { - const contentFieldData = node._rawData[contentFieldColumnName]; - if (Array.isArray(contentFieldData)) - contentDataRecordPKs.push(...contentFieldData); - else contentDataRecordPKs.push(contentFieldData); - const children = node.children || []; - for (const child of children) getContentDataRecordPKs(child); - }; - getContentDataRecordPKs(chartData); - const contentDataRecords = ( - await contentObj.model().findAll({ - where: { - glue: "and", - rules: [ - { - key: contentObjPK, - rule: "in", - value: contentDataRecordPKs, + const contentDataRecords = []; + if (contentField != null && contentObj != null) { + const getContentDataRecordPKs = (node) => { + const contentFieldData = node._rawData[contentFieldColumnName]; + if (Array.isArray(contentFieldData)) + contentDataRecordPKs.push(...contentFieldData); + else contentDataRecordPKs.push(contentFieldData); + const children = node.children || []; + for (const child of children) getContentDataRecordPKs(child); + }; + getContentDataRecordPKs(chartData); + contentDataRecords.push( + ...( + await contentObj.model().findAll({ + where: { + glue: "and", + rules: [ + { + key: contentObjPK, + rule: "in", + value: contentDataRecordPKs, + }, + JSON.parse(settings.contentFieldFilter), + ], }, - JSON.parse(settings.contentFieldFilter), - ], - }, - populate: true, - }) - ).data; - const contentGroupByField = contentObj.fieldByID( - settings.contentGroupByField + populate: true, + }) + ).data, + ); + } + const contentGroupByField = contentObj?.fieldByID( + settings.contentGroupByField, ); - const contentGroupOptions = contentGroupByField.settings.options; + const contentGroupOptions = contentGroupByField?.settings.options || []; const contentGroupOptionsLength = contentGroupOptions.length; - const contentGroupByFieldColumnName = contentGroupByField.columnName; - const contentFieldLinkColumnName = contentFieldLink.columnName; - const contentObjID = contentObj.id; + const contentGroupByFieldColumnName = contentGroupByField?.columnName; + const contentFieldLinkColumnName = contentFieldLink?.columnName; + const contentObjID = contentObj?.id; const contentDisplayedFields = settings.contentDisplayedFields; const contentDisplayedFieldsKeys = Object.keys(contentDisplayedFields); - const contentModel = contentObj.model(); + const contentModel = contentObj?.model(); const ids = this.ids; + const callAfterRender = (callback) => { + requestAnimationFrame(() => { + requestAnimationFrame(callback); + }); + }; + const fnContentDragStart = (event) => { + event.stopPropagation(); + const $eventTarget = event.target; + const dataset = $eventTarget.dataset; + const dataTransfer = event.dataTransfer; + switch ($eventTarget.className) { + case "webix_list_item": + dataTransfer.setData("dataPK", dataset.pk); + dataTransfer.setData( + "contentLinkedFieldId", + dataset.contentLinkedFieldId, + ); + break; + default: + dataTransfer.setData("source", dataset.source); + break; + } + // $eventTarget.style.opacity = "0.5"; + }; + const fnContentDragOver = (event) => { + event.preventDefault(); + event.stopPropagation(); + }; + const fnContentDragEnd = (event) => { + // event.target.style.opacity = "1"; + }; + const fnContentDrop = async (event) => { + event.stopPropagation(); + if (contentFieldLinkColumnName == null) return; + const $group = event.currentTarget; + const newGroupText = $group.dataset.text; + const newNodeDataPK = JSON.parse( + $group.parentElement.parentElement.dataset.source, + )._rawData[nodeObjPK]; + const dataTransfer = event.dataTransfer; + let updatedData = dataTransfer.getData("source"); + this.__orgchart.innerHTML = ""; + if (updatedData === "") { + const dataPK = dataTransfer.getData("dataPK"); + const contentLinkedFieldID = dataTransfer.getData( + "contentLinkedFieldId", + ); + const contentLinkedField = + contentObj.fieldByID(contentLinkedFieldID); + updatedData = {}; + updatedData[contentLinkedField.columnName] = dataPK; + updatedData[contentFieldLinkColumnName] = newNodeDataPK; + updatedData[contentGroupByFieldColumnName] = newGroupText; + await contentModel.create(updatedData); + } else { + if (dropContentToCreate) { + updatedData = JSON.parse(updatedData); + delete updatedData["id"]; + delete updatedData["uuid"]; + delete updatedData["created_at"]; + delete updatedData["updated_at"]; + updatedData[contentFieldLinkColumnName] = newNodeDataPK; + updatedData[contentGroupByFieldColumnName] = newGroupText; + await contentModel.create(updatedData); + } else { + updatedData[contentFieldLinkColumnName] = newNodeDataPK; + updatedData[contentGroupByFieldColumnName] = newGroupText; + await contentModel.update(updatedData.id, updatedData); + } + } + // TODO (Guy): This is refreshing the whole chart. + await this.onShow(); + // setTimeout(async () => { + // await this.onShow(); + // }, 1000); + }; const orgchart = new this.OrgChart({ data: chartData, direction: baseView.settings.direction, @@ -155,12 +237,14 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { // customize const $content = $node.children.item(1); $content.innerHTML = ""; - if (data.filteredOut) { + if (data.filteredOut || contentGroupOptionsLength === 0) { // This node doesn't pass the filter, but it's children do so // simplify the display. + $node.style.height = ""; $content.style.display = "none"; return; } + $node.style.height = "250px"; const averageHeight = 80 / contentGroupOptionsLength; const currentNodeDataRecordPK = data._rawData[nodeObjPK]; const $nodeSpacer = element("div", "spacer"); @@ -173,60 +257,21 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { groupStyle["height"] = `${averageHeight}%`; const groupColor = group.hex; groupStyle["backgroundColor"] = groupColor; - const $groupTitle = element("div", "team-group-title"); - // const groupTitleStyle = $groupTitle.style; - // groupTitleStyle["backgroundColor"] = groupColor; - // groupTitleStyle["height"] = "20%"; const groupText = group.text; - // $groupTitle.setAttribute( - // "id", - // `${currentNodeDataRecordPK}.${groupText}` - // ); - // $groupTitle.appendChild(document.createTextNode(groupText)); - // $group.appendChild($groupTitle); + $group.setAttribute("data-text", groupText); + if (showGroupTitle) { + const $groupTitle = element("div", "team-group-title"); + const groupTitleStyle = $groupTitle.style; + groupTitleStyle["backgroundColor"] = groupColor; + groupTitleStyle["height"] = "20%"; + $groupTitle.appendChild(document.createTextNode(groupText)); + $group.appendChild($groupTitle); + } const $groupContent = element("div", "team-group-content"); $group.appendChild($groupContent); if (draggable) { - $group.addEventListener("drop", async (event) => { - const elementID = event.dataTransfer.getData("element-id"); - if (elementID.includes("teamnode")) return; - const draggedContentDataRecord = JSON.parse( - document.getElementById(elementID).dataset.source - ); - orgchart.innerHTML = ""; - if (dropContentToCreate) { - // Trigger a process manager. - await contentModel.update( - draggedContentDataRecord.id, - draggedContentDataRecord - ); - delete draggedContentDataRecord["id"]; - delete draggedContentDataRecord["uuid"]; - delete draggedContentDataRecord["created_at"]; - delete draggedContentDataRecord["updated_at"]; - draggedContentDataRecord[contentFieldLinkColumnName] = - currentNodeDataRecordPK; - draggedContentDataRecord[ - contentGroupByFieldColumnName - ] = groupText; - await contentModel.create(draggedContentDataRecord); - } else { - draggedContentDataRecord[contentFieldLinkColumnName] = - currentNodeDataRecordPK; - draggedContentDataRecord[ - contentGroupByFieldColumnName - ] = groupText; - await contentModel.update( - draggedContentDataRecord.id, - draggedContentDataRecord - ); - } - - // TODO (Guy): This is refreshing the whole chart. - setTimeout(async () => { - await this.onShow(); - }, 1000); - }); + $group.addEventListener("dragover", fnContentDragOver); + $group.addEventListener("drop", fnContentDrop); } let contentDataRecordIndex = 0; while ( @@ -248,24 +293,17 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { for (const key in contentDataRecord) key.includes("__relation") && delete contentDataRecord[key]; - const contentDataRecordPK = contentDataRecord[contentObjPK]; - const rowDataID = `${currentNodeDataRecordPK}.${contentDataRecordPK}`; const $rowData = element("div", "team-group-record"); $rowData.setAttribute( "data-source", - JSON.stringify(contentDataRecord) + JSON.stringify(contentDataRecord), ); $groupContent.appendChild($rowData); - $rowData.setAttribute("id", rowDataID); + if (draggable) { $rowData.setAttribute("draggable", "true"); - $rowData.addEventListener("dragstart", (e) => { - e.dataTransfer.setData("element-id", e.target.id); - e.target.style.opacity = "0.5"; - }); - $rowData.addEventListener("dragend", (e) => { - e.target.style.opacity = "1"; - }); + $rowData.addEventListener("dragstart", fnContentDragStart); + $rowData.addEventListener("dragend", fnContentDragEnd); } const rowDataStyle = $rowData.style; rowDataStyle["borderColor"] = "#EF3340"; @@ -277,7 +315,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { const displayedObj = AB.objectByID(objID); const displayedObjPK = displayedObj.PK(); const displayedField = displayedObj.fieldByID( - contentDisplayedFields[displayedFieldKey] + contentDisplayedFields[displayedFieldKey], ); switch (objID) { case contentObjID: @@ -327,25 +365,30 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { } const $currentDisplay = element( "div", - "team-group-record-display" + "team-group-record-display", ); $rowData.appendChild($currentDisplay); const displayedFieldColumnName = displayedField.columnName; while (currentDataRecords.length > 0) { const $currentDisplayData = element( "div", - "team-group-record-display-data" + "team-group-record-display-data", ); $currentDisplay.appendChild($currentDisplayData); const currentDataRecord = currentDataRecords.pop(); + $currentDisplayData.setAttribute("data-obj-id", objID); + $currentDisplayData.setAttribute( + "data-field-id", + displayedField.id, + ); $currentDisplayData.setAttribute( - "id", - `${atDisplay}.${rowDataID}.${currentDataRecord[displayedObjPK]}` + "data-pk", + currentDataRecord[displayedObjPK], ); $currentDisplayData.appendChild( document.createTextNode( - currentDataRecord[displayedFieldColumnName] - ) + currentDataRecord[displayedFieldColumnName], + ), ); } currentField = null; @@ -361,7 +404,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { $buttons.append($editButton, $addButton); const dataID = this.teamRecordID(data.id); const values = this.datacollection.getData( - (e) => e.id === dataID + (e) => e.id === dataID, )[0]; $addButton.onclick = () => { this.teamForm("Add", { __parentID: dataID }); @@ -383,29 +426,23 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { $buttons.append($active); } }, - nodeContent: "description", }); - this.__orgchart = orgchart; - if (draggable) { // On drop update the parent (dropZone) of the node orgchart.addEventListener("nodedropped.orgchart", async (event) => { const eventDetail = event.detail; - const $draggedNode = eventDetail.draggedNode; - if (!$draggedNode.getAttribute("id").includes("teamnode")) return; - const dragNode = JSON.parse($draggedNode.dataset.source); - const dropNode = JSON.parse(eventDetail.dropZone.dataset.source); - const dragRecord = dragNode._rawData; - const dropID = dropNode._rawData.id; - - const linkField = this.getSettingField("teamLink"); - const parent = this.AB.definitionByID( - linkField.settings.linkColumn - ); - dragRecord[parent.columnName] = dropID; - await nodeModel.update(dragRecord.id, dragRecord); + 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; + await nodeModel.update(dragedRecord.id, dragedRecord); }); } const chartDom = document.querySelector(`#${ids.chartDom}`); @@ -413,31 +450,83 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { chartDom.textContent = ""; chartDom.innerHTML = ""; this.initFilter(chartDom); - const $chartContent = AB.Webix.ui({cols: [{ - view: "template", - },{ - view: "tabview", - width: 450, - cells: [ - { - header: "Placed", - body: { - id: "listView", - view: "list", - // list config - } - }, - { - header: "Unplaced", - body: { - id: "listView", - view: "list", - // list config - } - } - ] - }]}).$view; - $chartContent.children[0].children[0].appendChild(orgchart) + const ui = { + cols: [ + { + view: "template", + scroll: "auto", + }, + ], + }; + if (settings.showDataPanel === 1) { + const dataPanelUIs = []; + const dataPanelDCs = settings.dataPanelDCs; + for (const key in dataPanelDCs) { + const dc = AB.datacollectionByID(key.split(".")[1]); + await dc.waitForDataCollectionToInitialize(dc); + const panelObj = dc.datasource; + const contentFieldID = panelObj.connectFields( + (field) => field.datasourceLink.id === contentObjID, + )[0].fieldLink.id; + dataPanelUIs.push({ + header: dataPanelDCs[key], + body: { + view: "list", + template: `${panelObj + .fields() + .map((field) => `#${field.columnName}#`) + .join(" ")}`, + css: { overflow: "auto" }, + data: dc.getData(), + on: { + onAfterRender() { + callAfterRender(() => { + const $itemElements = + this.$view.children.item(0).children; + for (const $itemElement of $itemElements) { + $itemElement.setAttribute( + "data-content-linked-field-id", + contentFieldID, + ); + $itemElement.setAttribute( + "data-pk", + dc.getData( + (e) => + e.id === + $itemElement.getAttribute( + "webix_l_id", + ), + )[0][panelObj.PK()], + ); + $itemElement.setAttribute("draggable", "true"); + $itemElement.addEventListener( + "dragstart", + fnContentDragStart, + ); + $itemElement.addEventListener( + "dragend", + fnContentDragEnd, + ); + } + }); + }, + }, + }, + }); + } + ui.cols.push({ + view: "tabview", + width: 450, + tabbar: { + height: 60, + type: "bottom", + css: "webix_dark", + }, + cells: dataPanelUIs, + }); + } + const $chartContent = AB.Webix.ui(ui).$view; + $chartContent.children[0].children[0].appendChild(orgchart); chartDom.appendChild($chartContent); } } @@ -451,7 +540,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { let topNode = dc?.getCursor(); if (view.settings.topTeam) { const topNodeColumn = this.AB.definitionByID( - view.settings.topTeam + view.settings.topTeam, ).columnName; const topFromFeild = dc.getData((e) => e[topNodeColumn] === 1)[0]; topNode = topFromFeild ? topFromFeild : topNode; @@ -512,7 +601,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { } else { // sort children alphaetically node.children = node.children.sort((a, b) => - a.name > b.name ? 1 : -1 + a.name > b.name ? 1 : -1, ); } return; @@ -539,7 +628,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { let $popup = $$(this.ids.filterPopup); if (!$popup) { const [strategyField] = this.datacollection.datasource.fields( - (f) => f.id == this.view.settings["teamStrategy"] + (f) => f.id == this.view.settings["teamStrategy"], ); const strategyOptions = await strategyField.getOptions(); @@ -623,7 +712,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { const colors = this.settings.strategyColors; for (let key in colors) { css.push( - `org-chart .strategy-${key} .title{background:${colors[key]} !important;}` + `org-chart .strategy-${key} .title{background:${colors[key]} !important;}`, ); } const style = document.createElement("style"); @@ -635,11 +724,11 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { const { id } = await this.datacollection.model.create(values); const linkField = this.AB.definitionByID( - this.getSettingField("teamLink").settings.linkColumn + this.getSettingField("teamLink").settings.linkColumn, ).columnName; const nameField = this.getSettingField("teamName").columnName; const parent = document.querySelector( - `#${this.teamNodeID(values[linkField])}` + `#${this.teamNodeID(values[linkField])}`, ); const hasChild = parent.parentNode.colSpan > 1; const newChild = { @@ -729,12 +818,12 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { let $teamFormPopup = $$(this.ids.teamFormPopup); const inactive = this.getSettingField("teamInactive").columnName; const linkField = this.AB.definitionByID( - this.getSettingField("teamLink").settings.linkColumn + this.getSettingField("teamLink").settings.linkColumn, ).columnName; if (!$teamFormPopup) { const nameField = this.getSettingField("teamName"); const [strategyField] = this.datacollection.datasource.fields( - (f) => f.id == this.view.settings["teamStrategy"] + (f) => f.id == this.view.settings["teamStrategy"], ); const strategyOptions = await strategyField.getOptions(); $teamFormPopup = webix.ui({ @@ -795,7 +884,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { const values = $$(this.ids.teamForm).getValues(); const strategy = strategyOptions.find( (f) => - f.id === values[strategyField.columnName] + f.id === values[strategyField.columnName], ); if (values.id) { this.teamEdit(values, strategy); From feaf532abb96df3db2d79610e8c58f284bc51ba6 Mon Sep 17 00:00:00 2001 From: guyyoo Date: Tue, 19 Nov 2024 17:38:16 +0700 Subject: [PATCH 031/129] Team Widget can edit assignments --- .../ABViewOrgChartTeamsComponent.js | 222 ++++++++++++++++-- 1 file changed, 200 insertions(+), 22 deletions(-) diff --git a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js index ea361253..457374a9 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js @@ -11,6 +11,8 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { chartDom: "", filterPopup: "", filterForm: "", + contentForm: "", + contentFormData: "", teamForm: "", teamFormPopup: "", teamFormSubmit: "", @@ -85,6 +87,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { async displayOrgChart() { const baseView = this.view; const AB = this.AB; + const L = AB.Label(); const chartData = AB.cloneDeep(this.chartData); const settings = baseView.settings; const showGroupTitle = settings.showGroupTitle === 1; @@ -197,12 +200,13 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { updatedData[contentGroupByFieldColumnName] = newGroupText; await contentModel.create(updatedData); } else { + delete updatedData["created_at"]; + delete updatedData["updated_at"]; + delete updatedData["properties"]; if (dropContentToCreate) { updatedData = JSON.parse(updatedData); delete updatedData["id"]; delete updatedData["uuid"]; - delete updatedData["created_at"]; - delete updatedData["updated_at"]; updatedData[contentFieldLinkColumnName] = newNodeDataPK; updatedData[contentGroupByFieldColumnName] = newGroupText; await contentModel.create(updatedData); @@ -218,6 +222,192 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { // await this.onShow(); // }, 1000); }; + const editContentFieldsToCreateNew = + settings.editContentFieldsToCreateNew; + const showContentForm = async (contentDataRecord) => { + const rules = {}; + const contentFormElements = await Promise.all( + contentObj.fields().map(async (field) => { + const fieldKey = field.key; + const fieldName = field.columnName; + + // TODO (Guy): Add validators. + rules[fieldName] = () => true; + const fieldLabel = field.label; + const settings = field.settings; + switch (fieldKey) { + case "boolean": + return { + view: "checkbox", + name: fieldName, + label: fieldLabel, + }; + case "number": + return { + view: "counter", + name: fieldName, + label: fieldLabel, + type: "number", + }; + case "list": + return { + view: + (settings.isMultiple === 1 && "muticombo") || + "combo", + name: fieldName, + label: fieldLabel, + options: settings.options.map((option) => ({ + id: option.id, + value: option.text, + })), + }; + case "user": + case "connectObject": + const fieldLinkObj = field.datasourceLink; + + // TODO (Guy): Fix pulling all connections. + const options = ( + await fieldLinkObj.model().findAll() + ).data.map((e) => ({ + id: e.id, + value: fieldLinkObj.displayData(e), + })); + return field.linkType() === "one" + ? { + view: "combo", + name: fieldName, + label: fieldLabel, + options, + } + : { + view: "multicombo", + name: fieldName, + label: fieldLabel, + stringResult: false, + labelAlign: "left", + options, + }; + case "date": + case "datetime": + return { + view: "datepicker", + name: fieldName, + label: fieldLabel, + timepicker: fieldKey === "datetime", + width: 300, + }; + case "file": + case "image": + // TODO (Guy): Add logic + return { + // view: "", + name: fieldName, + label: fieldLabel, + }; + // case "json": + // case "LongText": + // case "string": + // case "email": + default: + return { + view: "text", + name: fieldName, + label: fieldLabel, + }; + } + }), + ); + contentFormElements.push({ + view: "button", + value: L("Submit"), + css: "webix_primary", + click: async () => { + const $contentFormData = $$(ids.contentFormData); + if (!$contentFormData.validate()) return; + const newFormData = $contentFormData.getValues(); + let isDataChanged = false; + for (const key in newFormData) + if ( + JSON.stringify(newFormData[key]) !== + JSON.stringify(contentDataRecord[key]) + ) { + isDataChanged = true; + break; + } + const $contentForm = $$(ids.contentForm); + if (!isDataChanged) { + $contentForm.hide(); + return; + } + delete newFormData["created_at"]; + delete newFormData["updated_at"]; + delete newFormData["properties"]; + for (const editContentFieldToCreateNew of editContentFieldsToCreateNew) { + const editContentFieldToCreateNewColumnName = + contentObj.fieldByID( + editContentFieldToCreateNew, + ).columnName; + if ( + newFormData[editContentFieldToCreateNewColumnName] !== + contentDataRecord[editContentFieldToCreateNewColumnName] + ) { + this.__orgchart.innerHTML = ""; + delete newFormData["id"]; + delete newFormData["uuid"]; + await contentModel.create(newFormData); + $contentForm.hide(); + await this.onShow(); + return; + } + } + this.__orgchart.innerHTML = ""; + await contentModel.update(newFormData.id, newFormData); + $contentForm.hide(); + await this.onShow(); + }, + }); + AB.Webix.ui({ + view: "popup", + id: ids.contentForm, + close: true, + position: "center", + body: { + rows: [ + { + view: "toolbar", + id: "myToolbar", + cols: [ + { + view: "label", + label: L("Edit Content"), + align: "left", + }, + { + view: "button", + value: "X", + align: "right", + click: () => { + $contentForm.hide(); + }, + }, + ], + }, + { + view: "form", + id: ids.contentFormData, + elements: contentFormElements, + rules, + }, + ], + }, + on: { + onHide() { + this.destructor(); + } + } + }).show(); + $$(ids.contentFormData).setValues(contentDataRecord); + }; const orgchart = new this.OrgChart({ data: chartData, direction: baseView.settings.direction, @@ -299,7 +489,9 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { JSON.stringify(contentDataRecord), ); $groupContent.appendChild($rowData); - + $rowData.addEventListener("click", async () => { + await showContentForm(contentDataRecord); + }); if (draggable) { $rowData.setAttribute("draggable", "true"); $rowData.addEventListener("dragstart", fnContentDragStart); @@ -369,28 +561,14 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { ); $rowData.appendChild($currentDisplay); const displayedFieldColumnName = displayedField.columnName; - while (currentDataRecords.length > 0) { - const $currentDisplayData = element( - "div", - "team-group-record-display-data", - ); - $currentDisplay.appendChild($currentDisplayData); - const currentDataRecord = currentDataRecords.pop(); - $currentDisplayData.setAttribute("data-obj-id", objID); - $currentDisplayData.setAttribute( - "data-field-id", - displayedField.id, - ); - $currentDisplayData.setAttribute( - "data-pk", - currentDataRecord[displayedObjPK], - ); - $currentDisplayData.appendChild( + while (currentDataRecords.length > 0) + $currentDisplay.appendChild( document.createTextNode( - currentDataRecord[displayedFieldColumnName], + currentDataRecords.pop()[ + displayedFieldColumnName + ], ), ); - } currentField = null; } } From f291eeee778ea7e9fe02981f7c0ab566aa14dd91 Mon Sep 17 00:00:00 2001 From: guyyoo Date: Tue, 19 Nov 2024 17:50:30 +0700 Subject: [PATCH 032/129] Fix not declared param bug --- .../views/viewComponent/ABViewOrgChartTeamsComponent.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js index 457374a9..8ea3fd8c 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js @@ -387,7 +387,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { value: "X", align: "right", click: () => { - $contentForm.hide(); + $$(ids.contentForm).hide(); }, }, ], From 4f82b222467e487602846d8c2204dde9a7923ff7 Mon Sep 17 00:00:00 2001 From: guyyoo Date: Tue, 19 Nov 2024 18:06:51 +0700 Subject: [PATCH 033/129] Fix the compared value bug --- .../viewComponent/ABViewOrgChartTeamsComponent.js | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js index 8ea3fd8c..cafe191d 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js @@ -348,8 +348,15 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { editContentFieldToCreateNew, ).columnName; if ( - newFormData[editContentFieldToCreateNewColumnName] !== - contentDataRecord[editContentFieldToCreateNewColumnName] + JSON.stringify( + newFormData[editContentFieldToCreateNewColumnName] ?? + "", + ) !== + JSON.stringify( + contentDataRecord[ + editContentFieldToCreateNewColumnName + ] ?? "", + ) ) { this.__orgchart.innerHTML = ""; delete newFormData["id"]; @@ -403,8 +410,8 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { on: { onHide() { this.destructor(); - } - } + }, + }, }).show(); $$(ids.contentFormData).setValues(contentDataRecord); }; From f25ad892f3db94c2f47abfa841232d96c239c7af Mon Sep 17 00:00:00 2001 From: guyyoo Date: Thu, 21 Nov 2024 11:42:40 +0700 Subject: [PATCH 034/129] Fix extra rendering node bug and prevent droping nodes in content area --- .../ABViewOrgChartTeamsComponent.js | 42 ++++++++++++------- styles/team-widget.css | 2 +- 2 files changed, 28 insertions(+), 16 deletions(-) diff --git a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js index cafe191d..9aa69dbf 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js @@ -67,9 +67,12 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { "../../../../styles/team-widget.css" ), ]); - - this.OrgChart = orgChartLoader.default; - + const OrgChart = (this.OrgChart = orgChartLoader.default); + const _oldOnDragStart = OrgChart.prototype._onDragStart; + OrgChart.prototype._onDragStart = (event) => { + event.dataTransfer.setData("isnode", 1) + _oldOnDragStart.call(this.__orgchart, event); + }; this.ready(); } @@ -104,7 +107,11 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { const contentObjPK = contentObj?.PK(); const contentDataRecordPKs = []; const contentDataRecords = []; - if (contentField != null && contentObj != null) { + if ( + Object.keys(chartData).length > 0 && + contentField != null && + contentObj != null + ) { const getContentDataRecordPKs = (node) => { const contentFieldData = node._rawData[contentFieldColumnName]; if (Array.isArray(contentFieldData)) @@ -177,6 +184,8 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { // event.target.style.opacity = "1"; }; const fnContentDrop = async (event) => { + const dataTransfer = event.dataTransfer; + if (dataTransfer.getData("isnode") == 1) return; event.stopPropagation(); if (contentFieldLinkColumnName == null) return; const $group = event.currentTarget; @@ -184,7 +193,6 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { const newNodeDataPK = JSON.parse( $group.parentElement.parentElement.dataset.source, )._rawData[nodeObjPK]; - const dataTransfer = event.dataTransfer; let updatedData = dataTransfer.getData("source"); this.__orgchart.innerHTML = ""; if (updatedData === "") { @@ -351,7 +359,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { JSON.stringify( newFormData[editContentFieldToCreateNewColumnName] ?? "", - ) !== + ) !== JSON.stringify( contentDataRecord[ editContentFieldToCreateNewColumnName @@ -437,7 +445,6 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { if (data.filteredOut || contentGroupOptionsLength === 0) { // This node doesn't pass the filter, but it's children do so // simplify the display. - $node.style.height = ""; $content.style.display = "none"; return; } @@ -634,12 +641,19 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { if (chartDom) { chartDom.textContent = ""; chartDom.innerHTML = ""; - this.initFilter(chartDom); const ui = { cols: [ { - view: "template", - scroll: "auto", + rows: [ + { + view: "template", + height: 50, + }, + { + view: "template", + scroll: "auto", + }, + ], }, ], }; @@ -711,8 +725,10 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { }); } const $chartContent = AB.Webix.ui(ui).$view; - $chartContent.children[0].children[0].appendChild(orgchart); chartDom.appendChild($chartContent); + const $chartContentLayout = $chartContent.children[0]; + this.initFilter($chartContentLayout.children[0].children[0]); + $chartContentLayout.children[1].children[0].appendChild(orgchart); } } @@ -721,7 +737,6 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { const view = this.view; const dc = view.datacollection; await dc?.waitForDataCollectionToInitialize(dc); - let topNode = dc?.getCursor(); if (view.settings.topTeam) { const topNodeColumn = this.AB.definitionByID( @@ -731,13 +746,11 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { topNode = topFromFeild ? topFromFeild : topNode; } if (!topNode) return null; - const teamLink = this.getSettingField("teamLink").columnName; const teamName = this.getSettingField("teamName").columnName; const teamInactive = this.getSettingField("teamInactive").columnName; const strategy = this.getSettingField("teamStrategy").columnName; const strategyCode = this.getSettingField("strategyCode").columnName; - const chartData = (this._chartData = {}); chartData.name = topNode[teamName] ?? ""; chartData.id = this.teamNodeID(topNode.id); @@ -746,7 +759,6 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { }`; chartData.isInactive = topNode[teamInactive]; chartData._rawData = topNode; - const maxDepth = 10; // prevent inifinite loop const self = this; function pullChildData(node, depth = 0) { diff --git a/styles/team-widget.css b/styles/team-widget.css index 6f868331..61e94646 100644 --- a/styles/team-widget.css +++ b/styles/team-widget.css @@ -6,7 +6,7 @@ org-chart .node { text-align: center; justify-content: center; width: 325px !important; - height: 250px; + /* height: 250px; */ font-family: "Jomhuria", sans-serif; font-style: normal; font-weight: 200; From 12beedff17bde1bad130481465d6494e878cdbd9 Mon Sep 17 00:00:00 2001 From: nh758 <7259@pm.me> Date: Thu, 21 Nov 2024 13:54:53 +0700 Subject: [PATCH 035/129] update core --- AppBuilder/core | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AppBuilder/core b/AppBuilder/core index 57ccee57..5b09c771 160000 --- a/AppBuilder/core +++ b/AppBuilder/core @@ -1 +1 @@ -Subproject commit 57ccee5746e8e23f1a9e44f96db9be64863c1392 +Subproject commit 5b09c77113537922d08d7d1cde9231ffe808f691 From 2464b5a97cb8c959967cc50afee375fa6af7539a Mon Sep 17 00:00:00 2001 From: nh758 <7259@pm.me> Date: Fri, 22 Nov 2024 12:59:39 +0700 Subject: [PATCH 036/129] fix data types to match real data --- .../ABViewOrgChartTeamsComponent.js | 113 +++++++++--------- 1 file changed, 59 insertions(+), 54 deletions(-) diff --git a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js index 9aa69dbf..d0e07473 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js @@ -19,8 +19,8 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { teamFormTitle: "", teamFormInactive: "", }, - ids, - ), + ids + ) ); this.__filters = {}; } @@ -70,7 +70,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { const OrgChart = (this.OrgChart = orgChartLoader.default); const _oldOnDragStart = OrgChart.prototype._onDragStart; OrgChart.prototype._onDragStart = (event) => { - event.dataTransfer.setData("isnode", 1) + event.dataTransfer.setData("isnode", 1); _oldOnDragStart.call(this.__orgchart, event); }; this.ready(); @@ -137,13 +137,17 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { }, populate: true, }) - ).data, + ).data ); } const contentGroupByField = contentObj?.fieldByID( - settings.contentGroupByField, + settings.contentGroupByField ); - const contentGroupOptions = contentGroupByField?.settings.options || []; + const { data: contentGroupOptions } = await this.AB.objectByID( + contentGroupByField?.settings.linkObject + ) + .model() + .findAll(); const contentGroupOptionsLength = contentGroupOptions.length; const contentGroupByFieldColumnName = contentGroupByField?.columnName; const contentFieldLinkColumnName = contentFieldLink?.columnName; @@ -167,7 +171,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { dataTransfer.setData("dataPK", dataset.pk); dataTransfer.setData( "contentLinkedFieldId", - dataset.contentLinkedFieldId, + dataset.contentLinkedFieldId ); break; default: @@ -191,14 +195,14 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { const $group = event.currentTarget; const newGroupText = $group.dataset.text; const newNodeDataPK = JSON.parse( - $group.parentElement.parentElement.dataset.source, + $group.parentElement.parentElement.dataset.source )._rawData[nodeObjPK]; let updatedData = dataTransfer.getData("source"); this.__orgchart.innerHTML = ""; if (updatedData === "") { const dataPK = dataTransfer.getData("dataPK"); const contentLinkedFieldID = dataTransfer.getData( - "contentLinkedFieldId", + "contentLinkedFieldId" ); const contentLinkedField = contentObj.fieldByID(contentLinkedFieldID); @@ -323,7 +327,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { label: fieldLabel, }; } - }), + }) ); contentFormElements.push({ view: "button", @@ -353,17 +357,16 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { for (const editContentFieldToCreateNew of editContentFieldsToCreateNew) { const editContentFieldToCreateNewColumnName = contentObj.fieldByID( - editContentFieldToCreateNew, + editContentFieldToCreateNew ).columnName; if ( JSON.stringify( - newFormData[editContentFieldToCreateNewColumnName] ?? - "", + newFormData[editContentFieldToCreateNewColumnName] ?? "" ) !== JSON.stringify( contentDataRecord[ editContentFieldToCreateNewColumnName - ] ?? "", + ] ?? "" ) ) { this.__orgchart.innerHTML = ""; @@ -459,9 +462,12 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { $content.appendChild($group); const groupStyle = $group.style; groupStyle["height"] = `${averageHeight}%`; - const groupColor = group.hex; + // TODO: should this be a config option + const groupColor = + group.name === "Leader" ? "#003366" : "#DDDDDD"; groupStyle["backgroundColor"] = groupColor; - const groupText = group.text; + // TODO: should this be a config option + const groupText = group.name; $group.setAttribute("data-text", groupText); if (showGroupTitle) { const $groupTitle = element("div", "team-group-title"); @@ -485,10 +491,10 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { const contentDataRecord = contentDataRecords[contentDataRecordIndex]; if ( - contentDataRecord[contentFieldLinkColumnName] !== + contentDataRecord[contentFieldLinkColumnName] != currentNodeDataRecordPK || - contentDataRecord[contentGroupByFieldColumnName] !== - groupText + contentDataRecord[contentGroupByFieldColumnName] != + group.id ) { contentDataRecordIndex++; continue; @@ -500,7 +506,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { const $rowData = element("div", "team-group-record"); $rowData.setAttribute( "data-source", - JSON.stringify(contentDataRecord), + JSON.stringify(contentDataRecord) ); $groupContent.appendChild($rowData); $rowData.addEventListener("click", async () => { @@ -521,7 +527,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { const displayedObj = AB.objectByID(objID); const displayedObjPK = displayedObj.PK(); const displayedField = displayedObj.fieldByID( - contentDisplayedFields[displayedFieldKey], + contentDisplayedFields[displayedFieldKey] ); switch (objID) { case contentObjID: @@ -571,17 +577,15 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { } const $currentDisplay = element( "div", - "team-group-record-display", + "team-group-record-display" ); $rowData.appendChild($currentDisplay); const displayedFieldColumnName = displayedField.columnName; while (currentDataRecords.length > 0) $currentDisplay.appendChild( document.createTextNode( - currentDataRecords.pop()[ - displayedFieldColumnName - ], - ), + currentDataRecords.pop()[displayedFieldColumnName] + ) ); currentField = null; } @@ -596,7 +600,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { $buttons.append($editButton, $addButton); const dataID = this.teamRecordID(data.id); const values = this.datacollection.getData( - (e) => e.id === dataID, + (e) => e.id === dataID )[0]; $addButton.onclick = () => { this.teamForm("Add", { __parentID: dataID }); @@ -626,12 +630,12 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { orgchart.addEventListener("nodedropped.orgchart", async (event) => { const eventDetail = event.detail; const dragedRecord = JSON.parse( - eventDetail.draggedNode.dataset.source, + eventDetail.draggedNode.dataset.source )._rawData; dragedRecord[ // Parent node definition. this.AB.definitionByID( - this.getSettingField("teamLink").settings.linkColumn, + this.getSettingField("teamLink").settings.linkColumn ).columnName ] = JSON.parse(eventDetail.dropZone.dataset.source)._rawData.id; await nodeModel.update(dragedRecord.id, dragedRecord); @@ -665,7 +669,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { await dc.waitForDataCollectionToInitialize(dc); const panelObj = dc.datasource; const contentFieldID = panelObj.connectFields( - (field) => field.datasourceLink.id === contentObjID, + (field) => field.datasourceLink.id === contentObjID )[0].fieldLink.id; dataPanelUIs.push({ header: dataPanelDCs[key], @@ -685,7 +689,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { for (const $itemElement of $itemElements) { $itemElement.setAttribute( "data-content-linked-field-id", - contentFieldID, + contentFieldID ); $itemElement.setAttribute( "data-pk", @@ -693,18 +697,18 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { (e) => e.id === $itemElement.getAttribute( - "webix_l_id", - ), - )[0][panelObj.PK()], + "webix_l_id" + ) + )[0][panelObj.PK()] ); $itemElement.setAttribute("draggable", "true"); $itemElement.addEventListener( "dragstart", - fnContentDragStart, + fnContentDragStart ); $itemElement.addEventListener( "dragend", - fnContentDragEnd, + fnContentDragEnd ); } }); @@ -740,7 +744,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { let topNode = dc?.getCursor(); if (view.settings.topTeam) { const topNodeColumn = this.AB.definitionByID( - view.settings.topTeam, + view.settings.topTeam ).columnName; const topFromFeild = dc.getData((e) => e[topNodeColumn] === 1)[0]; topNode = topFromFeild ? topFromFeild : topNode; @@ -749,13 +753,13 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { const teamLink = this.getSettingField("teamLink").columnName; const teamName = this.getSettingField("teamName").columnName; const teamInactive = this.getSettingField("teamInactive").columnName; - const strategy = this.getSettingField("teamStrategy").columnName; + const strategyField = this.getSettingField("teamStrategy").columnName; const strategyCode = this.getSettingField("strategyCode").columnName; const chartData = (this._chartData = {}); chartData.name = topNode[teamName] ?? ""; chartData.id = this.teamNodeID(topNode.id); chartData.className = `strategy-${ - topNode[`${strategy}__relation`]?.[strategyCode] + topNode[`${strategyField}__relation`]?.[strategyCode] }`; chartData.isInactive = topNode[teamInactive]; chartData._rawData = topNode; @@ -765,15 +769,16 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { if (depth >= maxDepth) return; node.children = []; node._rawData[teamLink].forEach((id) => { - const childData = dc.getData((e) => e.id === id)[0]; + const childData = dc.getData((e) => e.id == id)[0]; // Don't show inactive teams if ( - (filters?.inactive !== 1 && childData[teamInactive]) || - childData == null + !childData || + (filters?.inactive !== 1 && childData[teamInactive]) ) return; - const code = childData[`${strategy}__relation`]?.[strategyCode]; - const strategyClass = `strategy-${code}`; + const strategy = childData[`${strategyField}__relation`]; + + const strategyClass = `strategy-${strategy[strategyCode]}`; const child = { name: childData[teamName], id: self.teamNodeID(id), @@ -781,7 +786,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { isInactive: childData[teamInactive], _rawData: childData, }; - child.filteredOut = self.filterTeam(filters, child, code); + child.filteredOut = self.filterTeam(filters, child, strategy.id); if (child.name === "External Support") child.className = `strategy-external`; if (childData[teamLink].length > 0) { @@ -798,7 +803,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { } else { // sort children alphaetically node.children = node.children.sort((a, b) => - a.name > b.name ? 1 : -1, + a.name > b.name ? 1 : -1 ); } return; @@ -825,7 +830,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { let $popup = $$(this.ids.filterPopup); if (!$popup) { const [strategyField] = this.datacollection.datasource.fields( - (f) => f.id == this.view.settings["teamStrategy"], + (f) => f.id == this.view.settings["teamStrategy"] ); const strategyOptions = await strategyField.getOptions(); @@ -880,7 +885,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { // Apply filters (match using or) if (filters.strategy || filters.teamName) { let filter = true; - if (filters.strategy !== "" && filters.strategy === code) { + if (filters.strategy !== "" && filters.strategy == code) { filter = false; } if ( @@ -909,7 +914,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { const colors = this.settings.strategyColors; for (let key in colors) { css.push( - `org-chart .strategy-${key} .title{background:${colors[key]} !important;}`, + `org-chart .strategy-${key} .title{background:${colors[key]} !important;}` ); } const style = document.createElement("style"); @@ -921,11 +926,11 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { const { id } = await this.datacollection.model.create(values); const linkField = this.AB.definitionByID( - this.getSettingField("teamLink").settings.linkColumn, + this.getSettingField("teamLink").settings.linkColumn ).columnName; const nameField = this.getSettingField("teamName").columnName; const parent = document.querySelector( - `#${this.teamNodeID(values[linkField])}`, + `#${this.teamNodeID(values[linkField])}` ); const hasChild = parent.parentNode.colSpan > 1; const newChild = { @@ -1015,12 +1020,12 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { let $teamFormPopup = $$(this.ids.teamFormPopup); const inactive = this.getSettingField("teamInactive").columnName; const linkField = this.AB.definitionByID( - this.getSettingField("teamLink").settings.linkColumn, + this.getSettingField("teamLink").settings.linkColumn ).columnName; if (!$teamFormPopup) { const nameField = this.getSettingField("teamName"); const [strategyField] = this.datacollection.datasource.fields( - (f) => f.id == this.view.settings["teamStrategy"], + (f) => f.id == this.view.settings["teamStrategy"] ); const strategyOptions = await strategyField.getOptions(); $teamFormPopup = webix.ui({ @@ -1081,7 +1086,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { const values = $$(this.ids.teamForm).getValues(); const strategy = strategyOptions.find( (f) => - f.id === values[strategyField.columnName], + f.id === values[strategyField.columnName] ); if (values.id) { this.teamEdit(values, strategy); From 2534ea9354f694d062e0d244165db18d763f2a2e Mon Sep 17 00:00:00 2001 From: nh758 <7259@pm.me> Date: Fri, 22 Nov 2024 14:27:30 +0700 Subject: [PATCH 037/129] work on chart loading performance --- AppBuilder/ABFactory.js | 2 ++ .../ABViewOrgChartTeamsComponent.js | 30 ++++++++++++++++--- 2 files changed, 28 insertions(+), 4 deletions(-) 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/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js index d0e07473..ae8692eb 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js @@ -77,14 +77,18 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { } async onShow() { + this.AB.performance.mark("TeamChart.onShow"); super.onShow(); - this.busy(); this.generateStrategyCss(); - await this.loadOrgChartJs(); - await this.pullData(); + this.AB.performance.mark("TeamChart.load"); + await Promise.all([this.loadOrgChartJs(), this.pullData()]); + this.AB.performance.measure("TeamChart.load"); + this.AB.performance.mark("TeamChart.display"); await this.displayOrgChart(); + this.AB.performance.measure("TeamChart.display"); this.ready(); + this.AB.performance.measure("TeamChart.onShow"); } async displayOrgChart() { @@ -112,6 +116,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { contentField != null && contentObj != null ) { + this.AB.performance.mark("loadAssignments"); const getContentDataRecordPKs = (node) => { const contentFieldData = node._rawData[contentFieldColumnName]; if (Array.isArray(contentFieldData)) @@ -139,15 +144,19 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { }) ).data ); + this.AB.performance.measure("loadAssignments"); } const contentGroupByField = contentObj?.fieldByID( settings.contentGroupByField ); + this.AB.performance.mark("loadAssigmentType"); const { data: contentGroupOptions } = await this.AB.objectByID( contentGroupByField?.settings.linkObject ) .model() .findAll(); + this.AB.performance.measure("loadAssigmentType"); + this.AB.performance.mark("misc"); const contentGroupOptionsLength = contentGroupOptions.length; const contentGroupByFieldColumnName = contentGroupByField?.columnName; const contentFieldLinkColumnName = contentFieldLink?.columnName; @@ -212,11 +221,11 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { updatedData[contentGroupByFieldColumnName] = newGroupText; await contentModel.create(updatedData); } else { + updatedData = JSON.parse(updatedData); delete updatedData["created_at"]; delete updatedData["updated_at"]; delete updatedData["properties"]; if (dropContentToCreate) { - updatedData = JSON.parse(updatedData); delete updatedData["id"]; delete updatedData["uuid"]; updatedData[contentFieldLinkColumnName] = newNodeDataPK; @@ -426,6 +435,8 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { }).show(); $$(ids.contentFormData).setValues(contentDataRecord); }; + this.AB.performance.measure("misc"); + this.AB.performance.mark("createOrgChart"); const orgchart = new this.OrgChart({ data: chartData, direction: baseView.settings.direction, @@ -624,6 +635,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { }, nodeContent: "description", }); + this.AB.performance.measure("createOrgChart"); this.__orgchart = orgchart; if (draggable) { // On drop update the parent (dropZone) of the node @@ -736,6 +748,9 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { } } + /** + * load the data and format it for display + */ async pullData() { const filters = this.__filters; const view = this.view; @@ -765,6 +780,11 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { chartData._rawData = topNode; const maxDepth = 10; // prevent inifinite loop const self = this; + /** + * Recursive function to prepare child node data + * @param {object} node the current node + * @param {number} [depth=0] a count of how many times we have recursed + */ function pullChildData(node, depth = 0) { if (depth >= maxDepth) return; node.children = []; @@ -818,6 +838,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { return this._chartData; } + /** Add the filter button to the UI */ initFilter(domNode) { const filterButton = document.createElement("button"); filterButton.innerHTML = ` Filter`; @@ -826,6 +847,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { domNode.append(filterButton); } + /** Display the filter UI (popup) **/ async filterWindow(buttonNode) { let $popup = $$(this.ids.filterPopup); if (!$popup) { From e3a6dabeb69a9dccb228502f5e7b5729690a34dc Mon Sep 17 00:00:00 2001 From: guyyoo Date: Mon, 25 Nov 2024 12:10:48 +0700 Subject: [PATCH 038/129] [WIP] Content filter --- .../ABViewOrgChartTeamsComponent.js | 206 ++++++++++-------- 1 file changed, 116 insertions(+), 90 deletions(-) diff --git a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js index 9aa69dbf..25e1153b 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js @@ -19,10 +19,12 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { teamFormTitle: "", teamFormInactive: "", }, - ids, - ), + ids + ) ); this.__filters = {}; + this._chartData = null; + this._cachedContentDataRecords = null; } ui() { @@ -70,7 +72,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { const OrgChart = (this.OrgChart = orgChartLoader.default); const _oldOnDragStart = OrgChart.prototype._onDragStart; OrgChart.prototype._onDragStart = (event) => { - event.dataTransfer.setData("isnode", 1) + event.dataTransfer.setData("isnode", 1); _oldOnDragStart.call(this.__orgchart, event); }; this.ready(); @@ -82,9 +84,8 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { this.busy(); this.generateStrategyCss(); await this.loadOrgChartJs(); - await this.pullData(); - await this.displayOrgChart(); this.ready(); + await this.refresh(); } async displayOrgChart() { @@ -100,48 +101,12 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { const nodeModel = baseView.datacollection.model; const nodeObj = nodeDC?.datasource; const nodeObjPK = nodeObj.PK(); - const contentField = nodeObj.fieldByID(settings.contentField); - const contentFieldColumnName = contentField?.columnName; - const contentFieldLink = contentField?.fieldLink; + const contentFieldLink = nodeObj.fieldByID( + settings.contentField + )?.fieldLink; const contentObj = contentFieldLink?.object; - const contentObjPK = contentObj?.PK(); - const contentDataRecordPKs = []; - const contentDataRecords = []; - if ( - Object.keys(chartData).length > 0 && - contentField != null && - contentObj != null - ) { - const getContentDataRecordPKs = (node) => { - const contentFieldData = node._rawData[contentFieldColumnName]; - if (Array.isArray(contentFieldData)) - contentDataRecordPKs.push(...contentFieldData); - else contentDataRecordPKs.push(contentFieldData); - const children = node.children || []; - for (const child of children) getContentDataRecordPKs(child); - }; - getContentDataRecordPKs(chartData); - contentDataRecords.push( - ...( - await contentObj.model().findAll({ - where: { - glue: "and", - rules: [ - { - key: contentObjPK, - rule: "in", - value: contentDataRecordPKs, - }, - JSON.parse(settings.contentFieldFilter), - ], - }, - populate: true, - }) - ).data, - ); - } const contentGroupByField = contentObj?.fieldByID( - settings.contentGroupByField, + settings.contentGroupByField ); const contentGroupOptions = contentGroupByField?.settings.options || []; const contentGroupOptionsLength = contentGroupOptions.length; @@ -167,7 +132,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { dataTransfer.setData("dataPK", dataset.pk); dataTransfer.setData( "contentLinkedFieldId", - dataset.contentLinkedFieldId, + dataset.contentLinkedFieldId ); break; default: @@ -191,14 +156,14 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { const $group = event.currentTarget; const newGroupText = $group.dataset.text; const newNodeDataPK = JSON.parse( - $group.parentElement.parentElement.dataset.source, + $group.parentElement.parentElement.dataset.source )._rawData[nodeObjPK]; let updatedData = dataTransfer.getData("source"); this.__orgchart.innerHTML = ""; if (updatedData === "") { const dataPK = dataTransfer.getData("dataPK"); const contentLinkedFieldID = dataTransfer.getData( - "contentLinkedFieldId", + "contentLinkedFieldId" ); const contentLinkedField = contentObj.fieldByID(contentLinkedFieldID); @@ -225,10 +190,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { } } // TODO (Guy): This is refreshing the whole chart. - await this.onShow(); - // setTimeout(async () => { - // await this.onShow(); - // }, 1000); + await this.refresh(); }; const editContentFieldsToCreateNew = settings.editContentFieldsToCreateNew; @@ -323,7 +285,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { label: fieldLabel, }; } - }), + }) ); contentFormElements.push({ view: "button", @@ -353,17 +315,16 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { for (const editContentFieldToCreateNew of editContentFieldsToCreateNew) { const editContentFieldToCreateNewColumnName = contentObj.fieldByID( - editContentFieldToCreateNew, + editContentFieldToCreateNew ).columnName; if ( JSON.stringify( - newFormData[editContentFieldToCreateNewColumnName] ?? - "", + newFormData[editContentFieldToCreateNewColumnName] ?? "" ) !== JSON.stringify( contentDataRecord[ editContentFieldToCreateNewColumnName - ] ?? "", + ] ?? "" ) ) { this.__orgchart.innerHTML = ""; @@ -371,14 +332,14 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { delete newFormData["uuid"]; await contentModel.create(newFormData); $contentForm.hide(); - await this.onShow(); + await this.refresh(); return; } } this.__orgchart.innerHTML = ""; await contentModel.update(newFormData.id, newFormData); $contentForm.hide(); - await this.onShow(); + await this.refreshs(); }, }); AB.Webix.ui({ @@ -423,6 +384,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { }).show(); $$(ids.contentFormData).setValues(contentDataRecord); }; + const contentDataRecords = this._cachedContentDataRecords const orgchart = new this.OrgChart({ data: chartData, direction: baseView.settings.direction, @@ -500,7 +462,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { const $rowData = element("div", "team-group-record"); $rowData.setAttribute( "data-source", - JSON.stringify(contentDataRecord), + JSON.stringify(contentDataRecord) ); $groupContent.appendChild($rowData); $rowData.addEventListener("click", async () => { @@ -521,7 +483,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { const displayedObj = AB.objectByID(objID); const displayedObjPK = displayedObj.PK(); const displayedField = displayedObj.fieldByID( - contentDisplayedFields[displayedFieldKey], + contentDisplayedFields[displayedFieldKey] ); switch (objID) { case contentObjID: @@ -571,17 +533,15 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { } const $currentDisplay = element( "div", - "team-group-record-display", + "team-group-record-display" ); $rowData.appendChild($currentDisplay); const displayedFieldColumnName = displayedField.columnName; while (currentDataRecords.length > 0) $currentDisplay.appendChild( document.createTextNode( - currentDataRecords.pop()[ - displayedFieldColumnName - ], - ), + currentDataRecords.pop()[displayedFieldColumnName] + ) ); currentField = null; } @@ -596,7 +556,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { $buttons.append($editButton, $addButton); const dataID = this.teamRecordID(data.id); const values = this.datacollection.getData( - (e) => e.id === dataID, + (e) => e.id === dataID )[0]; $addButton.onclick = () => { this.teamForm("Add", { __parentID: dataID }); @@ -626,12 +586,12 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { orgchart.addEventListener("nodedropped.orgchart", async (event) => { const eventDetail = event.detail; const dragedRecord = JSON.parse( - eventDetail.draggedNode.dataset.source, + eventDetail.draggedNode.dataset.source )._rawData; dragedRecord[ // Parent node definition. this.AB.definitionByID( - this.getSettingField("teamLink").settings.linkColumn, + this.getSettingField("teamLink").settings.linkColumn ).columnName ] = JSON.parse(eventDetail.dropZone.dataset.source)._rawData.id; await nodeModel.update(dragedRecord.id, dragedRecord); @@ -665,7 +625,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { await dc.waitForDataCollectionToInitialize(dc); const panelObj = dc.datasource; const contentFieldID = panelObj.connectFields( - (field) => field.datasourceLink.id === contentObjID, + (field) => field.datasourceLink.id === contentObjID )[0].fieldLink.id; dataPanelUIs.push({ header: dataPanelDCs[key], @@ -685,7 +645,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { for (const $itemElement of $itemElements) { $itemElement.setAttribute( "data-content-linked-field-id", - contentFieldID, + contentFieldID ); $itemElement.setAttribute( "data-pk", @@ -693,18 +653,18 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { (e) => e.id === $itemElement.getAttribute( - "webix_l_id", - ), - )[0][panelObj.PK()], + "webix_l_id" + ) + )[0][panelObj.PK()] ); $itemElement.setAttribute("draggable", "true"); $itemElement.addEventListener( "dragstart", - fnContentDragStart, + fnContentDragStart ); $itemElement.addEventListener( "dragend", - fnContentDragEnd, + fnContentDragEnd ); } }); @@ -734,13 +694,13 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { async pullData() { const filters = this.__filters; - const view = this.view; - const dc = view.datacollection; + const settings = this.view.settings; + const dc = this.view.datacollection; await dc?.waitForDataCollectionToInitialize(dc); let topNode = dc?.getCursor(); - if (view.settings.topTeam) { + if (settings.topTeam) { const topNodeColumn = this.AB.definitionByID( - view.settings.topTeam, + settings.topTeam ).columnName; const topFromFeild = dc.getData((e) => e[topNodeColumn] === 1)[0]; topNode = topFromFeild ? topFromFeild : topNode; @@ -798,12 +758,58 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { } else { // sort children alphaetically node.children = node.children.sort((a, b) => - a.name > b.name ? 1 : -1, + a.name > b.name ? 1 : -1 ); } return; } pullChildData(chartData); + const contentField = dc?.datasource.fieldByID(settings.contentField); + if (contentField == null) return; + const contentFieldColumnName = contentField?.columnName; + const contentObj = contentField?.fieldLink?.object; + const contentDataRecordPKs = []; + const contentDataRecords = (this._cachedContentDataRecords = []); + if ( + Object.keys(chartData).length > 0 && + contentField != null && + contentObj != null + ) { + const getContentDataRecordPKs = (node) => { + const contentFieldData = node._rawData[contentFieldColumnName]; + if (Array.isArray(contentFieldData)) + contentDataRecordPKs.push(...contentFieldData); + else contentDataRecordPKs.push(contentFieldData); + const children = node.children || []; + for (const child of children) getContentDataRecordPKs(child); + }; + getContentDataRecordPKs(chartData); + contentDataRecords.push( + ...( + await contentObj.model().findAll({ + where: { + glue: "and", + rules: [ + { + key: contentObj.PK(), + rule: "in", + value: contentDataRecordPKs, + }, + JSON.parse(settings.contentFieldFilter), + ], + }, + populate: true, + }) + ).data + ); + } + } + + async refresh() { + this.busy() + await this.pullData(); + await this.displayOrgChart(); + this.ready() } get chartData() { @@ -822,10 +828,13 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { } async filterWindow(buttonNode) { + const AB = this.AB; + const contentDisplayedFieldFilters = + this.settings.contentDisplayedFieldFilters; let $popup = $$(this.ids.filterPopup); if (!$popup) { const [strategyField] = this.datacollection.datasource.fields( - (f) => f.id == this.view.settings["teamStrategy"], + (f) => f.id == this.view.settings["teamStrategy"] ); const strategyOptions = await strategyField.getOptions(); @@ -858,6 +867,23 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { labelRight: this.label("Show Inactive Teams"), labelWidth: 0, }, + ...(() => { + const contentDisplayedFieldFilterViews = []; + for (const contentDisplayedFieldFilterKey in contentDisplayedFieldFilters) { + if (contentDisplayedFieldFilterKey.split(".")[3] == 1) { + contentDisplayedFieldFilterViews.push({ + view: "text", + label: contentDisplayedFieldFilters[ + contentDisplayedFieldFilterKey + ], + labelWidth: 90, + name: contentDisplayedFieldFilterKey, + clear: true, + }); + } + } + return contentDisplayedFieldFilterViews; + })(), { view: "button", label: this.label("Apply"), @@ -870,10 +896,10 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { $popup.show(buttonNode); } - filterApply() { + async filterApply() { $$(this.ids.filterPopup).hide(); this.__filters = $$(this.ids.filterForm).getValues(); - this.pullData().then(() => this.displayOrgChart()); + await this.refresh(); } filterTeam(filters, team, code) { @@ -909,7 +935,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { const colors = this.settings.strategyColors; for (let key in colors) { css.push( - `org-chart .strategy-${key} .title{background:${colors[key]} !important;}`, + `org-chart .strategy-${key} .title{background:${colors[key]} !important;}` ); } const style = document.createElement("style"); @@ -921,11 +947,11 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { const { id } = await this.datacollection.model.create(values); const linkField = this.AB.definitionByID( - this.getSettingField("teamLink").settings.linkColumn, + this.getSettingField("teamLink").settings.linkColumn ).columnName; const nameField = this.getSettingField("teamName").columnName; const parent = document.querySelector( - `#${this.teamNodeID(values[linkField])}`, + `#${this.teamNodeID(values[linkField])}` ); const hasChild = parent.parentNode.colSpan > 1; const newChild = { @@ -1015,12 +1041,12 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { let $teamFormPopup = $$(this.ids.teamFormPopup); const inactive = this.getSettingField("teamInactive").columnName; const linkField = this.AB.definitionByID( - this.getSettingField("teamLink").settings.linkColumn, + this.getSettingField("teamLink").settings.linkColumn ).columnName; if (!$teamFormPopup) { const nameField = this.getSettingField("teamName"); const [strategyField] = this.datacollection.datasource.fields( - (f) => f.id == this.view.settings["teamStrategy"], + (f) => f.id == this.view.settings["teamStrategy"] ); const strategyOptions = await strategyField.getOptions(); $teamFormPopup = webix.ui({ @@ -1081,7 +1107,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { const values = $$(this.ids.teamForm).getValues(); const strategy = strategyOptions.find( (f) => - f.id === values[strategyField.columnName], + f.id === values[strategyField.columnName] ); if (values.id) { this.teamEdit(values, strategy); From 278e43ccf9325ed28148c34d1d4ca724316a3988 Mon Sep 17 00:00:00 2001 From: guyyoo Date: Mon, 25 Nov 2024 15:02:26 +0700 Subject: [PATCH 039/129] Fix dropping content data --- .../viewComponent/ABViewOrgChartTeamsComponent.js | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js index 567f5251..8941b7a7 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js @@ -114,11 +114,13 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { settings.contentGroupByField ); this.AB.performance.mark("loadAssigmentType"); - const { data: contentGroupOptions } = await this.AB.objectByID( + const contentGroupObj = this.AB.objectByID( contentGroupByField?.settings.linkObject ) + const { data: contentGroupOptions } = await contentGroupObj .model() .findAll(); + const groupObjPKColumeName = contentGroupObj.PK(); this.AB.performance.measure("loadAssigmentType"); this.AB.performance.mark("misc"); const contentGroupOptionsLength = contentGroupOptions.length; @@ -166,7 +168,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { event.stopPropagation(); if (contentFieldLinkColumnName == null) return; const $group = event.currentTarget; - const newGroupText = $group.dataset.text; + const newGroupDataPK = $group.dataset.pk; const newNodeDataPK = JSON.parse( $group.parentElement.parentElement.dataset.source )._rawData[nodeObjPK]; @@ -182,7 +184,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { updatedData = {}; updatedData[contentLinkedField.columnName] = dataPK; updatedData[contentFieldLinkColumnName] = newNodeDataPK; - updatedData[contentGroupByFieldColumnName] = newGroupText; + updatedData[contentGroupByFieldColumnName] = newGroupDataPK; await contentModel.create(updatedData); } else { updatedData = JSON.parse(updatedData); @@ -193,11 +195,11 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { delete updatedData["id"]; delete updatedData["uuid"]; updatedData[contentFieldLinkColumnName] = newNodeDataPK; - updatedData[contentGroupByFieldColumnName] = newGroupText; + updatedData[contentGroupByFieldColumnName] = newGroupDataPK; await contentModel.create(updatedData); } else { updatedData[contentFieldLinkColumnName] = newNodeDataPK; - updatedData[contentGroupByFieldColumnName] = newGroupText; + updatedData[contentGroupByFieldColumnName] = newGroupDataPK; await contentModel.update(updatedData.id, updatedData); } } @@ -441,7 +443,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { groupStyle["backgroundColor"] = groupColor; // TODO: should this be a config option const groupText = group.name; - $group.setAttribute("data-text", groupText); + $group.setAttribute("data-pk", group[groupObjPKColumeName]); if (showGroupTitle) { const $groupTitle = element("div", "team-group-title"); const groupTitleStyle = $groupTitle.style; @@ -498,7 +500,6 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { const displayedFieldKey = contentDisplayedFieldsKeys[j]; const [atDisplay, objID] = displayedFieldKey.split("."); const displayedObj = AB.objectByID(objID); - const displayedObjPK = displayedObj.PK(); const displayedField = displayedObj.fieldByID( contentDisplayedFields[displayedFieldKey] ); From d74b55c4fea9da33585f16177a1b12f7134f6f19 Mon Sep 17 00:00:00 2001 From: guyyoo Date: Mon, 25 Nov 2024 15:21:10 +0700 Subject: [PATCH 040/129] Fix typo --- .../views/viewComponent/ABViewOrgChartTeamsComponent.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js index 8941b7a7..5e54923b 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js @@ -353,7 +353,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { this.__orgchart.innerHTML = ""; await contentModel.update(newFormData.id, newFormData); $contentForm.hide(); - await this.refreshs(); + await this.refresh(); }, }); AB.Webix.ui({ From 92b6b3d9f67507c098b21765d5840a0cc268bdfa Mon Sep 17 00:00:00 2001 From: guyyoo Date: Tue, 26 Nov 2024 18:31:25 +0700 Subject: [PATCH 041/129] Data pannel displayed by object label reference --- .../views/viewComponent/ABViewOrgChartTeamsComponent.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js index 5e54923b..2b1bb7df 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js @@ -650,10 +650,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { header: dataPanelDCs[key], body: { view: "list", - template: `${panelObj - .fields() - .map((field) => `#${field.columnName}#`) - .join(" ")}`, + template: (data) => panelObj.displayData(data), css: { overflow: "auto" }, data: dc.getData(), on: { From 4406b9dc830350095ba3ff954e406fcfadd1a557 Mon Sep 17 00:00:00 2001 From: guyyoo Date: Tue, 26 Nov 2024 18:40:07 +0700 Subject: [PATCH 042/129] Adjust list's item position --- .../views/viewComponent/ABViewOrgChartTeamsComponent.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js index 2b1bb7df..9e4e90fe 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js @@ -116,7 +116,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { this.AB.performance.mark("loadAssigmentType"); const contentGroupObj = this.AB.objectByID( contentGroupByField?.settings.linkObject - ) + ); const { data: contentGroupOptions } = await contentGroupObj .model() .findAll(); @@ -650,7 +650,10 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { header: dataPanelDCs[key], body: { view: "list", - template: (data) => panelObj.displayData(data), + template: (data) => + `
${panelObj.displayData( + data + )}
`, css: { overflow: "auto" }, data: dc.getData(), on: { From 1528b17970d6d0c06e9e03e678d10c926525d47c Mon Sep 17 00:00:00 2001 From: nh758 <7259@pm.me> Date: Tue, 26 Nov 2024 16:35:34 +0700 Subject: [PATCH 043/129] add entity and attach to creates --- .../ABViewOrgChartTeamsComponent.js | 34 ++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js index 5e54923b..0e1feb7e 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js @@ -91,6 +91,17 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { this.AB.performance.measure("TeamChart.display"); this.ready(); this.AB.performance.measure("TeamChart.onShow"); + if (this.settings.entityDatacollection) { + this.entityDC = this.AB.datacollectionByID( + this.settings.entityDatacollection + ); + if (!this._entityChangeListener) { + // Reload the Chart if our Entity changes + this._entityChangeListener = this.entityDC.on("changeCursor", () => + this.refresh() + ); + } + } } async displayOrgChart() { @@ -116,7 +127,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { this.AB.performance.mark("loadAssigmentType"); const contentGroupObj = this.AB.objectByID( contentGroupByField?.settings.linkObject - ) + ); const { data: contentGroupOptions } = await contentGroupObj .model() .findAll(); @@ -162,6 +173,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { const fnContentDragEnd = (event) => { // event.target.style.opacity = "1"; }; + const fnContentDrop = async (event) => { const dataTransfer = event.dataTransfer; if (dataTransfer.getData("isnode") == 1) return; @@ -185,6 +197,13 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { updatedData[contentLinkedField.columnName] = dataPK; updatedData[contentFieldLinkColumnName] = newNodeDataPK; updatedData[contentGroupByFieldColumnName] = newGroupDataPK; + if (this.entityDC) { + const entityLink = this.entityDC?.datasource.connectFields( + (f) => f.settings.linkObject === contentObj.id + )[0].id; + const entityCol = this.AB.definitionByID(entityLink).columnName; + updatedData[entityCol] = this.entityDC.getCursor(); + } await contentModel.create(updatedData); } else { updatedData = JSON.parse(updatedData); @@ -976,6 +995,19 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { } async teamAddChild(values, strategy) { + // Add the entity value + if (this.entityDC) { + const connection = this.entityDC.datasource.connectFields( + (f) => f.settings.linkObject === this.datacollection.datasource.id + )[0]; + if (connection) { + const entity = this.entityDC.getCursor(); + const cName = this.AB.definitionByID( + connection.settings.linkColumn + ).columnName; + values[cName] = entity; + } + } const { id } = await this.datacollection.model.create(values); const linkField = this.AB.definitionByID( From e8342f0fd36fb50902b5ae5ee5896dafa497f1a4 Mon Sep 17 00:00:00 2001 From: guyyoo Date: Wed, 27 Nov 2024 14:08:33 +0700 Subject: [PATCH 044/129] Content filter and fix the collapse team node bug --- .../ABViewOrgChartTeamsComponent.js | 221 +++++++++++++++--- 1 file changed, 193 insertions(+), 28 deletions(-) diff --git a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js index 9e4e90fe..2d6e1abd 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js @@ -330,7 +330,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { const editContentFieldToCreateNewColumnName = contentObj.fieldByID( editContentFieldToCreateNew - ).columnName; + )?.columnName; if ( JSON.stringify( newFormData[editContentFieldToCreateNewColumnName] ?? "" @@ -733,16 +733,167 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { const teamInactive = this.getSettingField("teamInactive").columnName; const strategyField = this.getSettingField("teamStrategy").columnName; const strategyCode = this.getSettingField("strategyCode").columnName; + const self = this; + const contentField = dc?.datasource.fieldByID(settings.contentField); + const contentFieldColumnName = contentField?.columnName; + const contentObj = contentField?.fieldLink?.object; + const contentObjPK = contentObj.PK(); + const contentFieldFilter = JSON.parse(settings.contentFieldFilter); + const contentDisplayedFields = settings.contentDisplayedFields; + const contentDisplayFieldKeys = Object.keys(contentDisplayedFields); + const contentDisplayedFieldFilters = + settings.contentDisplayedFieldFilters; + const contentDataRecords = (this._cachedContentDataRecords = []); + let isContentFiltered = false; + + // TODO (Guy): Now, this approch is having so many requests. + this.AB.performance.mark("loadFilteredContent"); + for (const key in contentDisplayedFieldFilters) { + const [filterAtDisplay, filterObjID, filterFieldID, isFiltered] = + key.split("."); + const filterValue = filters[key]; + const contentAtDisplayFieldKeys = [contentDisplayFieldKeys.shift()]; + let currentAtDisplayFieldKey = contentAtDisplayFieldKeys[0]; + while ( + filterAtDisplay === currentAtDisplayFieldKey[0] && + contentDisplayedFields[currentAtDisplayFieldKey] !== + filterFieldID && + contentDisplayFieldKeys.length > 0 + ) { + currentAtDisplayFieldKey = contentDisplayFieldKeys.shift(); + contentAtDisplayFieldKeys.push(currentAtDisplayFieldKey); + } + if ( + isFiltered != 1 || + typeof filterValue !== "string" || + filterValue === "" || + contentAtDisplayFieldKeys.length === 0 + ) + continue; + contentAtDisplayFieldKeys.pop(); + const contentAtDisplayFieldKeysLength = + contentAtDisplayFieldKeys.length; + let filterObj = AB.objectByID(filterObjID); + const getContentDataRecords = async (obj, filterRule) => { + switch (obj.fieldByID(filterRule.key)?.key) { + case "connectObject": + case "user": + break; + default: + const dataRecords = ( + await obj.model().findAll({ + where: { + glue: "and", + rules: [filterRule, contentFieldFilter], + }, + populate: true, + }) + ).data; + for (const dataRecord of dataRecords) { + if ( + contentDataRecords.findIndex( + (contentDataRecord) => + contentDataRecord.id === dataRecord.id + ) > -1 + ) + continue; + contentDataRecords.push(dataRecord); + } + isContentFiltered = true; + break; + } + }; + if (contentAtDisplayFieldKeysLength === 0) { + await getContentDataRecords(filterObj, { + key: filterFieldID, + rule: "contains", + value: filterValue, + }); + continue; + } + const prevDisplayFieldKey = + contentAtDisplayFieldKeys[contentAtDisplayFieldKeysLength - 1]; + const prevContentDisplayLinkedField = + (prevDisplayFieldKey !== "" && + AB.objectByID(prevDisplayFieldKey.split(".")[1]).fieldByID( + contentDisplayedFields[prevDisplayFieldKey] + )?.fieldLink) || + null; + let filteredRecordDataPKs = ( + await filterObj.model().findAll({ + where: { + glue: "and", + rules: [ + { + key: filterFieldID, + rule: "contains", + value: filterValue, + }, + ], + }, + populate: true, + }) + ).data.flatMap( + (record) => record[prevContentDisplayLinkedField.columnName] + ); + while (contentAtDisplayFieldKeys.length > 1) { + if (filteredRecordDataPKs.length === 0) break; + filterObj = AB.objectByID( + contentAtDisplayFieldKeys.pop().split(".")[1] + ); + const filterObjPK = filterObj.PK(); + filteredRecordDataPKs = ( + await filterObj.model().findAll({ + where: { + glue: "and", + rules: [ + { + key: filterObjPK, + rule: "in", + value: filteredRecordDataPKs, + }, + ], + }, + populate: true, + }) + ).data.map((record) => record[filterObjPK]); + } + if (filteredRecordDataPKs.length === 0) continue; + filterObj = AB.objectByID( + contentAtDisplayFieldKeys.pop().split(".")[1] + ); + await getContentDataRecords(filterObj, { + key: filterObj.PK(), + rule: "in", + value: filteredRecordDataPKs, + }); + isContentFiltered = true; + } + this.AB.performance.measure("loadFilteredContent"); + const contentDataRecordPKs = []; const chartData = (this._chartData = {}); chartData.name = topNode[teamName] ?? ""; chartData.id = this.teamNodeID(topNode.id); - chartData.className = `strategy-${ - topNode[`${strategyField}__relation`]?.[strategyCode] - }`; + const topNodeStrategy = topNode[`${strategyField}__relation`]; + chartData.className = `strategy-${topNodeStrategy?.[strategyCode]}`; + const topNodeStrategyID = topNodeStrategy.id; chartData.isInactive = topNode[teamInactive]; chartData._rawData = topNode; + const topNodeContentFieldData = topNode[contentFieldColumnName]; + if (!isContentFiltered) { + if (Array.isArray(topNodeContentFieldData)) + contentDataRecordPKs.push(...topNodeContentFieldData); + else contentDataRecordPKs.push(topNodeContentFieldData); + } + chartData.filteredOut = isContentFiltered + ? self.filterTeam( + filters, + chartData, + topNodeStrategyID, + topNodeContentFieldData + ) + : self.filterTeam(filters, chartData, topNodeStrategyID); const maxDepth = 10; // prevent inifinite loop - const self = this; /** * Recursive function to prepare child node data * @param {object} node the current node @@ -760,16 +911,28 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { ) return; const strategy = childData[`${strategyField}__relation`]; - - const strategyClass = `strategy-${strategy[strategyCode]}`; const child = { name: childData[teamName], id: self.teamNodeID(id), - className: strategyClass, + className: `strategy-${strategy[strategyCode]}`, isInactive: childData[teamInactive], _rawData: childData, }; - child.filteredOut = self.filterTeam(filters, child, strategy.id); + const childContentFieldData = childData[contentFieldColumnName]; + if (!isContentFiltered) { + if (Array.isArray(childContentFieldData)) + contentDataRecordPKs.push(...childContentFieldData); + else contentDataRecordPKs.push(childContentFieldData); + } + const strategyID = strategy.id; + child.filteredOut = isContentFiltered + ? self.filterTeam( + filters, + child, + strategyID, + childContentFieldData + ) + : self.filterTeam(filters, child, strategyID); if (child.name === "External Support") child.className = `strategy-external`; if (childData[teamLink].length > 0) { @@ -792,27 +955,14 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { return; } pullChildData(chartData); - const contentField = dc?.datasource.fieldByID(settings.contentField); if (contentField == null) return; - const contentFieldColumnName = contentField?.columnName; - const contentObj = contentField?.fieldLink?.object; - const contentDataRecordPKs = []; - const contentDataRecords = (this._cachedContentDataRecords = []); if ( Object.keys(chartData).length > 0 && contentField != null && - contentObj != null + contentObj != null && + !isContentFiltered ) { this.AB.performance.mark("loadAssignments"); - const getContentDataRecordPKs = (node) => { - const contentFieldData = node._rawData[contentFieldColumnName]; - if (Array.isArray(contentFieldData)) - contentDataRecordPKs.push(...contentFieldData); - else contentDataRecordPKs.push(contentFieldData); - const children = node.children || []; - for (const child of children) getContentDataRecordPKs(child); - }; - getContentDataRecordPKs(chartData); contentDataRecords.push( ...( await contentObj.model().findAll({ @@ -820,11 +970,11 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { glue: "and", rules: [ { - key: contentObj.PK(), + key: contentObjPK, rule: "in", value: contentDataRecordPKs, }, - JSON.parse(settings.contentFieldFilter), + contentFieldFilter, ], }, populate: true, @@ -934,9 +1084,9 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { await this.refresh(); } - filterTeam(filters, team, code) { + filterTeam(filters, team, code, contentFieldData) { // Apply filters (match using or) - if (filters.strategy || filters.teamName) { + if (filters.strategy || filters.teamName || contentFieldData != null) { let filter = true; if (filters.strategy !== "" && filters.strategy == code) { filter = false; @@ -947,6 +1097,21 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { ) { filter = false; } + const contentObjPK = this.view.datacollection?.datasource + .fieldByID(this.settings.contentField) + ?.fieldLink?.object.PK(); + if ( + this._cachedContentDataRecords.findIndex((contentDataRecord) => { + if (Array.isArray(contentFieldData)) + return ( + contentFieldData.indexOf(contentDataRecord[contentObjPK]) > + -1 + ); + return contentFieldData === contentDataRecord[contentObjPK]; + }) > -1 + ) { + filter = false; + } return filter; } } From 96bd860523a263f2fadd89bf79401cbf29a14bf8 Mon Sep 17 00:00:00 2001 From: guyyoo Date: Thu, 28 Nov 2024 10:12:36 +0700 Subject: [PATCH 045/129] Mapping data and types --- .../ABViewOrgChartTeamsComponent.js | 168 ++++++++++++++++-- styles/team-widget.css | 34 ++-- 2 files changed, 174 insertions(+), 28 deletions(-) diff --git a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js index fd40888f..145a2397 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js @@ -420,6 +420,9 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { const contentDataRecords = this._cachedContentDataRecords; this.AB.performance.measure("misc"); this.AB.performance.mark("createOrgChart"); + const contentDisplayedFieldTypes = settings.contentDisplayedFieldTypes; + const contentDisplayedFieldMappingData = + settings.contentDisplayedFieldMappingData; const orgchart = new this.OrgChart({ data: chartData, direction: baseView.settings.direction, @@ -445,7 +448,8 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { $content.style.display = "none"; return; } - $node.style.height = "250px"; + $node.style.height = "300px"; + // $node.style.minHeight = "400px"; const averageHeight = 80 / contentGroupOptionsLength; const currentNodeDataRecordPK = data._rawData[nodeObjPK]; const $nodeSpacer = element("div", "spacer"); @@ -519,9 +523,10 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { const displayedFieldKey = contentDisplayedFieldsKeys[j]; const [atDisplay, objID] = displayedFieldKey.split("."); const displayedObj = AB.objectByID(objID); - const displayedField = displayedObj.fieldByID( - contentDisplayedFields[displayedFieldKey] - ); + const displayedFieldID = + contentDisplayedFields[displayedFieldKey]; + const displayedField = + displayedObj.fieldByID(displayedFieldID); switch (objID) { case contentObjID: currentDataRecords = [contentDataRecord]; @@ -574,12 +579,139 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { ); $rowData.appendChild($currentDisplay); const displayedFieldColumnName = displayedField.columnName; - while (currentDataRecords.length > 0) - $currentDisplay.appendChild( - document.createTextNode( - currentDataRecords.pop()[displayedFieldColumnName] - ) - ); + const contentDisplayedFieldTypePrefix = `${displayedFieldKey}.${displayedFieldID}`; + const contentDisplayedFieldMappingDataObj = JSON.parse( + contentDisplayedFieldMappingData?.[ + contentDisplayedFieldTypePrefix + ] || null + ) || {}; + if ( + contentDisplayedFieldTypes[ + `${contentDisplayedFieldTypePrefix}.0` + ] != null + ) { + currentField = null; + continue; + } + switch ( + 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 $div = element( + "div", + "team-group-record-display-svg" + ); + $currentDisplay.appendChild($div); + 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" + ); + $div.appendChild($svg); + $svg.setAttribute("width", "20"); + $svg.setAttribute("height", "20"); + $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 + ) + ); + } + break; + } currentField = null; } } @@ -593,7 +725,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { $buttons.append($editButton, $addButton); const dataID = this.teamRecordID(data.id); const values = this.datacollection.getData( - (e) => e.id === dataID + (e) => e.id == dataID )[0]; $addButton.onclick = () => { this.teamForm("Add", { __parentID: dataID }); @@ -663,7 +795,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { await dc.waitForDataCollectionToInitialize(dc); const panelObj = dc.datasource; const contentFieldID = panelObj.connectFields( - (field) => field.datasourceLink.id === contentObjID + (field) => field.datasourceLink.id == contentObjID )[0].fieldLink.id; dataPanelUIs.push({ header: dataPanelDCs[key], @@ -689,7 +821,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { "data-pk", dc.getData( (e) => - e.id === + e.id == $itemElement.getAttribute( "webix_l_id" ) @@ -812,7 +944,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { if ( contentDataRecords.findIndex( (contentDataRecord) => - contentDataRecord.id === dataRecord.id + contentDataRecord.id == dataRecord.id ) > -1 ) continue; @@ -855,8 +987,10 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { ).data.flatMap( (record) => record[prevContentDisplayLinkedField.columnName] ); - while (contentAtDisplayFieldKeys.length > 1) { - if (filteredRecordDataPKs.length === 0) break; + while ( + contentAtDisplayFieldKeys.length > 1 && + filteredRecordDataPKs.length > 0 + ) { filterObj = AB.objectByID( contentAtDisplayFieldKeys.pop().split(".")[1] ); @@ -1126,7 +1260,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { contentFieldData.indexOf(contentDataRecord[contentObjPK]) > -1 ); - return contentFieldData === contentDataRecord[contentObjPK]; + return contentFieldData == contentDataRecord[contentObjPK]; }) > -1 ) { filter = false; diff --git a/styles/team-widget.css b/styles/team-widget.css index 61e94646..c0e81dfa 100644 --- a/styles/team-widget.css +++ b/styles/team-widget.css @@ -6,7 +6,7 @@ org-chart .node { text-align: center; justify-content: center; width: 325px !important; - /* height: 250px; */ + /* height: 600px !important; */ font-family: "Jomhuria", sans-serif; font-style: normal; font-weight: 200; @@ -79,7 +79,7 @@ org-chart tr.lines .downLine { } .team-group-title { - width: 100%; + width: 100%; } .team-group-content { @@ -103,15 +103,27 @@ org-chart tr.lines .downLine { } .team-group-record-display { - } -.team-group-record-display-data { +.team-group-record-display > img { + width: 60px; + height: 60px; +} +.team-group-record-display-svg { + width: 30px; + height: 30px; + padding: 5px; + background-color: skyblue; + border-radius: 10px; +} +.team-group-record-display-svg svg { + height: 20px; + width: 20px; } .team-group-record:hover { - background-color: yellow; + background-color: yellow; } .team-button-section { @@ -154,9 +166,9 @@ org-chart tr.lines .downLine { .filter-button { border-radius: 10px; - background: #FFF; + background: #fff; box-shadow: 0px 4px 4px 0px rgba(0, 0, 0, 0.25); - color: #2F27CE; + color: #2f27ce; font-family: Roboto; font-size: 20px; font-style: normal; @@ -167,19 +179,19 @@ org-chart tr.lines .downLine { } .filter-button:hover { - background: #2F27CE; - color: #FFF; + background: #2f27ce; + color: #fff; } .filter-popup { border-radius: 10px; - background: #FFF; + background: #fff; box-shadow: 0px 4px 4px 0px rgba(0, 0, 0, 0.25); } .active-text { color: #ffffff; - font-family: 'Jomhuria', sans-serif; + font-family: "Jomhuria", sans-serif; height: 6.5px; font-size: 8px; font-weight: 400; From 3add1c2637693e93819c8da1df61186c96c8d8cb Mon Sep 17 00:00:00 2001 From: guyyoo Date: Tue, 3 Dec 2024 09:55:19 +0700 Subject: [PATCH 046/129] Add start date and end date fields --- .../ABViewOrgChartTeamsComponent.js | 63 +++++++++++++++---- styles/team-widget.css | 1 + 2 files changed, 52 insertions(+), 12 deletions(-) diff --git a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js index 145a2397..e15082d4 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js @@ -121,6 +121,12 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { 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 ); @@ -185,16 +191,27 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { $group.parentElement.parentElement.dataset.source )._rawData[nodeObjPK]; let updatedData = dataTransfer.getData("source"); - this.__orgchart.innerHTML = ""; + const orgchart = this.__orgchart; if (updatedData === "") { const dataPK = dataTransfer.getData("dataPK"); const contentLinkedFieldID = dataTransfer.getData( "contentLinkedFieldId" ); - const contentLinkedField = - contentObj.fieldByID(contentLinkedFieldID); + const contentLinkedFieldColumnName = + contentObj.fieldByID(contentLinkedFieldID).columnName; + const pendingPromises = []; + const newDate = new Date(); + orgchart.querySelectorAll(".team-group-record").forEach((e) => { + const contentData = JSON.parse(e.dataset.source); + contentData[contentDateEndFieldColumnName] = newDate; + contentData[contentLinkedFieldColumnName] === dataPK && + pendingPromises.push( + contentModel.update(contentData.id, contentData) + ); + }); updatedData = {}; - updatedData[contentLinkedField.columnName] = dataPK; + updatedData[contentDateStartFieldColumnName] = newDate; + updatedData[contentLinkedFieldColumnName] = dataPK; updatedData[contentFieldLinkColumnName] = newNodeDataPK; updatedData[contentGroupByFieldColumnName] = newGroupDataPK; if (this.entityDC) { @@ -204,18 +221,29 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { const entityCol = this.AB.definitionByID(entityLink).columnName; updatedData[entityCol] = this.entityDC.getCursor(); } - await contentModel.create(updatedData); + pendingPromises.push(contentModel.create(updatedData)); + await Promise.all(pendingPromises); } else { + orgchart.innerHTML = ""; updatedData = JSON.parse(updatedData); delete updatedData["created_at"]; delete updatedData["updated_at"]; delete updatedData["properties"]; if (dropContentToCreate) { + const pendingPromises = []; + updatedData[contentDateEndFieldColumnName] = new Date(); + pendingPromises.push( + contentModel.update(updatedData.id, updatedData) + ); + updatedData[contentDateStartFieldColumnName] = + updatedData[contentDateEndFieldColumnName]; delete updatedData["id"]; delete updatedData["uuid"]; + delete updatedData[contentDateEndFieldColumnName]; updatedData[contentFieldLinkColumnName] = newNodeDataPK; updatedData[contentGroupByFieldColumnName] = newGroupDataPK; - await contentModel.create(updatedData); + pendingPromises.push(contentModel.create(updatedData)); + await Promise.all(pendingPromises); } else { updatedData[contentFieldLinkColumnName] = newNodeDataPK; updatedData[contentGroupByFieldColumnName] = newGroupDataPK; @@ -361,9 +389,19 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { ) ) { this.__orgchart.innerHTML = ""; + const pendingPromises = []; + const oldData = {}; + oldData[contentDateEndFieldColumnName] = new Date(); + pendingPromises.push( + contentModel.update(newFormData.id, oldData) + ); + newFormData[contentDateStartFieldColumnName] = + oldData[contentDateEndFieldColumnName]; delete newFormData["id"]; delete newFormData["uuid"]; - await contentModel.create(newFormData); + delete newFormData[contentDateEndFieldColumnName]; + pendingPromises.push(contentModel.create(newFormData)); + await Promise.all(pendingPromises); $contentForm.hide(); await this.refresh(); return; @@ -580,11 +618,12 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { $rowData.appendChild($currentDisplay); const displayedFieldColumnName = displayedField.columnName; const contentDisplayedFieldTypePrefix = `${displayedFieldKey}.${displayedFieldID}`; - const contentDisplayedFieldMappingDataObj = JSON.parse( - contentDisplayedFieldMappingData?.[ - contentDisplayedFieldTypePrefix - ] || null - ) || {}; + const contentDisplayedFieldMappingDataObj = + JSON.parse( + contentDisplayedFieldMappingData?.[ + contentDisplayedFieldTypePrefix + ] || null + ) || {}; if ( contentDisplayedFieldTypes[ `${contentDisplayedFieldTypePrefix}.0` diff --git a/styles/team-widget.css b/styles/team-widget.css index c0e81dfa..e3210bea 100644 --- a/styles/team-widget.css +++ b/styles/team-widget.css @@ -116,6 +116,7 @@ org-chart tr.lines .downLine { padding: 5px; background-color: skyblue; border-radius: 10px; + display: inline-block; } .team-group-record-display-svg svg { height: 20px; From 900d34df176b09e2fa973631d48e93b0e7b39309 Mon Sep 17 00:00:00 2001 From: guyyoo Date: Wed, 4 Dec 2024 11:12:03 +0700 Subject: [PATCH 047/129] UI improvements --- .../ABViewOrgChartTeamsComponent.js | 124 +++++++++++++++--- styles/team-widget.css | 59 ++++++--- 2 files changed, 147 insertions(+), 36 deletions(-) diff --git a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js index e15082d4..6abbabb0 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js @@ -147,6 +147,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { const contentDisplayedFields = settings.contentDisplayedFields; const contentDisplayedFieldsKeys = Object.keys(contentDisplayedFields); const contentModel = contentObj?.model(); + const strategyColors = settings.strategyColors; const ids = this.ids; const callAfterRender = (callback) => { requestAnimationFrame(() => { @@ -418,21 +419,26 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { id: ids.contentForm, close: true, position: "center", + css: { "border-radius": "10px" }, body: { + width: 450, rows: [ { view: "toolbar", id: "myToolbar", + css: "webix_dark", cols: [ { view: "label", label: L("Edit Content"), - align: "left", + align: "center", }, { view: "button", value: "X", + width: 60, align: "right", + css: "webix_transparent", click: () => { $$(ids.contentForm).hide(); }, @@ -492,16 +498,23 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { const currentNodeDataRecordPK = data._rawData[nodeObjPK]; const $nodeSpacer = element("div", "spacer"); $content.appendChild($nodeSpacer); - $nodeSpacer.style.backgroundColor = contentGroupOptions[0].hex; + const nodeSpacerStyle = $nodeSpacer.style; + nodeSpacerStyle.backgroundColor = ""; + const strategyColor = + strategyColors[$node.classList.item(1).replace("strategy-", "")]; for (const group of contentGroupOptions) { const $group = element("div", "team-group-section"); $content.appendChild($group); const groupStyle = $group.style; groupStyle["height"] = `${averageHeight}%`; + // 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]); @@ -545,6 +558,8 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { JSON.stringify(contentDataRecord) ); $groupContent.appendChild($rowData); + const rowDataStyle = $rowData.style; + rowDataStyle["borderColor"] = strategyColor; $rowData.addEventListener("click", async () => { await showContentForm(contentDataRecord); }); @@ -553,10 +568,20 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { $rowData.addEventListener("dragstart", fnContentDragStart); $rowData.addEventListener("dragend", fnContentDragEnd); } - const rowDataStyle = $rowData.style; - rowDataStyle["borderColor"] = "#EF3340"; let currentDataRecords = []; let currentField = null; + + // 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"), + ]; + const $hardcodedSpecialDisplay = element( + "div", + "team-group-record-display" + ); + let currentDisplayIndex = 0; for (let j = 0; j < contentDisplayedFieldsKeys.length; j++) { const displayedFieldKey = contentDisplayedFieldsKeys[j]; const [atDisplay, objID] = displayedFieldKey.split("."); @@ -615,7 +640,34 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { "div", "team-group-record-display" ); - $rowData.appendChild($currentDisplay); + + // TODO (Guy): Now we are hardcoding for each display. + // $rowData.appendChild($currentDisplay); + switch (currentDisplayIndex) { + case 0: + hardcodedDisplays[0].appendChild($currentDisplay); + break; + case 1: + 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 displayedFieldColumnName = displayedField.columnName; const contentDisplayedFieldTypePrefix = `${displayedFieldKey}.${displayedFieldID}`; const contentDisplayedFieldMappingDataObj = @@ -628,10 +680,8 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { contentDisplayedFieldTypes[ `${contentDisplayedFieldTypePrefix}.0` ] != null - ) { - currentField = null; - continue; - } + ) + $currentDisplay.style.display = "none"; switch ( contentDisplayedFieldTypes[ `${contentDisplayedFieldTypePrefix}.1` @@ -663,20 +713,13 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { const currentDataRecordID = currentDataRecord.id; const currentDataRecordValue = currentDataRecord[displayedFieldColumnName]; - const $div = element( - "div", - "team-group-record-display-svg" - ); - $currentDisplay.appendChild($div); 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" ); - $div.appendChild($svg); - $svg.setAttribute("width", "20"); - $svg.setAttribute("height", "20"); + $currentDisplay.appendChild($svg); $svg.setAttribute("viewBox", "0 0 6 6"); $svg.setAttribute("fill", "none"); $svg.setAttribute("xmlns", SVG_NS); @@ -753,6 +796,41 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { } 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]; + $rowData.appendChild($hardcodedDisplay); + const children = $hardcodedDisplay.children; + let isShown = false; + let j = 0; + switch (i) { + case 1: + const child = children.item(j); + const grandChildren = child.children; + const 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 $buttons = element("div", "team-button-section"); @@ -1068,7 +1146,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { chartData.id = this.teamNodeID(topNode.id); const topNodeStrategy = topNode[`${strategyField}__relation`]; chartData.className = `strategy-${topNodeStrategy?.[strategyCode]}`; - const topNodeStrategyID = topNodeStrategy.id; + const topNodeStrategyID = topNodeStrategy?.id; chartData.isInactive = topNode[teamInactive]; chartData._rawData = topNode; const topNodeContentFieldData = topNode[contentFieldColumnName]; @@ -1106,7 +1184,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { const child = { name: childData[teamName], id: self.teamNodeID(id), - className: `strategy-${strategy[strategyCode]}`, + className: `strategy-${strategy?.[strategyCode]}`, isInactive: childData[teamInactive], _rawData: childData, }; @@ -1116,7 +1194,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { contentDataRecordPKs.push(...childContentFieldData); else contentDataRecordPKs.push(childContentFieldData); } - const strategyID = strategy.id; + const strategyID = strategy?.id; child.filteredOut = isContentFiltered ? self.filterTeam( filters, @@ -1456,22 +1534,26 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { id: this.ids.teamFormPopup, close: true, position: "center", + css: { "border-radius": "10px" }, body: { rows: [ { view: "toolbar", id: "myToolbar", + css: "webix_dark", cols: [ { view: "label", label: `Edit Team`, - align: "left", + align: "center", id: this.ids.teamFormTitle, }, { view: "button", value: "X", align: "right", + width: 60, + css: "webix_transparent", click: () => $teamFormPopup.hide(), }, ], diff --git a/styles/team-widget.css b/styles/team-widget.css index e3210bea..cc4ac767 100644 --- a/styles/team-widget.css +++ b/styles/team-widget.css @@ -87,44 +87,73 @@ org-chart tr.lines .downLine { width: 100%; display: flex; flex-direction: column; - gap: 5%; + gap: 10%; overflow: auto; } .team-group-record { width: 100%; - border-width: 1%; + border-width: 4px; border-style: solid; - border-radius: 5px; + border-radius: 12px; + border-color: #ef3340; background: #ffffff; display: flex; flex-direction: row; + align-items: center; justify-content: center; + gap: 8%; } -.team-group-record-display { +.team-group-record:hover { + filter: brightness(0.75); } -.team-group-record-display > img { - width: 60px; - height: 60px; +.team-group-record-display { + padding: 5px; + height: 20px; + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; } -.team-group-record-display-svg { +.team-group-record-display img { + border-radius: 50%; width: 30px; height: 30px; - padding: 5px; +} + +.team-group-record-display svg { + margin: 0px 3px; + width: 12px; + height: 12px; + padding: 2px 8px; background-color: skyblue; - border-radius: 10px; + border-radius: 5px; display: inline-block; } -.team-group-record-display-svg svg { - height: 20px; - width: 20px; + +/* TODO (Guy): Now we are hardcoding for each display. */ +.team-group-record-display-hardcode { + display: flex; + flex-direction: row; +} +.display-block { + display: inline-block; } -.team-group-record:hover { - background-color: yellow; +.display-block.display-block-right { + height: 30px; + width: 80px; + padding: 5px 0px; + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + background-color: #036; + border-radius: 10px; + color: #fff; } .team-button-section { From c9a56f5a8ed3009b9c40497cece2c6d32d8b7654 Mon Sep 17 00:00:00 2001 From: nh758 <7259@pm.me> Date: Wed, 27 Nov 2024 15:52:17 +0700 Subject: [PATCH 048/129] fix strategy on add/edit team --- .../ABViewOrgChartTeamsComponent.js | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js index e15082d4..163d156c 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js @@ -1346,7 +1346,8 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { values[cName] = entity; } } - const { id } = await this.datacollection.model.create(values); + const _rawData = await this.datacollection.model.create(values); + const id = _rawData.id; const linkField = this.AB.definitionByID( this.getSettingField("teamLink").settings.linkColumn @@ -1355,12 +1356,18 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { const parent = document.querySelector( `#${this.teamNodeID(values[linkField])}` ); + + const strategyLink = this.getSettingField("teamStrategy").columnName; + const strategyField = this.getSettingField("strategyCode").columnName; + const strategyCode = _rawData[`${strategyLink}__relation`][strategyField]; + const hasChild = parent.parentNode.colSpan > 1; const newChild = { name: values[nameField], id: this.teamNodeID(id), relationship: hasChild ? "110" : "100", - className: `strategy-${strategy.text}`, + className: `strategy-${strategyCode}`, + _rawData, }; // Need to add differently if the node already has child nodes @@ -1414,14 +1421,19 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { }); } - teamEdit(values, strategy) { - this.datacollection.model.update(values.id, values).catch((err) => { + async teamEdit(values, strategy) { + const strategyLink = this.getSettingField("teamStrategy").columnName; + const strategyField = this.getSettingField("strategyCode").columnName; + const strategyCode = strategy[strategyField]; + values[strategyLink] = strategyCode; + + await this.datacollection.model.update(values.id, values).catch((err) => { //TODO }); const nodeID = this.teamNodeID(values.id); const node = document.querySelector(`#${nodeID}`); const currentStrategy = node.classList?.value?.match(/strategy-\S+/)[0]; - const newStrategy = `strategy-${strategy.text}`; + const newStrategy = `strategy-${strategyCode}`; if (currentStrategy !== newStrategy) { node.classList?.remove(currentStrategy); node.classList?.add(newStrategy); From 86035dedd1b98136124484f28e2e946114cfd92c Mon Sep 17 00:00:00 2001 From: nh758 <7259@pm.me> Date: Thu, 28 Nov 2024 09:40:27 +0700 Subject: [PATCH 049/129] fix can inactivate logic --- .../ABViewOrgChartTeamsComponent.js | 25 ++++++++++++------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js index 163d156c..8ae51d6d 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js @@ -22,7 +22,9 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { ids ) ); - this.__filters = {}; + this.__filters = { + inactive: 0, + }; this._chartData = null; this._cachedContentDataRecords = null; } @@ -1387,7 +1389,14 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { const canInactive = this.getSettingField("teamCanInactivate").columnName; if (!values[canInactive]) return false; const children = this.getSettingField("teamLink").columnName; - if (values[children].length > 0) return false; + if ( + values[children].some( + (c) => + this.datacollection.getData((r) => r.id == c)[0]?.[isInactive] == + false + ) + ) + return false; // @TODO check for active assignment // if (hasActiveAssignment) return false; return true; @@ -1425,8 +1434,8 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { const strategyLink = this.getSettingField("teamStrategy").columnName; const strategyField = this.getSettingField("strategyCode").columnName; const strategyCode = strategy[strategyField]; - values[strategyLink] = strategyCode; - + values[strategyLink] = strategy.id; + delete values[`${strategyLink}__relation`]; await this.datacollection.model.update(values.id, values).catch((err) => { //TODO }); @@ -1440,11 +1449,9 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { } const inactive = this.getSettingField("teamInactive").columnName; - if ( - this.__filters.inactive && - this.__filters.inactive === 0 && - values[inactive] - ) { + // Remove inactive node from display, unless the filter setting to show + // inctive nodes is on. + if (this.__filters?.inactive !== 1 && values[inactive] === 1) { this.__orgchart.removeNodes(node); } const nameCol = this.getSettingField("teamName").columnName; From b1c801191a971274da2f2ede8f1a1fa5d69e6c84 Mon Sep 17 00:00:00 2001 From: guyyoo Date: Fri, 6 Dec 2024 15:07:42 +0700 Subject: [PATCH 050/129] Fix label and fix limit edit form's fields --- .../ABViewOrgChartTeamsComponent.js | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js index 8c3f3262..60aa03c3 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js @@ -258,10 +258,15 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { }; const editContentFieldsToCreateNew = settings.editContentFieldsToCreateNew; + const setEditableContentFields = settings.setEditableContentFields; + const contentEditableFields = contentObj.fields( + (field) => setEditableContentFields.indexOf(field.id) > -1 + ); const showContentForm = async (contentDataRecord) => { const rules = {}; + const labelWidth = 200; const contentFormElements = await Promise.all( - contentObj.fields().map(async (field) => { + contentEditableFields.map(async (field) => { const fieldKey = field.key; const fieldName = field.columnName; @@ -275,12 +280,14 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { view: "checkbox", name: fieldName, label: fieldLabel, + labelWidth, }; case "number": return { view: "counter", name: fieldName, label: fieldLabel, + labelWidth, type: "number", }; case "list": @@ -290,6 +297,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { "combo", name: fieldName, label: fieldLabel, + labelWidth, options: settings.options.map((option) => ({ id: option.id, value: option.text, @@ -311,12 +319,14 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { view: "combo", name: fieldName, label: fieldLabel, + labelWidth, options, } : { view: "multicombo", name: fieldName, label: fieldLabel, + labelWidth, stringResult: false, labelAlign: "left", options, @@ -327,8 +337,8 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { view: "datepicker", name: fieldName, label: fieldLabel, + labelWidth, timepicker: fieldKey === "datetime", - width: 300, }; case "file": case "image": @@ -337,6 +347,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { // view: "", name: fieldName, label: fieldLabel, + labelWidth, }; // case "json": // case "LongText": @@ -347,6 +358,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { view: "text", name: fieldName, label: fieldLabel, + labelWidth, }; } }) @@ -423,7 +435,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { position: "center", css: { "border-radius": "10px" }, body: { - width: 450, + width: 600, rows: [ { view: "toolbar", @@ -432,7 +444,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { cols: [ { view: "label", - label: L("Edit Content"), + label: `${L("Edit")} ${contentObj.label}`, align: "center", }, { From 683f1a6a0046073a193feef860434a88651a379c Mon Sep 17 00:00:00 2001 From: guyyoo Date: Mon, 9 Dec 2024 10:09:02 +0700 Subject: [PATCH 051/129] Fix the edit assignment issue --- .../ABViewOrgChartTeamsComponent.js | 215 ++++++++++-------- 1 file changed, 117 insertions(+), 98 deletions(-) diff --git a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js index 60aa03c3..66af9b94 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js @@ -266,102 +266,106 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { const rules = {}; const labelWidth = 200; const contentFormElements = await Promise.all( - contentEditableFields.map(async (field) => { - const fieldKey = field.key; - const fieldName = field.columnName; + contentEditableFields + .filter( + (field) => contentDataRecord[field.columnName] !== undefined + ) + .map(async (field) => { + const fieldKey = field.key; + const fieldName = field.columnName; - // TODO (Guy): Add validators. - rules[fieldName] = () => true; - const fieldLabel = field.label; - const settings = field.settings; - switch (fieldKey) { - case "boolean": - return { - view: "checkbox", - name: fieldName, - label: fieldLabel, - labelWidth, - }; - case "number": - return { - view: "counter", - name: fieldName, - label: fieldLabel, - labelWidth, - type: "number", - }; - 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, - })), - }; - case "user": - case "connectObject": - const fieldLinkObj = field.datasourceLink; + // TODO (Guy): Add validators. + rules[fieldName] = () => true; + const fieldLabel = field.label; + const settings = field.settings; + switch (fieldKey) { + case "boolean": + return { + view: "checkbox", + name: fieldName, + label: fieldLabel, + labelWidth, + }; + case "number": + return { + view: "counter", + name: fieldName, + label: fieldLabel, + labelWidth, + type: "number", + }; + 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, + })), + }; + case "user": + case "connectObject": + const fieldLinkObj = field.datasourceLink; - // TODO (Guy): Fix pulling all connections. - const options = ( - await fieldLinkObj.model().findAll() - ).data.map((e) => ({ - id: e.id, - value: fieldLinkObj.displayData(e), - })); - return field.linkType() === "one" - ? { - view: "combo", - name: fieldName, - label: fieldLabel, - labelWidth, - options, - } - : { - view: "multicombo", - name: fieldName, - label: fieldLabel, - labelWidth, - stringResult: false, - labelAlign: "left", - options, - }; - case "date": - case "datetime": - return { - view: "datepicker", - name: fieldName, - label: fieldLabel, - labelWidth, - timepicker: fieldKey === "datetime", - }; - case "file": - case "image": - // TODO (Guy): Add logic - return { - // view: "", - name: fieldName, - label: fieldLabel, - labelWidth, - }; - // case "json": - // case "LongText": - // case "string": - // case "email": - default: - return { - view: "text", - name: fieldName, - label: fieldLabel, - labelWidth, - }; - } - }) + // TODO (Guy): Fix pulling all connections. + const options = ( + await fieldLinkObj.model().findAll() + ).data.map((e) => ({ + id: e.id, + value: fieldLinkObj.displayData(e), + })); + return field.linkType() === "one" + ? { + view: "combo", + name: fieldName, + label: fieldLabel, + labelWidth, + options, + } + : { + view: "multicombo", + name: fieldName, + label: fieldLabel, + labelWidth, + stringResult: false, + labelAlign: "left", + options, + }; + case "date": + case "datetime": + return { + view: "datepicker", + name: fieldName, + label: fieldLabel, + labelWidth, + timepicker: fieldKey === "datetime", + }; + case "file": + case "image": + // TODO (Guy): Add logic + return { + // view: "", + name: fieldName, + label: fieldLabel, + labelWidth, + }; + // case "json": + // case "LongText": + // case "string": + // case "email": + default: + return { + view: "text", + name: fieldName, + label: fieldLabel, + labelWidth, + }; + } + }) ); contentFormElements.push({ view: "button", @@ -372,14 +376,29 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { if (!$contentFormData.validate()) return; const newFormData = $contentFormData.getValues(); let isDataChanged = false; - for (const key in newFormData) + for (const key in newFormData) { + const oldValue = contentDataRecord[key]; + const newValue = newFormData[key]; + switch (typeof oldValue) { + case "boolean": + if (newValue == 0) newFormData[key] = false; + else newFormData[key] = true; + break; + case "number": + newFormData[key] = parseInt(newValue); + break; + case "string": + newFormData[key] = newValue?.toString(); + break; + default: + break; + } if ( JSON.stringify(newFormData[key]) !== JSON.stringify(contentDataRecord[key]) - ) { + ) isDataChanged = true; - break; - } + } const $contentForm = $$(ids.contentForm); if (!isDataChanged) { $contentForm.hide(); From 27f457a3f8404db3ce5cf478b2d96f5ca8370f09 Mon Sep 17 00:00:00 2001 From: nh758 <7259@pm.me> Date: Fri, 6 Dec 2024 15:13:23 +0700 Subject: [PATCH 052/129] fix only show sstrategies from the selected entity --- .../ABViewOrgChartTeamsComponent.js | 30 +++++++++++++++++-- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js index 66af9b94..ecd5a831 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js @@ -99,8 +99,12 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { ); if (!this._entityChangeListener) { // Reload the Chart if our Entity changes - this._entityChangeListener = this.entityDC.on("changeCursor", () => - this.refresh() + this._entityChangeListener = this.entityDC.on( + "changeCursor", + () => { + delete this.entitySrategyOptions; + this.refresh(); + } ); } } @@ -1578,7 +1582,27 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { const [strategyField] = this.datacollection.datasource.fields( (f) => f.id == this.view.settings["teamStrategy"] ); - const strategyOptions = await strategyField.getOptions(); + if (!this.entityStategyOptions) { + const strategyObj = this.AB.objectByID( + strategyField.settings.linkObject + ); + const entityLink = strategyObj.connectFields( + (f) => f.settings.linkObject === this.entityDC.datasource.id + )[0]; + const cond = { + glue: "and", + rules: [ + { + key: entityLink.columnName, + value: this.entityDC.getCursor().id, + rule: "equals", + }, + ], + }; + this.entitySrategyOptions = await strategyField.getOptions(cond); + } + const strategyOptions = this.entitySrategyOptions; + $teamFormPopup = webix.ui({ view: "popup", id: this.ids.teamFormPopup, From 6cf49d0913159d92955c953ad1edc6b92ec16723 Mon Sep 17 00:00:00 2001 From: nh758 <7259@pm.me> Date: Fri, 6 Dec 2024 15:14:17 +0700 Subject: [PATCH 053/129] style edit team form to match widget --- .../ABViewOrgChartTeamsComponent.js | 83 +++++++++++++------ styles/team-widget.css | 16 +++- 2 files changed, 71 insertions(+), 28 deletions(-) diff --git a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js index ecd5a831..a511666f 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js @@ -1608,26 +1608,39 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { id: this.ids.teamFormPopup, close: true, position: "center", - css: { "border-radius": "10px" }, + css: { "border-radius": "15px" }, body: { + type: "clean", rows: [ { view: "toolbar", id: "myToolbar", - css: "webix_dark", + borderless: true, + css: { + background: "#1a3e72", + }, cols: [ + { width: 5 }, { - view: "label", - label: `Edit Team`, + view: "template", + template: "#value#", align: "center", + height: 40, + css: { + "font-weight": 400, + "font-size": "32px", + "font-family": `"Jomhuria", sans-serif`, + color: "white", + background: "transparent", + border: "none", + }, id: this.ids.teamFormTitle, }, { - view: "button", - value: "X", + view: "icon", + icon: "fa fa-times", align: "right", width: 60, - css: "webix_transparent", click: () => $teamFormPopup.hide(), }, ], @@ -1635,11 +1648,13 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { { view: "form", id: this.ids.teamForm, + borderless: true, elements: [ { view: "text", label: nameField.label ?? nameField.columnName, name: nameField.columnName, + required: true, }, { view: "richselect", @@ -1647,6 +1662,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { strategyField.label ?? strategyField.columnName, name: strategyField.columnName, options: strategyOptions.map(fieldToOption), + required: true, }, { view: "switch", @@ -1657,23 +1673,35 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { { view: "text", name: "id", hidden: true }, { view: "text", name: linkField, hidden: true }, { - view: "button", - id: this.ids.teamFormSubmit, - value: "Add", - css: "webix_primary", - click: () => { - const values = $$(this.ids.teamForm).getValues(); - const strategy = strategyOptions.find( - (f) => - f.id === values[strategyField.columnName] - ); - if (values.id) { - this.teamEdit(values, strategy); - } else { - this.teamAddChild(values, strategy); - } - $teamFormPopup.hide(); - }, + cols: [ + {}, + { + view: "template", + id: this.ids.teamFormSubmit, + template: "#value#", + height: 31, + css: "team-form-button", + onClick: { + "team-form-button": () => { + const values = $$( + this.ids.teamForm + ).getValues(); + const strategy = strategyOptions.find( + (f) => + f.id === + values[strategyField.columnName] + ); + if (values.id) { + this.teamEdit(values, strategy); + } else { + this.teamAddChild(values, strategy); + } + $teamFormPopup.hide(); + }, + }, + }, + {}, + ], }, ], }, @@ -1685,9 +1713,10 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { values[linkField] = values.__parentID; delete values.__parentID; } - $$(this.ids.teamFormTitle).define("label", `${mode} Team`); - $$(this.ids.teamFormTitle).refresh(); - $$(this.ids.teamFormSubmit).setValue(mode); + $$(this.ids.teamFormTitle).setValues({ + value: `${this.label(mode)} Team`, + }); + $$(this.ids.teamFormSubmit).setValues({ value: this.label("Submit") }); $$(this.ids.teamForm).setValues(values); this.teamCanInactivate(values) ? $$(this.ids.teamFormInactive).enable() diff --git a/styles/team-widget.css b/styles/team-widget.css index cc4ac767..ca134188 100644 --- a/styles/team-widget.css +++ b/styles/team-widget.css @@ -178,7 +178,7 @@ org-chart tr.lines .downLine { border-radius: 5px; cursor: pointer; font-size: 8px; - color: white; + color: white !important; } .team-chart-toolbar { @@ -237,3 +237,17 @@ org-chart tr.lines .downLine { background: grey; cursor: default; } + +.team-form-button { + text-align: center; + font-weight: 400; + font-size: 17px; + font-family: "Roboto" sans-serif; + color: white; + background: #1a3e72; + border-radius: 10px; +} + +.team-form-button:hover { + background: #0e2341; +} From 0461163dad393ee91429bd3ca171d7fb81e2850d Mon Sep 17 00:00:00 2001 From: nh758 <7259@pm.me> Date: Fri, 6 Dec 2024 16:13:48 +0700 Subject: [PATCH 054/129] disable team edit/add submit until form is valid --- .../ABViewOrgChartTeamsComponent.js | 28 +++++++++++++++++-- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js index a511666f..d505756c 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js @@ -1678,9 +1678,12 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { { view: "template", id: this.ids.teamFormSubmit, - template: "#value#", + template: ``, + data: { disabled: "disabled" }, + borderless: true, height: 31, - css: "team-form-button", onClick: { "team-form-button": () => { const values = $$( @@ -1704,6 +1707,22 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { ], }, ], + on: { + onChange: () => { + const values = $$(this.ids.teamForm).getValues(); + const valid = + !!values[strategyField.columnName] && + !!values[nameField.columnName]; + if (valid) + $$(this.ids.teamFormSubmit).setValues({ + disabled: "", + }); + else + $$(this.ids.teamFormSubmit).setValues({ + disabled: "disabled", + }); + }, + }, }, ], }, @@ -1716,8 +1735,11 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { $$(this.ids.teamFormTitle).setValues({ value: `${this.label(mode)} Team`, }); - $$(this.ids.teamFormSubmit).setValues({ value: this.label("Submit") }); $$(this.ids.teamForm).setValues(values); + $$(this.ids.teamFormSubmit).setValues({ + disabled: "disabled", + }); + this.teamCanInactivate(values) ? $$(this.ids.teamFormInactive).enable() : $$(this.ids.teamFormInactive).disable(); From 1077b1397677d81d58946f4185222d0ec771a084 Mon Sep 17 00:00:00 2001 From: nh758 <7259@pm.me> Date: Mon, 9 Dec 2024 09:14:50 +0700 Subject: [PATCH 055/129] use sub strategy code for strategy label --- .../platform/dataFields/ABFieldConnect.js | 4 +- .../ABViewOrgChartTeamsComponent.js | 53 +++++++++++-------- styles/team-widget.css | 5 ++ 3 files changed, 39 insertions(+), 23 deletions(-) diff --git a/AppBuilder/platform/dataFields/ABFieldConnect.js b/AppBuilder/platform/dataFields/ABFieldConnect.js index ad3bd226..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, }); }; diff --git a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js index d505756c..d2a98818 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js @@ -102,7 +102,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { this._entityChangeListener = this.entityDC.on( "changeCursor", () => { - delete this.entitySrategyOptions; + $$(this.ids.teamFormPopup)?.destructor(); this.refresh(); } ); @@ -1582,26 +1582,37 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { const [strategyField] = this.datacollection.datasource.fields( (f) => f.id == this.view.settings["teamStrategy"] ); - if (!this.entityStategyOptions) { - const strategyObj = this.AB.objectByID( - strategyField.settings.linkObject - ); - const entityLink = strategyObj.connectFields( - (f) => f.settings.linkObject === this.entityDC.datasource.id - )[0]; - const cond = { - glue: "and", - rules: [ - { - key: entityLink.columnName, - value: this.entityDC.getCursor().id, - rule: "equals", - }, - ], + const strategyObj = this.AB.objectByID( + strategyField.settings.linkObject + ); + const entityLink = strategyObj.connectFields( + (f) => f.settings.linkObject === this.entityDC.datasource.id + )[0]; + const cond = { + glue: "and", + rules: [ + { + key: entityLink.columnName, + value: this.entityDC.getCursor().id, + rule: "equals", + }, + ], + }; + const subCol = this.getSettingField("subStrategy").columnName; + this.entitySrategyOptions = await strategyField.getOptions( + cond, + null, + null, + null, + [subCol] + ); + + const strategyOptions = this.entitySrategyOptions.map((o) => { + return { + id: o.id, + value: o[`${subCol}__relation`].name, }; - this.entitySrategyOptions = await strategyField.getOptions(cond); - } - const strategyOptions = this.entitySrategyOptions; + }); $teamFormPopup = webix.ui({ view: "popup", @@ -1661,7 +1672,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { label: strategyField.label ?? strategyField.columnName, name: strategyField.columnName, - options: strategyOptions.map(fieldToOption), + options: strategyOptions, required: true, }, { diff --git a/styles/team-widget.css b/styles/team-widget.css index ca134188..a77d60d8 100644 --- a/styles/team-widget.css +++ b/styles/team-widget.css @@ -239,6 +239,7 @@ org-chart tr.lines .downLine { } .team-form-button { + padding: 3px 10px; text-align: center; font-weight: 400; font-size: 17px; @@ -251,3 +252,7 @@ org-chart tr.lines .downLine { .team-form-button:hover { background: #0e2341; } + +.team-form-button:disabled { + background: #AAA; +} From 002fc2496964f0419e34f2919ba3207fcd833b7a Mon Sep 17 00:00:00 2001 From: nh758 <7259@pm.me> Date: Mon, 9 Dec 2024 13:50:29 +0700 Subject: [PATCH 056/129] fix filter by strategy and cleanup filter ui --- .../ABViewOrgChartTeamsComponent.js | 50 +++++++++++-------- styles/team-widget.css | 6 ++- 2 files changed, 33 insertions(+), 23 deletions(-) diff --git a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js index d2a98818..131d2417 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js @@ -1182,8 +1182,8 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { chartData.name = topNode[teamName] ?? ""; chartData.id = this.teamNodeID(topNode.id); const topNodeStrategy = topNode[`${strategyField}__relation`]; - chartData.className = `strategy-${topNodeStrategy?.[strategyCode]}`; - const topNodeStrategyID = topNodeStrategy?.id; + const topNodeCode = topNodeStrategy?.[strategyCode]; + chartData.className = `strategy-${topNodeCode}`; chartData.isInactive = topNode[teamInactive]; chartData._rawData = topNode; const topNodeContentFieldData = topNode[contentFieldColumnName]; @@ -1196,10 +1196,10 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { ? self.filterTeam( filters, chartData, - topNodeStrategyID, + topNodeCode, topNodeContentFieldData ) - : self.filterTeam(filters, chartData, topNodeStrategyID); + : self.filterTeam(filters, chartData, topNodeCode); const maxDepth = 10; // prevent inifinite loop /** * Recursive function to prepare child node data @@ -1218,10 +1218,11 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { ) return; const strategy = childData[`${strategyField}__relation`]; + const code = strategy?.[strategyCode]; const child = { name: childData[teamName], id: self.teamNodeID(id), - className: `strategy-${strategy?.[strategyCode]}`, + className: `strategy-${code}`, isInactive: childData[teamInactive], _rawData: childData, }; @@ -1231,15 +1232,10 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { contentDataRecordPKs.push(...childContentFieldData); else contentDataRecordPKs.push(childContentFieldData); } - const strategyID = strategy?.id; + child.filteredOut = isContentFiltered - ? self.filterTeam( - filters, - child, - strategyID, - childContentFieldData - ) - : self.filterTeam(filters, child, strategyID); + ? self.filterTeam(filters, child, code, childContentFieldData) + : self.filterTeam(filters, child, code); if (child.name === "External Support") child.className = `strategy-external`; if (childData[teamLink].length > 0) { @@ -1317,15 +1313,18 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { /** Display the filter UI (popup) **/ async filterWindow(buttonNode) { - const AB = this.AB; const contentDisplayedFieldFilters = this.settings.contentDisplayedFieldFilters; let $popup = $$(this.ids.filterPopup); if (!$popup) { - const [strategyField] = this.datacollection.datasource.fields( - (f) => f.id == this.view.settings["teamStrategy"] - ); - const strategyOptions = await strategyField.getOptions(); + const strategyID = + this.getSettingField("teamStrategy").settings.linkObject; + const strategyObj = this.AB.objectByID(strategyID); + const strategyCodeFieldID = this.getSettingField("strategyCode").id; + const strategyCodeField = strategyObj.fields( + (f) => f.id === strategyCodeFieldID + )[0]; + const strategyOptions = await strategyCodeField.getOptions(); $popup = webix.ui({ view: "popup", @@ -1333,6 +1332,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { id: this.ids.filterPopup, body: { view: "form", + borderless: true, id: this.ids.filterForm, elements: [ { @@ -1346,7 +1346,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { view: "combo", label: this.label("Strategy"), labelWidth: 90, - options: strategyOptions.map((f) => f.text), + options: strategyOptions.map(fieldToOption), name: "strategy", clear: "replace", }, @@ -1374,9 +1374,15 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { return contentDisplayedFieldFilterViews; })(), { - view: "button", - label: this.label("Apply"), - click: () => this.filterApply(), + cols: [ + {}, + { + view: "icon", + icon: "fa fa-check", + css: "filter-apply", + click: () => this.filterApply(), + }, + ], }, ], }, diff --git a/styles/team-widget.css b/styles/team-widget.css index a77d60d8..a3151e12 100644 --- a/styles/team-widget.css +++ b/styles/team-widget.css @@ -214,7 +214,7 @@ org-chart tr.lines .downLine { } .filter-popup { - border-radius: 10px; + border-radius: 15px; background: #fff; box-shadow: 0px 4px 4px 0px rgba(0, 0, 0, 0.25); } @@ -256,3 +256,7 @@ org-chart tr.lines .downLine { .team-form-button:disabled { background: #AAA; } + +.filter-apply > div > button > span { + color: #2f27ce !important; +} From a67cd54f7a520b4092debaaffedb6721ce7e8418 Mon Sep 17 00:00:00 2001 From: guyyoo Date: Mon, 9 Dec 2024 16:21:18 +0700 Subject: [PATCH 057/129] Still get undefined columns --- .../ABViewOrgChartTeamsComponent.js | 193 +++++++++--------- 1 file changed, 95 insertions(+), 98 deletions(-) diff --git a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js index 66af9b94..41a4b769 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js @@ -266,106 +266,102 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { const rules = {}; const labelWidth = 200; const contentFormElements = await Promise.all( - contentEditableFields - .filter( - (field) => contentDataRecord[field.columnName] !== undefined - ) - .map(async (field) => { - const fieldKey = field.key; - const fieldName = field.columnName; + contentEditableFields.map(async (field) => { + const fieldKey = field.key; + const fieldName = field.columnName; - // TODO (Guy): Add validators. - rules[fieldName] = () => true; - const fieldLabel = field.label; - const settings = field.settings; - switch (fieldKey) { - case "boolean": - return { - view: "checkbox", - name: fieldName, - label: fieldLabel, - labelWidth, - }; - case "number": - return { - view: "counter", - name: fieldName, - label: fieldLabel, - labelWidth, - type: "number", - }; - 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, - })), - }; - case "user": - case "connectObject": - const fieldLinkObj = field.datasourceLink; + // TODO (Guy): Add validators. + rules[fieldName] = () => true; + const fieldLabel = field.label; + const settings = field.settings; + switch (fieldKey) { + case "boolean": + return { + view: "checkbox", + name: fieldName, + label: fieldLabel, + labelWidth, + }; + case "number": + return { + view: "counter", + name: fieldName, + label: fieldLabel, + labelWidth, + type: "number", + }; + 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, + })), + }; + case "user": + case "connectObject": + const fieldLinkObj = field.datasourceLink; - // TODO (Guy): Fix pulling all connections. - const options = ( - await fieldLinkObj.model().findAll() - ).data.map((e) => ({ - id: e.id, - value: fieldLinkObj.displayData(e), - })); - return field.linkType() === "one" - ? { - view: "combo", - name: fieldName, - label: fieldLabel, - labelWidth, - options, - } - : { - view: "multicombo", - name: fieldName, - label: fieldLabel, - labelWidth, - stringResult: false, - labelAlign: "left", - options, - }; - case "date": - case "datetime": - return { - view: "datepicker", - name: fieldName, - label: fieldLabel, - labelWidth, - timepicker: fieldKey === "datetime", - }; - case "file": - case "image": - // TODO (Guy): Add logic - return { - // view: "", - name: fieldName, - label: fieldLabel, - labelWidth, - }; - // case "json": - // case "LongText": - // case "string": - // case "email": - default: - return { - view: "text", - name: fieldName, - label: fieldLabel, - labelWidth, - }; - } - }) + // TODO (Guy): Fix pulling all connections. + const options = ( + await fieldLinkObj.model().findAll() + ).data.map((e) => ({ + id: e.id, + value: fieldLinkObj.displayData(e), + })); + return field.linkType() === "one" + ? { + view: "combo", + name: fieldName, + label: fieldLabel, + labelWidth, + options, + } + : { + view: "multicombo", + name: fieldName, + label: fieldLabel, + labelWidth, + stringResult: false, + labelAlign: "left", + options, + }; + case "date": + case "datetime": + return { + view: "datepicker", + name: fieldName, + label: fieldLabel, + labelWidth, + timepicker: fieldKey === "datetime", + }; + case "file": + case "image": + // TODO (Guy): Add logic + return { + // view: "", + name: fieldName, + label: fieldLabel, + labelWidth, + }; + // case "json": + // case "LongText": + // case "string": + // case "email": + default: + return { + view: "text", + name: fieldName, + label: fieldLabel, + labelWidth, + }; + } + }) ); contentFormElements.push({ view: "button", @@ -391,6 +387,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { newFormData[key] = newValue?.toString(); break; default: + newFormData[key] = newValue; break; } if ( From 3fd614339dc46790a9f5d03902ca88605476ac73 Mon Sep 17 00:00:00 2001 From: nh758 <7259@pm.me> Date: Wed, 11 Dec 2024 14:39:31 +0700 Subject: [PATCH 058/129] add loading ui --- .../ABViewOrgChartTeamsComponent.js | 39 +++++++++++++++++++ styles/team-widget.css | 16 ++++++++ 2 files changed, 55 insertions(+) diff --git a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js index 1440c939..4a1a7b72 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js @@ -33,6 +33,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { const ids = this.ids; const _ui = super.ui([ { + id: ids.chartView, view: "template", template: `
`, css: { @@ -78,6 +79,44 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { _oldOnDragStart.call(this.__orgchart, event); }; this.ready(); + // SKELETON CHART + const orgchart = new OrgChart({ + data: { title: "", children: [{}, {}] }, + chartContainer: `#${this.ids.chartDom}`, + nodeContent: "title", + createNode: ($node) => { + // __AUTO_GENERATED_PRINT_VAR_START__ + console.log("loadOrgChartJs#(anon) node:", $node); // __AUTO_GENERATED_PRINT_VAR_END__ + $node.querySelector(".title").remove(); + $node.querySelector(".content").innerHTML = ""; + + $node.classList.add("team-node-skeleton"); + }, + }); + const chartDom = document.querySelector(`#${this.ids.chartDom}`); + chartDom.textContent = ""; + chartDom.innerHTML = ""; + const ui = { + cols: [ + { + rows: [ + { + view: "template", + height: 50, + }, + { + view: "template", + scroll: "auto", + }, + ], + }, + ], + }; + const $chartContent = AB.Webix.ui(ui).$view; + chartDom.appendChild($chartContent); + const $chartContentLayout = $chartContent.children[0]; + $chartContentLayout.children[1].children[0].appendChild(orgchart); + this.loadContentData() } async onShow() { diff --git a/styles/team-widget.css b/styles/team-widget.css index a3151e12..bd8ceecf 100644 --- a/styles/team-widget.css +++ b/styles/team-widget.css @@ -260,3 +260,19 @@ org-chart tr.lines .downLine { .filter-apply > div > button > span { color: #2f27ce !important; } + +@keyframes skeleton { + 0% { background-color: #ddd; } + 50% { background-color: #eee; } + 100% { background-color: #ddd} +} + +.team-node-skeleton .content { + /* background: grey; */ + border-radius: 15px !important; + height: 100px !important; + /* width: 325px; */ + animation-name: skeleton; + animation-duration: 2s; + animation-iteration-count: infinite; +} From ec09d1b6edb1257056a4e54bcc12981e7314aac4 Mon Sep 17 00:00:00 2001 From: nh758 <7259@pm.me> Date: Wed, 11 Dec 2024 15:47:17 +0700 Subject: [PATCH 059/129] prevent uncessary call to cursorChange when populating --- .../platform/views/viewComponent/ABViewDataSelectComponent.js | 1 + 1 file changed, 1 insertion(+) diff --git a/AppBuilder/platform/views/viewComponent/ABViewDataSelectComponent.js b/AppBuilder/platform/views/viewComponent/ABViewDataSelectComponent.js index eaac84a5..8a22e68a 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewDataSelectComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewDataSelectComponent.js @@ -20,6 +20,7 @@ export default class ABViewDataSelectComponent extends ABViewComponent { id: this.ids.select, on: { onChange: (n, o) => { + if (!o) return; if (n !== o) this.cursorChange(n); }, }, From 04fd1643efd03f7194d3bbe7efc4dd9dcdc9ae99 Mon Sep 17 00:00:00 2001 From: nh758 <7259@pm.me> Date: Thu, 12 Dec 2024 13:49:23 +0700 Subject: [PATCH 060/129] drop moves assingment without chart refresh --- .../ABViewOrgChartTeamsComponent.js | 275 ++++++++++-------- 1 file changed, 161 insertions(+), 114 deletions(-) diff --git a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js index 4a1a7b72..344c5c4d 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js @@ -78,15 +78,12 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { event.dataTransfer.setData("isnode", 1); _oldOnDragStart.call(this.__orgchart, event); }; - this.ready(); // SKELETON CHART const orgchart = new OrgChart({ data: { title: "", children: [{}, {}] }, chartContainer: `#${this.ids.chartDom}`, nodeContent: "title", createNode: ($node) => { - // __AUTO_GENERATED_PRINT_VAR_START__ - console.log("loadOrgChartJs#(anon) node:", $node); // __AUTO_GENERATED_PRINT_VAR_END__ $node.querySelector(".title").remove(); $node.querySelector(".content").innerHTML = ""; @@ -157,7 +154,6 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { const settings = baseView.settings; const showGroupTitle = settings.showGroupTitle === 1; const draggable = settings.draggable === 1; - const dropContentToCreate = settings.dropContentToCreate === 1; const nodeDC = baseView.datacollection; const nodeModel = baseView.datacollection.model; const nodeObj = nodeDC?.datasource; @@ -199,106 +195,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { requestAnimationFrame(callback); }); }; - const fnContentDragStart = (event) => { - event.stopPropagation(); - const $eventTarget = event.target; - const dataset = $eventTarget.dataset; - const dataTransfer = event.dataTransfer; - switch ($eventTarget.className) { - case "webix_list_item": - dataTransfer.setData("dataPK", dataset.pk); - dataTransfer.setData( - "contentLinkedFieldId", - dataset.contentLinkedFieldId - ); - break; - default: - dataTransfer.setData("source", dataset.source); - break; - } - // $eventTarget.style.opacity = "0.5"; - }; - const fnContentDragOver = (event) => { - event.preventDefault(); - event.stopPropagation(); - }; - const fnContentDragEnd = (event) => { - // event.target.style.opacity = "1"; - }; - const fnContentDrop = async (event) => { - const dataTransfer = event.dataTransfer; - if (dataTransfer.getData("isnode") == 1) return; - event.stopPropagation(); - if (contentFieldLinkColumnName == null) return; - const $group = event.currentTarget; - const newGroupDataPK = $group.dataset.pk; - const newNodeDataPK = JSON.parse( - $group.parentElement.parentElement.dataset.source - )._rawData[nodeObjPK]; - let updatedData = dataTransfer.getData("source"); - const orgchart = this.__orgchart; - if (updatedData === "") { - const dataPK = dataTransfer.getData("dataPK"); - const contentLinkedFieldID = dataTransfer.getData( - "contentLinkedFieldId" - ); - const contentLinkedFieldColumnName = - contentObj.fieldByID(contentLinkedFieldID).columnName; - const pendingPromises = []; - const newDate = new Date(); - orgchart.querySelectorAll(".team-group-record").forEach((e) => { - const contentData = JSON.parse(e.dataset.source); - contentData[contentDateEndFieldColumnName] = newDate; - contentData[contentLinkedFieldColumnName] === dataPK && - pendingPromises.push( - contentModel.update(contentData.id, contentData) - ); - }); - updatedData = {}; - updatedData[contentDateStartFieldColumnName] = newDate; - updatedData[contentLinkedFieldColumnName] = dataPK; - updatedData[contentFieldLinkColumnName] = newNodeDataPK; - updatedData[contentGroupByFieldColumnName] = newGroupDataPK; - if (this.entityDC) { - const entityLink = this.entityDC?.datasource.connectFields( - (f) => f.settings.linkObject === contentObj.id - )[0].id; - const entityCol = this.AB.definitionByID(entityLink).columnName; - updatedData[entityCol] = this.entityDC.getCursor(); - } - pendingPromises.push(contentModel.create(updatedData)); - await Promise.all(pendingPromises); - } else { - orgchart.innerHTML = ""; - updatedData = JSON.parse(updatedData); - delete updatedData["created_at"]; - delete updatedData["updated_at"]; - delete updatedData["properties"]; - if (dropContentToCreate) { - const pendingPromises = []; - updatedData[contentDateEndFieldColumnName] = new Date(); - pendingPromises.push( - contentModel.update(updatedData.id, updatedData) - ); - updatedData[contentDateStartFieldColumnName] = - updatedData[contentDateEndFieldColumnName]; - delete updatedData["id"]; - delete updatedData["uuid"]; - delete updatedData[contentDateEndFieldColumnName]; - updatedData[contentFieldLinkColumnName] = newNodeDataPK; - updatedData[contentGroupByFieldColumnName] = newGroupDataPK; - pendingPromises.push(contentModel.create(updatedData)); - await Promise.all(pendingPromises); - } else { - updatedData[contentFieldLinkColumnName] = newNodeDataPK; - updatedData[contentGroupByFieldColumnName] = newGroupDataPK; - await contentModel.update(updatedData.id, updatedData); - } - } - // TODO (Guy): This is refreshing the whole chart. - await this.refresh(); - }; const editContentFieldsToCreateNew = settings.editContentFieldsToCreateNew; const setEditableContentFields = settings.setEditableContentFields; @@ -602,8 +499,8 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { const $groupContent = element("div", "team-group-content"); $group.appendChild($groupContent); if (draggable) { - $group.addEventListener("dragover", fnContentDragOver); - $group.addEventListener("drop", fnContentDrop); + $group.addEventListener("dragover", this.fnContentDragOver); + $group.addEventListener("drop", (e) => this.fnContentDrop(e)); } let contentDataRecordIndex = 0; while ( @@ -626,6 +523,10 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { key.includes("__relation") && delete contentDataRecord[key]; const $rowData = element("div", "team-group-record"); + $rowData.setAttribute( + "id", + this.contentNodeID(contentDataRecord.id) + ); $rowData.setAttribute( "data-source", JSON.stringify(contentDataRecord) @@ -638,8 +539,14 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { }); if (draggable) { $rowData.setAttribute("draggable", "true"); - $rowData.addEventListener("dragstart", fnContentDragStart); - $rowData.addEventListener("dragend", fnContentDragEnd); + $rowData.addEventListener( + "dragstart", + this.fnContentDragStart + ); + $rowData.addEventListener( + "dragend", + this.fnContentDragEnd + ); } let currentDataRecords = []; let currentField = null; @@ -1020,11 +927,10 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { $itemElement.setAttribute("draggable", "true"); $itemElement.addEventListener( "dragstart", - fnContentDragStart + this.fnContentDragStart ); - $itemElement.addEventListener( - "dragend", - fnContentDragEnd + $itemElement.addEventListener("dragend", (e) => + this.fnContentDragEnd(e) ); } }); @@ -1062,9 +968,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { await dc?.waitForDataCollectionToInitialize(dc); let topNode = dc?.getCursor(); if (settings.topTeam) { - const topNodeColumn = this.AB.definitionByID( - settings.topTeam - ).columnName; + const topNodeColumn = this.getSettingField("topTeam").columnName; const topFromFeild = dc.getData((e) => e[topNodeColumn] === 1)[0]; topNode = topFromFeild ? topFromFeild : topNode; } @@ -1802,6 +1706,149 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { $teamFormPopup.show(); } + // DRAG EVENTS + fnContentDragStart(event) { + event.stopPropagation(); + const $eventTarget = event.target; + const dataset = $eventTarget.dataset; + const dataTransfer = event.dataTransfer; + switch ($eventTarget.className) { + case "webix_list_item": + dataTransfer.setData("dataPK", dataset.pk); + dataTransfer.setData( + "contentLinkedFieldId", + dataset.contentLinkedFieldId + ); + break; + default: + dataTransfer.setData("source", dataset.source); + dataTransfer.setData("text/plain", $eventTarget.id); + break; + } + // $eventTarget.style.opacity = "0.5"; + } + fnContentDragOver(event) { + event.preventDefault(); + event.stopPropagation(); + } + + fnContentDragEnd(event) { + // event.target.style.opacity = "1"; + } + + async fnContentDrop(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; + const $group = event.currentTarget; + const newGroupDataPK = $group.dataset.pk; + const newNodeDataPK = JSON.parse( + $group.parentElement.parentElement.dataset.source + )._rawData[nodeObjPK]; + let updatedData = dataTransfer.getData("source"); + const orgchart = this.__orgchart; + if (updatedData === "") { + const dataPK = dataTransfer.getData("dataPK"); + const contentLinkedFieldID = dataTransfer.getData( + "contentLinkedFieldId" + ); + const contentLinkedFieldColumnName = + contentObj.fieldByID(contentLinkedFieldID).columnName; + const pendingPromises = []; + const newDate = new Date(); + orgchart.querySelectorAll(".team-group-record").forEach((e) => { + const contentData = JSON.parse(e.dataset.source); + contentData[contentDateEndFieldColumnName] = newDate; + contentData[contentLinkedFieldColumnName] === dataPK && + pendingPromises.push( + contentModel.update(contentData.id, contentData) + ); + }); + updatedData = {}; + updatedData[contentDateStartFieldColumnName] = newDate; + updatedData[contentLinkedFieldColumnName] = dataPK; + updatedData[contentFieldLinkColumnName] = newNodeDataPK; + updatedData[contentGroupByFieldColumnName] = newGroupDataPK; + if (this.entityDC) { + const entityLink = this.entityDC?.datasource.connectFields( + (f) => f.settings.linkObject === contentObj.id + )[0].id; + const entityCol = this.AB.definitionByID(entityLink).columnName; + updatedData[entityCol] = this.entityDC.getCursor(); + } + pendingPromises.push(contentModel.create(updatedData)); + await Promise.all(pendingPromises); + } else { + updatedData = JSON.parse(updatedData); + // Move the child node to the target + const dragged = document.querySelector( + `#${this.contentNodeID(updatedData.id)}` + ); + dragged.parentNode.removeChild(dragged); + $group.querySelector(".team-group-content").appendChild(dragged); + // Save the data + delete updatedData["created_at"]; + delete updatedData["updated_at"]; + delete updatedData["properties"]; + if (dropContentToCreate) { + const pendingPromises = []; + updatedData[contentDateEndFieldColumnName] = new Date(); + pendingPromises.push( + contentModel.update(updatedData.id, updatedData) + ); + updatedData[contentDateStartFieldColumnName] = + updatedData[contentDateEndFieldColumnName]; + delete updatedData["id"]; + delete updatedData["uuid"]; + delete updatedData[contentDateEndFieldColumnName]; + updatedData[contentFieldLinkColumnName] = newNodeDataPK; + updatedData[contentGroupByFieldColumnName] = newGroupDataPK; + pendingPromises.push(contentModel.create(updatedData)); + await Promise.all(pendingPromises); + } else { + updatedData[contentFieldLinkColumnName] = newNodeDataPK; + updatedData[contentGroupByFieldColumnName] = newGroupDataPK; + await contentModel.update(updatedData.id, updatedData); + } + + return; + } + // TODO (Guy): This is refreshing the whole chart. + await this.refresh(); + } + + // HELPERS + + /** + * generate a id for the assignment dom node based on it's record id + * @param {string} id record id + */ + contentNodeID(id) { + return `contentnode_${id}`; + } + /** * generate a id for the team dom node based on it's record id * @param {string} id record id From b52c56e4f05351155ddd1af37f16b146176e1f4e Mon Sep 17 00:00:00 2001 From: nh758 <7259@pm.me> Date: Thu, 12 Dec 2024 14:50:59 +0700 Subject: [PATCH 061/129] move content ui to it's own function --- .../ABViewOrgChartTeamsComponent.js | 1034 ++++++++--------- 1 file changed, 498 insertions(+), 536 deletions(-) diff --git a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js index 344c5c4d..b95fe5bd 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js @@ -144,12 +144,19 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { ); } } + this.loadContentData(); + await Promise.all([this.loadOrgChartJs(), this.pullData()]); + this.AB.performance.measure("TeamChart.load"); + this.AB.performance.mark("TeamChart.display"); + await this.displayOrgChart(); + this.AB.performance.measure("TeamChart.display"); + this.ready(); + this.AB.performance.measure("TeamChart.onShow"); } async displayOrgChart() { const baseView = this.view; const AB = this.AB; - const L = AB.Label(); const chartData = AB.cloneDeep(this.chartData); const settings = baseView.settings; const showGroupTitle = settings.showGroupTitle === 1; @@ -161,13 +168,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { 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 contentObj = this.contentObject(); const contentGroupByField = contentObj?.fieldByID( settings.contentGroupByField ); @@ -185,9 +186,6 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { const contentGroupByFieldColumnName = contentGroupByField?.columnName; const contentFieldLinkColumnName = contentFieldLink?.columnName; const contentObjID = contentObj?.id; - const contentDisplayedFields = settings.contentDisplayedFields; - const contentDisplayedFieldsKeys = Object.keys(contentDisplayedFields); - const contentModel = contentObj?.model(); const strategyColors = settings.strategyColors; const ids = this.ids; const callAfterRender = (callback) => { @@ -196,247 +194,9 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { }); }; - const editContentFieldsToCreateNew = - settings.editContentFieldsToCreateNew; - const setEditableContentFields = settings.setEditableContentFields; - const contentEditableFields = contentObj.fields( - (field) => setEditableContentFields.indexOf(field.id) > -1 - ); - const showContentForm = async (contentDataRecord) => { - const rules = {}; - const labelWidth = 200; - const contentFormElements = await Promise.all( - contentEditableFields.map(async (field) => { - const fieldKey = field.key; - const fieldName = field.columnName; - - // TODO (Guy): Add validators. - rules[fieldName] = () => true; - const fieldLabel = field.label; - const settings = field.settings; - switch (fieldKey) { - case "boolean": - return { - view: "checkbox", - name: fieldName, - label: fieldLabel, - labelWidth, - }; - case "number": - return { - view: "counter", - name: fieldName, - label: fieldLabel, - labelWidth, - type: "number", - }; - 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, - })), - }; - case "user": - case "connectObject": - const fieldLinkObj = field.datasourceLink; - - // TODO (Guy): Fix pulling all connections. - const options = ( - await fieldLinkObj.model().findAll() - ).data.map((e) => ({ - id: e.id, - value: fieldLinkObj.displayData(e), - })); - return field.linkType() === "one" - ? { - view: "combo", - name: fieldName, - label: fieldLabel, - labelWidth, - options, - } - : { - view: "multicombo", - name: fieldName, - label: fieldLabel, - labelWidth, - stringResult: false, - labelAlign: "left", - options, - }; - case "date": - case "datetime": - return { - view: "datepicker", - name: fieldName, - label: fieldLabel, - labelWidth, - timepicker: fieldKey === "datetime", - }; - case "file": - case "image": - // TODO (Guy): Add logic - return { - // view: "", - name: fieldName, - label: fieldLabel, - labelWidth, - }; - // case "json": - // case "LongText": - // case "string": - // case "email": - default: - return { - view: "text", - name: fieldName, - label: fieldLabel, - labelWidth, - }; - } - }) - ); - contentFormElements.push({ - view: "button", - value: L("Submit"), - css: "webix_primary", - click: async () => { - const $contentFormData = $$(ids.contentFormData); - if (!$contentFormData.validate()) return; - const newFormData = $contentFormData.getValues(); - let isDataChanged = false; - for (const key in newFormData) { - const oldValue = contentDataRecord[key]; - const newValue = newFormData[key]; - switch (typeof oldValue) { - case "boolean": - if (newValue == 0) newFormData[key] = false; - else newFormData[key] = true; - break; - case "number": - newFormData[key] = parseInt(newValue); - break; - case "string": - newFormData[key] = newValue?.toString(); - break; - default: - newFormData[key] = newValue; - break; - } - if ( - JSON.stringify(newFormData[key]) !== - JSON.stringify(contentDataRecord[key]) - ) - isDataChanged = true; - } - const $contentForm = $$(ids.contentForm); - if (!isDataChanged) { - $contentForm.hide(); - return; - } - delete newFormData["created_at"]; - delete newFormData["updated_at"]; - delete newFormData["properties"]; - for (const editContentFieldToCreateNew of editContentFieldsToCreateNew) { - const editContentFieldToCreateNewColumnName = - contentObj.fieldByID( - editContentFieldToCreateNew - )?.columnName; - if ( - JSON.stringify( - newFormData[editContentFieldToCreateNewColumnName] ?? "" - ) !== - JSON.stringify( - contentDataRecord[ - editContentFieldToCreateNewColumnName - ] ?? "" - ) - ) { - this.__orgchart.innerHTML = ""; - const pendingPromises = []; - const oldData = {}; - oldData[contentDateEndFieldColumnName] = new Date(); - pendingPromises.push( - contentModel.update(newFormData.id, oldData) - ); - newFormData[contentDateStartFieldColumnName] = - oldData[contentDateEndFieldColumnName]; - delete newFormData["id"]; - delete newFormData["uuid"]; - delete newFormData[contentDateEndFieldColumnName]; - pendingPromises.push(contentModel.create(newFormData)); - await Promise.all(pendingPromises); - $contentForm.hide(); - await this.refresh(); - return; - } - } - this.__orgchart.innerHTML = ""; - await contentModel.update(newFormData.id, newFormData); - $contentForm.hide(); - await this.refresh(); - }, - }); - AB.Webix.ui({ - view: "popup", - id: ids.contentForm, - close: true, - position: "center", - css: { "border-radius": "10px" }, - body: { - width: 600, - rows: [ - { - view: "toolbar", - id: "myToolbar", - css: "webix_dark", - cols: [ - { - view: "label", - label: `${L("Edit")} ${contentObj.label}`, - align: "center", - }, - { - view: "button", - value: "X", - width: 60, - align: "right", - css: "webix_transparent", - click: () => { - $$(ids.contentForm).hide(); - }, - }, - ], - }, - { - view: "form", - id: ids.contentFormData, - elements: contentFormElements, - rules, - }, - ], - }, - on: { - onHide() { - this.destructor(); - }, - }, - }).show(); - $$(ids.contentFormData).setValues(contentDataRecord); - }; const contentDataRecords = this._cachedContentDataRecords; this.AB.performance.measure("misc"); this.AB.performance.mark("createOrgChart"); - const contentDisplayedFieldTypes = settings.contentDisplayedFieldTypes; - const contentDisplayedFieldMappingData = - settings.contentDisplayedFieldMappingData; const orgchart = new this.OrgChart({ data: chartData, direction: baseView.settings.direction, @@ -522,295 +282,11 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { for (const key in contentDataRecord) key.includes("__relation") && delete contentDataRecord[key]; - const $rowData = element("div", "team-group-record"); - $rowData.setAttribute( - "id", - this.contentNodeID(contentDataRecord.id) - ); - $rowData.setAttribute( - "data-source", - JSON.stringify(contentDataRecord) + const $rowData = await this.contentRecordUI( + contentDataRecord, + strategyColor ); $groupContent.appendChild($rowData); - const rowDataStyle = $rowData.style; - rowDataStyle["borderColor"] = strategyColor; - $rowData.addEventListener("click", async () => { - await showContentForm(contentDataRecord); - }); - if (draggable) { - $rowData.setAttribute("draggable", "true"); - $rowData.addEventListener( - "dragstart", - this.fnContentDragStart - ); - $rowData.addEventListener( - "dragend", - this.fnContentDragEnd - ); - } - let currentDataRecords = []; - let currentField = null; - - // 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"), - ]; - const $hardcodedSpecialDisplay = element( - "div", - "team-group-record-display" - ); - let currentDisplayIndex = 0; - 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); - switch (objID) { - case contentObjID: - currentDataRecords = [contentDataRecord]; - 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); - currentDataRecords = ( - await displayedObj.model().findAll({ - where: { - glue: "and", - rules: [ - { - key: displayedObj.PK(), - rule: "in", - value: currentDataPKs, - }, - ], - }, - populate: true, - }) - ).data; - } - break; - } - if ( - contentDisplayedFieldsKeys[j + 1]?.split(".")[0] === - atDisplay - ) { - currentField = displayedField; - continue; - } - const $currentDisplay = element( - "div", - "team-group-record-display" - ); - - // TODO (Guy): Now we are hardcoding for each display. - // $rowData.appendChild($currentDisplay); - switch (currentDisplayIndex) { - case 0: - hardcodedDisplays[0].appendChild($currentDisplay); - break; - case 1: - 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 displayedFieldColumnName = displayedField.columnName; - const contentDisplayedFieldTypePrefix = `${displayedFieldKey}.${displayedFieldID}`; - const contentDisplayedFieldMappingDataObj = - JSON.parse( - contentDisplayedFieldMappingData?.[ - contentDisplayedFieldTypePrefix - ] || null - ) || {}; - if ( - contentDisplayedFieldTypes[ - `${contentDisplayedFieldTypePrefix}.0` - ] != null - ) - $currentDisplay.style.display = "none"; - switch ( - 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 - ) - ); - } - 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]; - $rowData.appendChild($hardcodedDisplay); - const children = $hardcodedDisplay.children; - let isShown = false; - let j = 0; - switch (i) { - case 1: - const child = children.item(j); - const grandChildren = child.children; - const 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 $buttons = element("div", "team-button-section"); @@ -1228,6 +704,12 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { } } + contentObject() { + return this.AB.objectByID( + this.getSettingField("contentField").settings.linkObject + ); + } + async refresh() { this.busy(); await this.pullData(); @@ -1706,6 +1188,486 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { $teamFormPopup.show(); } + async showContentForm(contentDataRecord) { + const contentObj = this.contentObject(); + const contentModel = contentObj?.model(); + const setEditableContentFields = this.settings.setEditableContentFields; + const contentEditableFields = contentObj.fields( + (field) => setEditableContentFields.indexOf(field.id) > -1 + ); + const editContentFieldsToCreateNew = + this.settings.editContentFieldsToCreateNew; + const contentDateStartFieldColumnName = this.getSettingField( + "contentFieldDateStart" + )?.columnName; + const contentDateEndFieldColumnName = this.getSettingField( + "contentFieldDateEnd" + )?.columnName; + + const rules = {}; + const labelWidth = 200; + const contentFormElements = await Promise.all( + contentEditableFields.map(async (field) => { + const fieldKey = field.key; + const fieldName = field.columnName; + + // TODO (Guy): Add validators. + rules[fieldName] = () => true; + const fieldLabel = field.label; + const settings = field.settings; + let fieldLinkObj, options; + switch (fieldKey) { + case "boolean": + return { + view: "checkbox", + name: fieldName, + label: fieldLabel, + labelWidth, + }; + case "number": + return { + view: "counter", + name: fieldName, + label: fieldLabel, + labelWidth, + type: "number", + }; + 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, + })), + }; + case "user": + case "connectObject": + fieldLinkObj = field.datasourceLink; + + // TODO (Guy): Fix pulling all connections. + options = (await fieldLinkObj.model().findAll()).data.map( + (e) => ({ + id: e.id, + value: fieldLinkObj.displayData(e), + }) + ); + return field.linkType() === "one" + ? { + view: "combo", + name: fieldName, + label: fieldLabel, + labelWidth, + options, + } + : { + view: "multicombo", + name: fieldName, + label: fieldLabel, + labelWidth, + stringResult: false, + labelAlign: "left", + options, + }; + case "date": + case "datetime": + return { + view: "datepicker", + name: fieldName, + label: fieldLabel, + labelWidth, + timepicker: fieldKey === "datetime", + }; + case "file": + case "image": + // TODO (Guy): Add logic + return { + // view: "", + name: fieldName, + label: fieldLabel, + labelWidth, + }; + // case "json": + // case "LongText": + // case "string": + // case "email": + default: + return { + view: "text", + name: fieldName, + label: fieldLabel, + labelWidth, + }; + } + }) + ); + contentFormElements.push({ + view: "button", + value: this.label("Submit"), + css: "webix_primary", + click: async () => { + const $contentFormData = $$(this.ids.contentFormData); + if (!$contentFormData.validate()) return; + const newFormData = $contentFormData.getValues(); + let isDataChanged = false; + for (const key in newFormData) { + const oldValue = contentDataRecord[key]; + const newValue = newFormData[key]; + switch (typeof oldValue) { + case "boolean": + if (newValue == 0) newFormData[key] = false; + else newFormData[key] = true; + break; + case "number": + newFormData[key] = parseInt(newValue); + break; + case "string": + newFormData[key] = newValue?.toString(); + break; + default: + newFormData[key] = newValue; + break; + } + if ( + JSON.stringify(newFormData[key]) !== + JSON.stringify(contentDataRecord[key]) + ) + isDataChanged = true; + } + const $contentForm = $$(this.ids.contentForm); + if (!isDataChanged) { + $contentForm.hide(); + return; + } + delete newFormData["created_at"]; + delete newFormData["updated_at"]; + delete newFormData["properties"]; + for (const editContentFieldToCreateNew of editContentFieldsToCreateNew) { + const editContentFieldToCreateNewColumnName = + contentObj.fieldByID(editContentFieldToCreateNew)?.columnName; + if ( + JSON.stringify( + newFormData[editContentFieldToCreateNewColumnName] ?? "" + ) !== + JSON.stringify( + contentDataRecord[editContentFieldToCreateNewColumnName] ?? + "" + ) + ) { + this.__orgchart.innerHTML = ""; + const pendingPromises = []; + const oldData = {}; + oldData[contentDateEndFieldColumnName] = new Date(); + pendingPromises.push( + contentModel.update(newFormData.id, oldData) + ); + newFormData[contentDateStartFieldColumnName] = + oldData[contentDateEndFieldColumnName]; + delete newFormData["id"]; + delete newFormData["uuid"]; + delete newFormData[contentDateEndFieldColumnName]; + pendingPromises.push(contentModel.create(newFormData)); + await Promise.all(pendingPromises); + $contentForm.hide(); + await this.refresh(); + return; + } + } + this.__orgchart.innerHTML = ""; + await contentModel.update(newFormData.id, newFormData); + $contentForm.hide(); + await this.refresh(); + }, + }); + AB.Webix.ui({ + view: "popup", + id: this.ids.contentForm, + close: true, + position: "center", + css: { "border-radius": "10px" }, + body: { + width: 600, + rows: [ + { + view: "toolbar", + id: "myToolbar", + css: "webix_dark", + cols: [ + { + view: "label", + label: `${this.label("Edit")} ${contentObj.label}`, + align: "center", + }, + { + view: "button", + value: "X", + width: 60, + align: "right", + css: "webix_transparent", + click: () => { + $$(this.ids.contentForm).hide(); + }, + }, + ], + }, + { + view: "form", + id: this.ids.contentFormData, + elements: contentFormElements, + rules, + }, + ], + }, + on: { + onHide() { + this.destructor(); + }, + }, + }).show(); + $$(this.ids.contentFormData).setValues(contentDataRecord); + } + + async contentRecordUI(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("click", async () => { + await this.showContentForm(data); + }); + 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"), + ]; + const $hardcodedSpecialDisplay = element( + "div", + "team-group-record-display" + ); + let currentDataRecords = []; + let currentField = null; + let currentDisplayIndex = 0; + const contentObj = this.contentObject(); + const contentDisplayedFields = this.settings.contentDisplayedFields; + const contentDisplayedFieldsKeys = Object.keys(contentDisplayedFields); + + 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); + switch (objID) { + case contentObj.id: + 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); + currentDataRecords = ( + await displayedObj.model().findAll({ + where: { + glue: "and", + rules: [ + { + key: displayedObj.PK(), + rule: "in", + value: currentDataPKs, + }, + ], + }, + populate: true, + }) + ).data; + } + break; + } + if (contentDisplayedFieldsKeys[j + 1]?.split(".")[0] === atDisplay) { + currentField = displayedField; + continue; + } + const $currentDisplay = element("div", "team-group-record-display"); + + // TODO (Guy): Now we are hardcoding for each display. + // $rowData.appendChild($currentDisplay); + switch (currentDisplayIndex) { + case 0: + hardcodedDisplays[0].appendChild($currentDisplay); + break; + case 1: + 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 displayedFieldColumnName = displayedField.columnName; + 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 + ) + ); + } + 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"); + } + return $ui; + } + // DRAG EVENTS fnContentDragStart(event) { event.stopPropagation(); From 81865fe807f69d31300067ceec2c0d60946be91b Mon Sep 17 00:00:00 2001 From: nh758 <7259@pm.me> Date: Fri, 13 Dec 2024 16:24:54 +0700 Subject: [PATCH 062/129] drag from sidebar doesn not refresh the whole chart --- .../ABViewOrgChartTeamsComponent.js | 41 +++++++++---------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js index b95fe5bd..1e4c9ed5 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js @@ -370,6 +370,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { const contentFieldID = panelObj.connectFields( (field) => field.datasourceLink.id == contentObjID )[0].fieldLink.id; + const self = this; dataPanelUIs.push({ header: dataPanelDCs[key], body: { @@ -403,10 +404,10 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { $itemElement.setAttribute("draggable", "true"); $itemElement.addEventListener( "dragstart", - this.fnContentDragStart + (e) => self.fnContentDragStart(e) ); $itemElement.addEventListener("dragend", (e) => - this.fnContentDragEnd(e) + self.fnContentDragEnd(e) ); } }); @@ -1674,19 +1675,17 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { const $eventTarget = event.target; const dataset = $eventTarget.dataset; const dataTransfer = event.dataTransfer; + const data = {}; switch ($eventTarget.className) { case "webix_list_item": - dataTransfer.setData("dataPK", dataset.pk); - dataTransfer.setData( - "contentLinkedFieldId", - dataset.contentLinkedFieldId - ); + data.pk = dataset.pk; + data.contentLinkedFieldID = dataset.contentLinkedFieldId; break; default: - dataTransfer.setData("source", dataset.source); - dataTransfer.setData("text/plain", $eventTarget.id); + data.source = dataset.source; break; } + dataTransfer.setData("text/plain", JSON.stringify(data)); // $eventTarget.style.opacity = "0.5"; } fnContentDragOver(event) { @@ -1721,7 +1720,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { const contentModel = contentObj?.model(); const dataTransfer = event.dataTransfer; - if (dataTransfer.getData("isnode") == 1) return; + // if (dataTransfer.getData("isnode") == 1) return; event.stopPropagation(); if (contentFieldLinkColumnName == null) return; const $group = event.currentTarget; @@ -1729,13 +1728,14 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { const newNodeDataPK = JSON.parse( $group.parentElement.parentElement.dataset.source )._rawData[nodeObjPK]; - let updatedData = dataTransfer.getData("source"); + let { + source: updatedData, + pk: dataPK, + contentLinkedFieldID, + } = JSON.parse(dataTransfer.getData("text/plain")); const orgchart = this.__orgchart; - if (updatedData === "") { - const dataPK = dataTransfer.getData("dataPK"); - const contentLinkedFieldID = dataTransfer.getData( - "contentLinkedFieldId" - ); + if (!updatedData) { + // This is a drop from Employee list (new assignment) const contentLinkedFieldColumnName = contentObj.fieldByID(contentLinkedFieldID).columnName; const pendingPromises = []; @@ -1753,6 +1753,9 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { updatedData[contentLinkedFieldColumnName] = dataPK; updatedData[contentFieldLinkColumnName] = newNodeDataPK; updatedData[contentGroupByFieldColumnName] = newGroupDataPK; + this.contentRecordUI(updatedData, "grey").then(($record) => + $group.querySelector(".team-group-content").appendChild($record) + ); if (this.entityDC) { const entityLink = this.entityDC?.datasource.connectFields( (f) => f.settings.linkObject === contentObj.id @@ -1763,7 +1766,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { pendingPromises.push(contentModel.create(updatedData)); await Promise.all(pendingPromises); } else { - updatedData = JSON.parse(updatedData); + // This is move form another team node // Move the child node to the target const dragged = document.querySelector( `#${this.contentNodeID(updatedData.id)}` @@ -1794,11 +1797,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { updatedData[contentGroupByFieldColumnName] = newGroupDataPK; await contentModel.update(updatedData.id, updatedData); } - - return; } - // TODO (Guy): This is refreshing the whole chart. - await this.refresh(); } // HELPERS From b18f2fd5ce44ac1f51f5aca0b68bb2e6f2905e48 Mon Sep 17 00:00:00 2001 From: guyyoo Date: Fri, 13 Dec 2024 19:25:31 +0700 Subject: [PATCH 063/129] Hardcode date end filter includes null value --- .../ABViewOrgChartTeamsComponent.js | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js index 1440c939..7bdf9e24 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js @@ -1274,7 +1274,23 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { rule: "in", value: contentDataRecordPKs, }, - contentFieldFilter, + { + glue: "or", + rules: + (contentFieldFilter.rules?.length > 0 && [ + contentFieldFilter, + + // TODO (Guy): Hardcode date end filter. + { + key: contentObj?.fieldByID( + settings.contentFieldDateEnd + )?.id, + rule: "is_null", + value: "", + }, + ]) || + [], + }, ], }, populate: true, From 12cf5e68a8ee2674569f31ea44a9f2355dadfbf6 Mon Sep 17 00:00:00 2001 From: guyyoo Date: Mon, 16 Dec 2024 02:44:51 +0700 Subject: [PATCH 064/129] Sort fields on the editing assignment page --- .../viewComponent/ABViewOrgChartTeamsComponent.js | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js index 7bdf9e24..a4676f26 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js @@ -262,15 +262,20 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { }; const editContentFieldsToCreateNew = settings.editContentFieldsToCreateNew; - const setEditableContentFields = settings.setEditableContentFields; - const contentEditableFields = contentObj.fields( - (field) => setEditableContentFields.indexOf(field.id) > -1 - ); const showContentForm = async (contentDataRecord) => { const rules = {}; const labelWidth = 200; const contentFormElements = await Promise.all( - contentEditableFields.map(async (field) => { + settings.setEditableContentFields.map(async (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; From 99c53d2c98864369d5a38485c8a0526c3a81e189 Mon Sep 17 00:00:00 2001 From: guyyoo Date: Tue, 17 Dec 2024 09:47:26 +0700 Subject: [PATCH 065/129] New hiding popup logic --- .../ABViewOrgChartTeamsComponent.js | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js index a4676f26..ad734d3b 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js @@ -238,6 +238,8 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { delete updatedData["properties"]; if (dropContentToCreate) { const pendingPromises = []; + + // TODO (Guy): Force update Date End with a current date. updatedData[contentDateEndFieldColumnName] = new Date(); pendingPromises.push( contentModel.update(updatedData.id, updatedData) @@ -257,6 +259,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { await contentModel.update(updatedData.id, updatedData); } } + // TODO (Guy): This is refreshing the whole chart. await this.refresh(); }; @@ -265,6 +268,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { const showContentForm = async (contentDataRecord) => { const rules = {}; const labelWidth = 200; + const orgchart = this.__orgchart; const contentFormElements = await Promise.all( settings.setEditableContentFields.map(async (fieldID) => { const field = contentObj.fields( @@ -406,10 +410,9 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { isDataChanged = true; } const $contentForm = $$(ids.contentForm); - if (!isDataChanged) { - $contentForm.hide(); - return; - } + $contentForm.hide(); + if (!isDataChanged) return; + orgchart.innerHTML = ""; delete newFormData["created_at"]; delete newFormData["updated_at"]; delete newFormData["properties"]; @@ -428,7 +431,6 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { ] ?? "" ) ) { - this.__orgchart.innerHTML = ""; const pendingPromises = []; const oldData = {}; oldData[contentDateEndFieldColumnName] = new Date(); @@ -442,14 +444,11 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { delete newFormData[contentDateEndFieldColumnName]; pendingPromises.push(contentModel.create(newFormData)); await Promise.all(pendingPromises); - $contentForm.hide(); await this.refresh(); return; } } - this.__orgchart.innerHTML = ""; await contentModel.update(newFormData.id, newFormData); - $contentForm.hide(); await this.refresh(); }, }); @@ -479,7 +478,9 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { align: "right", css: "webix_transparent", click: () => { - $$(ids.contentForm).hide(); + const $contentForm = $$(ids.contentForm); + $contentForm.blockEvent(); + $contentForm.destructor(); }, }, ], From f11e2ab48053f7ec57b29118d3f033667081452a Mon Sep 17 00:00:00 2001 From: guyyoo Date: Tue, 17 Dec 2024 09:54:49 +0700 Subject: [PATCH 066/129] Change Submit to Save --- .../views/viewComponent/ABViewOrgChartTeamsComponent.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js index ad734d3b..4733198a 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js @@ -378,7 +378,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { ); contentFormElements.push({ view: "button", - value: L("Submit"), + value: L("Save"), css: "webix_primary", click: async () => { const $contentFormData = $$(ids.contentFormData); @@ -1715,7 +1715,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { view: "template", id: this.ids.teamFormSubmit, template: ``, data: { disabled: "disabled" }, borderless: true, From c73b16a7b8a998e96681db74c5be0e2df890dc0e Mon Sep 17 00:00:00 2001 From: guyyoo Date: Tue, 17 Dec 2024 10:54:51 +0700 Subject: [PATCH 067/129] Fix missing popup logic --- .../views/viewComponent/ABViewOrgChartTeamsComponent.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js index 4733198a..c4c29a93 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js @@ -410,7 +410,8 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { isDataChanged = true; } const $contentForm = $$(ids.contentForm); - $contentForm.hide(); + $contentForm.blockEvent(); + $contentForm.destructor(); if (!isDataChanged) return; orgchart.innerHTML = ""; delete newFormData["created_at"]; From 212fb43c843f5453888b6b8e2f3f6387a60449f4 Mon Sep 17 00:00:00 2001 From: guyyoo Date: Tue, 17 Dec 2024 11:27:11 +0700 Subject: [PATCH 068/129] Hardcode date start filter includes NOT NULL values --- .../ABViewOrgChartTeamsComponent.js | 49 +++++++++++-------- 1 file changed, 29 insertions(+), 20 deletions(-) diff --git a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js index c4c29a93..c2ae4d38 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js @@ -210,11 +210,12 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { const newDate = new Date(); orgchart.querySelectorAll(".team-group-record").forEach((e) => { const contentData = JSON.parse(e.dataset.source); - contentData[contentDateEndFieldColumnName] = newDate; - contentData[contentLinkedFieldColumnName] === dataPK && + if (contentData[contentLinkedFieldColumnName] === dataPK) { + contentData[contentDateEndFieldColumnName] = newDate; pendingPromises.push( contentModel.update(contentData.id, contentData) ); + } }); updatedData = {}; updatedData[contentDateStartFieldColumnName] = newDate; @@ -1048,6 +1049,30 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { const contentObj = contentField?.fieldLink?.object; const contentObjPK = contentObj.PK(); const contentFieldFilter = JSON.parse(settings.contentFieldFilter); + const parsedContentFilterRules = [ + // TODO (Guy): Hardcode date start filter. + { + key: contentObj?.fieldByID(settings.contentFieldDateStart)?.id, + rule: "is_not_null", + value: "", + }, + { + glue: "or", + rules: + (contentFieldFilter.rules?.length > 0 && [ + contentFieldFilter, + + // TODO (Guy): Hardcode date end filter. + { + key: contentObj?.fieldByID(settings.contentFieldDateEnd) + ?.id, + rule: "is_null", + value: "", + }, + ]) || + [], + }, + ]; const contentDisplayedFields = settings.contentDisplayedFields; const contentDisplayFieldKeys = Object.keys(contentDisplayedFields); const contentDisplayedFieldFilters = @@ -1093,7 +1118,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { await obj.model().findAll({ where: { glue: "and", - rules: [filterRule, contentFieldFilter], + rules: [filterRule, ...parsedContentFilterRules], }, populate: true, }) @@ -1281,23 +1306,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { rule: "in", value: contentDataRecordPKs, }, - { - glue: "or", - rules: - (contentFieldFilter.rules?.length > 0 && [ - contentFieldFilter, - - // TODO (Guy): Hardcode date end filter. - { - key: contentObj?.fieldByID( - settings.contentFieldDateEnd - )?.id, - rule: "is_null", - value: "", - }, - ]) || - [], - }, + ...parsedContentFilterRules, ], }, populate: true, From c7a0c0b0f71728e564e811a9efd68bad85055f7d Mon Sep 17 00:00:00 2001 From: guyyoo Date: Tue, 17 Dec 2024 13:43:28 +0700 Subject: [PATCH 069/129] PK could be a string of a number --- .../ABViewOrgChartTeamsComponent.js | 63 +++++++++++-------- 1 file changed, 38 insertions(+), 25 deletions(-) diff --git a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js index c2ae4d38..f2dbd927 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js @@ -29,6 +29,29 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { this._cachedContentDataRecords = null; } + _parseFormValueByType(oldFormData, newFormData) { + for (const key in newFormData) { + const oldValue = oldFormData[key]; + const newValue = newFormData[key]; + switch (typeof oldValue) { + case "boolean": + if (newValue == 0) newFormData[key] = false; + else newFormData[key] = true; + break; + case "number": + newFormData[key] = parseInt(newValue); + break; + case "string": + newFormData[key] = newValue?.toString(); + break; + default: + newFormData[key] = newValue; + break; + } + } + return newFormData; + } + ui() { const ids = this.ids; const _ui = super.ui([ @@ -210,7 +233,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { const newDate = new Date(); orgchart.querySelectorAll(".team-group-record").forEach((e) => { const contentData = JSON.parse(e.dataset.source); - if (contentData[contentLinkedFieldColumnName] === dataPK) { + if (contentData[contentLinkedFieldColumnName] == dataPK) { contentData[contentDateEndFieldColumnName] = newDate; pendingPromises.push( contentModel.update(contentData.id, contentData) @@ -219,9 +242,12 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { }); updatedData = {}; updatedData[contentDateStartFieldColumnName] = newDate; - updatedData[contentLinkedFieldColumnName] = dataPK; - updatedData[contentFieldLinkColumnName] = newNodeDataPK; - updatedData[contentGroupByFieldColumnName] = newGroupDataPK; + updatedData[contentLinkedFieldColumnName] = + parseInt(dataPK) || dataPK; + updatedData[contentFieldLinkColumnName] = + parseInt(newNodeDataPK) || newNodeDataPK; + updatedData[contentGroupByFieldColumnName] = + parseInt(newGroupDataPK) || newGroupDataPK; if (this.entityDC) { const entityLink = this.entityDC?.datasource.connectFields( (f) => f.settings.linkObject === contentObj.id @@ -384,32 +410,19 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { click: async () => { const $contentFormData = $$(ids.contentFormData); if (!$contentFormData.validate()) return; - const newFormData = $contentFormData.getValues(); + const newFormData = this._parseFormValueByType( + contentDataRecord, + $contentFormData.getValues() + ); let isDataChanged = false; - for (const key in newFormData) { - const oldValue = contentDataRecord[key]; - const newValue = newFormData[key]; - switch (typeof oldValue) { - case "boolean": - if (newValue == 0) newFormData[key] = false; - else newFormData[key] = true; - break; - case "number": - newFormData[key] = parseInt(newValue); - break; - case "string": - newFormData[key] = newValue?.toString(); - break; - default: - newFormData[key] = newValue; - break; - } + for (const key in newFormData) if ( JSON.stringify(newFormData[key]) !== JSON.stringify(contentDataRecord[key]) - ) + ) { isDataChanged = true; - } + break; + } const $contentForm = $$(ids.contentForm); $contentForm.blockEvent(); $contentForm.destructor(); From 34a7a242ddb2efeba80e385db4c9a39a984d67b7 Mon Sep 17 00:00:00 2001 From: guyyoo Date: Wed, 18 Dec 2024 13:45:15 +0700 Subject: [PATCH 070/129] Fix team UI layouts --- .../ABViewOrgChartTeamsComponent.js | 36 +++++++++++++++++-- styles/team-widget.css | 7 ++-- 2 files changed, 36 insertions(+), 7 deletions(-) diff --git a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js index f2dbd927..6c8d3e28 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js @@ -547,8 +547,6 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { $content.style.display = "none"; return; } - $node.style.height = "300px"; - // $node.style.minHeight = "400px"; const averageHeight = 80 / contentGroupOptionsLength; const currentNodeDataRecordPK = data._rawData[nodeObjPK]; const $nodeSpacer = element("div", "spacer"); @@ -577,7 +575,6 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { const $groupTitle = element("div", "team-group-title"); const groupTitleStyle = $groupTitle.style; groupTitleStyle["backgroundColor"] = groupColor; - groupTitleStyle["height"] = "20%"; $groupTitle.appendChild(document.createTextNode(groupText)); $group.appendChild($groupTitle); } @@ -918,6 +915,39 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { $active.append($span); $buttons.append($active); } + callAfterRender(() => { + const groupHeightThreshold = (246.5 * averageHeight) / 100; + const groupSections = $node.querySelectorAll( + ".team-group-section" + ); + let isOverflow = false; + for (const $groupSection of groupSections) + if ( + $groupSection.getBoundingClientRect().height > + groupHeightThreshold + ) { + isOverflow = true; + break; + } + if (!isOverflow) { + $node.style.height = "300px"; + return; + } + groupSections.forEach(($groupSection) => { + const groupHeight = + $groupSection.getBoundingClientRect().height; + const groupStyle = $groupSection.style; + groupStyle.height = + (groupHeight < groupHeightThreshold && + `${groupHeightThreshold}px`) || + `${groupHeight}px`; + }); + const nodeChildren = $node.children; + nodeChildren.item(0).style.height = "43.5px"; + const $content = nodeChildren.item(1); + $content.style.top = "-22.5px"; + $content.children.item(0).style.height = "24.65px"; + }); }, nodeContent: "description", }); diff --git a/styles/team-widget.css b/styles/team-widget.css index a3151e12..2a20124d 100644 --- a/styles/team-widget.css +++ b/styles/team-widget.css @@ -6,7 +6,6 @@ org-chart .node { text-align: center; justify-content: center; width: 325px !important; - /* height: 600px !important; */ font-family: "Jomhuria", sans-serif; font-style: normal; font-weight: 200; @@ -75,11 +74,12 @@ org-chart tr.lines .downLine { .team-group-section { width: 100%; - padding: 2%; + padding: 8px; } .team-group-title { width: 100%; + height: 20%; } .team-group-content { @@ -87,8 +87,7 @@ org-chart tr.lines .downLine { width: 100%; display: flex; flex-direction: column; - gap: 10%; - overflow: auto; + gap: 3px; } .team-group-record { From b8304a6249d3f540ceaf7ab2eef0b553290022db Mon Sep 17 00:00:00 2001 From: nh758 <7259@pm.me> Date: Wed, 18 Dec 2024 15:35:48 +0700 Subject: [PATCH 071/129] reduce requests to load content data --- .../ABViewOrgChartTeamsComponent.js | 100 +++++++++++++----- 1 file changed, 73 insertions(+), 27 deletions(-) diff --git a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js index 1e4c9ed5..3c108629 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js @@ -113,22 +113,16 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { chartDom.appendChild($chartContent); const $chartContentLayout = $chartContent.children[0]; $chartContentLayout.children[1].children[0].appendChild(orgchart); - this.loadContentData() } async onShow() { this.AB.performance.mark("TeamChart.onShow"); + this.AB.performance.mark("TeamChart.load"); + const loadingJS = this.loadOrgChartJs(); + const loadingData = this.pullData(); super.onShow(); this.busy(); this.generateStrategyCss(); - this.AB.performance.mark("TeamChart.load"); - await Promise.all([this.loadOrgChartJs(), this.pullData()]); - this.AB.performance.measure("TeamChart.load"); - this.AB.performance.mark("TeamChart.display"); - await this.displayOrgChart(); - this.AB.performance.measure("TeamChart.display"); - this.ready(); - this.AB.performance.measure("TeamChart.onShow"); if (this.settings.entityDatacollection) { this.entityDC = this.AB.datacollectionByID( this.settings.entityDatacollection @@ -144,8 +138,9 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { ); } } - this.loadContentData(); - await Promise.all([this.loadOrgChartJs(), this.pullData()]); + // const loadingContent = + this.loadContentDisplayData(); + await Promise.all([loadingJS, loadingData, this.entityDC.waitReady()]); this.AB.performance.measure("TeamChart.load"); this.AB.performance.mark("TeamChart.display"); await this.displayOrgChart(); @@ -442,7 +437,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { const filters = this.__filters; const settings = this.view.settings; const dc = this.view.datacollection; - await dc?.waitForDataCollectionToInitialize(dc); + await dc?.waitReady(dc); let topNode = dc?.getCursor(); if (settings.topTeam) { const topNodeColumn = this.getSettingField("topTeam").columnName; @@ -705,6 +700,25 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { } } + /** + * creates a datacollection to hold the content data linked to the current + * enitity; + */ + loadContentData() { + const contentObj = this.contentObject(); + const connectField = contentObj.connectFields( + (f) => f.settings.linkObject === this.entityDC.datasource.id + )[0]; + const contentDC = this.AB.datacollectionNew({ + datasourceID: contentObj.id, + loadAll: true, + linkDatacollectionID: this.settings.entityDatacollection, + linkFieldID: connectField.id, + }); + this.contentDC = contentDC; + return contentDC.init(); + } + contentObject() { return this.AB.objectByID( this.getSettingField("contentField").settings.linkObject @@ -1431,6 +1445,35 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { $$(this.ids.contentFormData).setValues(contentDataRecord); } + loadContentDisplayData() { + const contentID = this.contentObject().id; + const displayedObjects = Object.keys(this.settings.contentDisplayedFields) + .map((r) => r.split(".")[1]) + .filter((r) => r != contentID); + this.contentDisplayDCs = {}; + displayedObjects.forEach(async (id) => { + const abObj = this.AB.objectByID(id); + const connectField = abObj.connectFields( + (f) => f.settings.linkObject === this.entityDC.datasource.id + )[0]; + this.contentDisplayDCs[id] = this.AB.datacollectionNew({ + id, + name: id, + settings: { + datasourceID: id, + // loadAll: true, + linkDatacollectionID: connectField + ? this.settings.entityDatacollection + : undefined, + linkFieldID: connectField?.id, + fixSelect: "", + }, + }); + await this.contentDisplayDCs[id].init(); + this.contentDisplayDCs[id].loadData(); + }); + } + async contentRecordUI(data, color) { const $ui = element("div", "team-group-record"); $ui.setAttribute("id", this.contentNodeID(data.id)); @@ -1467,6 +1510,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { const displayedObj = AB.objectByID(objID); const displayedFieldID = contentDisplayedFields[displayedFieldKey]; const displayedField = displayedObj.fieldByID(displayedFieldID); + const displayDC = this.contentDisplayDCs[objID]; switch (objID) { case contentObj.id: currentDataRecords = [data]; @@ -1485,21 +1529,23 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { } else if (currentFieldData != null) currentDataPKs.push(currentFieldData); } while (currentDataRecords.length > 0); - currentDataRecords = ( - await displayedObj.model().findAll({ - where: { - glue: "and", - rules: [ - { - key: displayedObj.PK(), - rule: "in", - value: currentDataPKs, - }, - ], - }, - populate: true, - }) - ).data; + currentDataRecords = displayDC.getData((r) => { + return currentDataPKs.some((id) => id == r.id); + }); + // await displayedObj.model().findAll({ + // where: { + // glue: "and", + // rules: [ + // { + // key: displayedObj.PK(), + // rule: "in", + // value: currentDataPKs, + // }, + // ], + // }, + // populate: true, + // }) + // ).data; } break; } From 4bbbe63720371f44cf519a140198a5651e40af8f Mon Sep 17 00:00:00 2001 From: guyyoo Date: Fri, 20 Dec 2024 14:37:00 +0700 Subject: [PATCH 072/129] Disable the zoom feature --- .../views/viewComponent/ABViewOrgChartTeamsComponent.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js index 6c8d3e28..c857f40c 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js @@ -528,7 +528,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { // depth: baseView.settings.depth, chartContainer: `#${ids.chartDom}`, pan: true, // baseView.settings.pan == 1, - zoom: true, //baseView.settings.zoom == 1, + zoom: false, //baseView.settings.zoom == 1, draggable, // visibleLevel: baseView.settings.visibleLevel, parentNodeSymbol: false, From 571fd180b707e8fc53ebeb0b6f362f30651f84d2 Mon Sep 17 00:00:00 2001 From: guyyoo Date: Mon, 23 Dec 2024 10:23:20 +0700 Subject: [PATCH 073/129] Merge nh/teams-performance, fix data update and date type bugs --- .../ABViewOrgChartTeamsComponent.js | 294 ++++++++++-------- 1 file changed, 168 insertions(+), 126 deletions(-) diff --git a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js index fdecb615..ed0475af 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js @@ -29,6 +29,15 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { this._cachedContentDataRecords = null; } + _parseDataPK(dataPK) { + const intDataPk = parseInt(dataPK); + return ( + ((isNaN(intDataPk) || intDataPk.toString().length !== dataPK.length) && + dataPK) || + intDataPk + ); + } + _parseFormValueByType(oldFormData, newFormData) { for (const key in newFormData) { const oldValue = oldFormData[key]; @@ -101,10 +110,13 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { event.dataTransfer.setData("isnode", 1); _oldOnDragStart.call(this.__orgchart, event); }; + + const ids = this.ids; + // SKELETON CHART const orgchart = new OrgChart({ data: { title: "", children: [{}, {}] }, - chartContainer: `#${this.ids.chartDom}`, + chartContainer: `#${ids.chartDom}`, nodeContent: "title", createNode: ($node) => { $node.querySelector(".title").remove(); @@ -113,7 +125,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { $node.classList.add("team-node-skeleton"); }, }); - const chartDom = document.querySelector(`#${this.ids.chartDom}`); + const chartDom = document.querySelector(`#${ids.chartDom}`); chartDom.textContent = ""; chartDom.innerHTML = ""; const ui = { @@ -1283,9 +1295,9 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { async showContentForm(contentDataRecord) { const contentObj = this.contentObject(); const contentModel = contentObj?.model(); - const editContentFieldsToCreateNew = this.getSettingField( - "editContentFieldsToCreateNew" - ); + const settings = this.settings; + const editContentFieldsToCreateNew = + settings.editContentFieldsToCreateNew; const contentDateStartFieldColumnName = this.getSettingField( "contentFieldDateStart" )?.columnName; @@ -1295,118 +1307,114 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { const rules = {}; const labelWidth = 200; + const ids = this.ids; const contentFormElements = await Promise.all( - this.getSettingField("setEditableContentFields").map( - async (fieldID) => { - const field = contentObj.fields( - (field) => field.id === fieldID - )[0]; - if (field == null) + settings.setEditableContentFields.map(async (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. + rules[fieldName] = () => true; + const fieldLabel = field.label; + const settings = field.settings; + switch (fieldKey) { + case "boolean": return { - view: "label", - label: this.label("Missing Field"), + view: "checkbox", + name: fieldName, + label: fieldLabel, + labelWidth, + }; + case "number": + return { + view: "counter", + name: fieldName, + label: fieldLabel, + labelWidth, + type: "number", + }; + 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, + })), + }; + case "user": + case "connectObject": + const fieldLinkObj = field.datasourceLink; + + // TODO (Guy): Fix pulling all connections. + const options = ( + await fieldLinkObj.model().findAll() + ).data.map((e) => ({ + id: e.id, + value: fieldLinkObj.displayData(e), + })); + return field.linkType() === "one" + ? { + view: "combo", + name: fieldName, + label: fieldLabel, + labelWidth, + options, + } + : { + view: "multicombo", + name: fieldName, + label: fieldLabel, + labelWidth, + stringResult: false, + labelAlign: "left", + options, + }; + case "date": + case "datetime": + return { + view: "datepicker", + name: fieldName, + label: fieldLabel, + labelWidth, + timepicker: fieldKey === "datetime", + }; + case "file": + case "image": + // TODO (Guy): Add logic + return { + // view: "", + name: fieldName, + label: fieldLabel, + labelWidth, + }; + // case "json": + // case "LongText": + // case "string": + // case "email": + default: + return { + view: "text", + name: fieldName, + label: fieldLabel, labelWidth, }; - const fieldKey = field.key; - const fieldName = field.columnName; - - // TODO (Guy): Add validators. - rules[fieldName] = () => true; - const fieldLabel = field.label; - const settings = field.settings; - switch (fieldKey) { - case "boolean": - return { - view: "checkbox", - name: fieldName, - label: fieldLabel, - labelWidth, - }; - case "number": - return { - view: "counter", - name: fieldName, - label: fieldLabel, - labelWidth, - type: "number", - }; - 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, - })), - }; - case "user": - case "connectObject": - const fieldLinkObj = field.datasourceLink; - - // TODO (Guy): Fix pulling all connections. - const options = ( - await fieldLinkObj.model().findAll() - ).data.map((e) => ({ - id: e.id, - value: fieldLinkObj.displayData(e), - })); - return field.linkType() === "one" - ? { - view: "combo", - name: fieldName, - label: fieldLabel, - labelWidth, - options, - } - : { - view: "multicombo", - name: fieldName, - label: fieldLabel, - labelWidth, - stringResult: false, - labelAlign: "left", - options, - }; - case "date": - case "datetime": - return { - view: "datepicker", - name: fieldName, - label: fieldLabel, - labelWidth, - timepicker: fieldKey === "datetime", - }; - case "file": - case "image": - // TODO (Guy): Add logic - return { - // view: "", - name: fieldName, - label: fieldLabel, - labelWidth, - }; - // case "json": - // case "LongText": - // case "string": - // case "email": - default: - return { - view: "text", - name: fieldName, - label: fieldLabel, - labelWidth, - }; - } } - ) + }) ); contentFormElements.push({ view: "button", - value: L("Save"), + value: this.label("Save"), css: "webix_primary", click: async () => { const $contentFormData = $$(ids.contentFormData); @@ -1428,7 +1436,6 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { $contentForm.blockEvent(); $contentForm.destructor(); if (!isDataChanged) return; - orgchart.innerHTML = ""; delete newFormData["created_at"]; delete newFormData["updated_at"]; delete newFormData["properties"]; @@ -1481,7 +1488,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { cols: [ { view: "label", - label: `${L("Edit")} ${contentObj.label}`, + label: `${this.label("Edit")} ${contentObj.label}`, align: "center", }, { @@ -1814,6 +1821,27 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { } async fnContentDrop(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(); @@ -1823,13 +1851,14 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { const newNodeDataPK = JSON.parse( $group.parentElement.parentElement.dataset.source )._rawData[nodeObjPK]; - let updatedData = dataTransfer.getData("source"); + let { + source: updatedData, + pk: dataPK, + contentLinkedFieldID, + } = JSON.parse(dataTransfer.getData("text/plain")); const orgchart = this.__orgchart; - if (updatedData === "") { - const dataPK = dataTransfer.getData("dataPK"); - const contentLinkedFieldID = dataTransfer.getData( - "contentLinkedFieldId" - ); + if (!updatedData) { + // This is a drop from Employee list (new assignment) const contentLinkedFieldColumnName = contentObj.fieldByID(contentLinkedFieldID).columnName; const pendingPromises = []; @@ -1845,23 +1874,39 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { }); updatedData = {}; updatedData[contentDateStartFieldColumnName] = newDate; - updatedData[contentLinkedFieldColumnName] = parseInt(dataPK) || dataPK; + updatedData[contentLinkedFieldColumnName] = this._parseDataPK(dataPK); updatedData[contentFieldLinkColumnName] = - parseInt(newNodeDataPK) || newNodeDataPK; + this._parseDataPK(newNodeDataPK); updatedData[contentGroupByFieldColumnName] = - parseInt(newGroupDataPK) || newGroupDataPK; + this._parseDataPK(newGroupDataPK); if (this.entityDC) { const entityLink = this.entityDC?.datasource.connectFields( (f) => f.settings.linkObject === contentObj.id )[0].id; const entityCol = this.AB.definitionByID(entityLink).columnName; - updatedData[entityCol] = this.entityDC.getCursor(); + updatedData[entityCol] = this._parseDataPK( + this.entityDC.getCursor() + ); } - pendingPromises.push(contentModel.create(updatedData)); + pendingPromises.push( + contentModel.create(updatedData), + (async () => { + $group + .querySelector(".team-group-content") + .appendChild(await this.contentRecordUI(updatedData, "grey")); + })() + ); await Promise.all(pendingPromises); } else { - orgchart.innerHTML = ""; updatedData = JSON.parse(updatedData); + + // This is move form another team node + // Move the child node to the target + const dragged = document.querySelector( + `#${this.contentNodeID(updatedData.id)}` + ); + dragged.parentNode.removeChild(dragged); + $group.querySelector(".team-group-content").appendChild(dragged); delete updatedData["created_at"]; delete updatedData["updated_at"]; delete updatedData["properties"]; @@ -1888,9 +1933,6 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { await contentModel.update(updatedData.id, updatedData); } } - - // TODO (Guy): This is refreshing the whole chart. - await this.refresh(); } // HELPERS From e3959ff8b56c1c61ef1bc98ade6ab3f2d882435d Mon Sep 17 00:00:00 2001 From: guyyoo Date: Mon, 23 Dec 2024 17:31:39 +0700 Subject: [PATCH 074/129] Fix connection field rendering in content form --- .../ABViewOrgChartTeamsComponent.js | 48 +++++++++++++------ 1 file changed, 33 insertions(+), 15 deletions(-) diff --git a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js index ed0475af..3879d406 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js @@ -1308,8 +1308,8 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { const rules = {}; const labelWidth = 200; const ids = this.ids; - const contentFormElements = await Promise.all( - settings.setEditableContentFields.map(async (fieldID) => { + const contentFormElements = settings.setEditableContentFields.map( + (fieldID) => { const field = contentObj.fields((field) => field.id === fieldID)[0]; if (field == null) return { @@ -1354,22 +1354,34 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { }; case "user": case "connectObject": - const fieldLinkObj = field.datasourceLink; - - // TODO (Guy): Fix pulling all connections. - const options = ( - await fieldLinkObj.model().findAll() - ).data.map((e) => ({ - id: e.id, - value: fieldLinkObj.displayData(e), - })); + const onViewShow = async function () { + const fieldLinkObj = field.datasourceLink; + try { + // TODO (Guy): Add spinner. + this.define( + "options", + (await fieldLinkObj.model().findAll()).data.map( + (e) => ({ + id: e.id, + value: fieldLinkObj.displayData(e), + }) + ) + ); + this.refresh(); + } catch { + // Close popup before response or possily response fail + } + }; return field.linkType() === "one" ? { view: "combo", name: fieldName, label: fieldLabel, labelWidth, - options, + options: [], + on: { + onViewShow, + }, } : { view: "multicombo", @@ -1378,7 +1390,10 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { labelWidth, stringResult: false, labelAlign: "left", - options, + options: [], + on: { + onViewShow, + }, }; case "date": case "datetime": @@ -1410,7 +1425,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { labelWidth, }; } - }) + } ); contentFormElements.push({ view: "button", @@ -1508,6 +1523,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { { view: "form", id: ids.contentFormData, + hidden: true, elements: contentFormElements, rules, }, @@ -1519,7 +1535,9 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { }, }, }).show(); - $$(ids.contentFormData).setValues(contentDataRecord); + const $contentFormData = $$(ids.contentFormData); + $contentFormData.setValues(contentDataRecord); + $contentFormData.show(); } loadContentDisplayData() { From 2d074dcd96b3611464fd07c3769049ebba608a39 Mon Sep 17 00:00:00 2001 From: guyyoo Date: Tue, 24 Dec 2024 05:18:57 +0700 Subject: [PATCH 075/129] Close popup method, the drop from panel bug, hardcode for the employee field and add spiner on connect fiends --- .../ABViewOrgChartTeamsComponent.js | 58 +++++++++++++++++-- 1 file changed, 54 insertions(+), 4 deletions(-) diff --git a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js index 3879d406..f7e1f4a3 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js @@ -166,9 +166,9 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { // Reload the Chart if our Entity changes this._entityChangeListener = this.entityDC.on( "changeCursor", - () => { + async () => { $$(this.ids.teamFormPopup)?.destructor(); - this.refresh(); + await this.refresh(); } ); } @@ -1354,8 +1354,51 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { }; 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, + 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 () { - const fieldLinkObj = field.datasourceLink; + abWebix.extend(this, abWebix.ProgressBar); + this.showProgress({ type: "icon" }); try { // TODO (Guy): Add spinner. this.define( @@ -1367,7 +1410,9 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { }) ) ); - this.refresh(); + this.hideProgress(); + this.enable(); + await this.refresh(); } catch { // Close popup before response or possily response fail } @@ -1377,6 +1422,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { view: "combo", name: fieldName, label: fieldLabel, + disabled: true, labelWidth, options: [], on: { @@ -1449,6 +1495,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { } const $contentForm = $$(ids.contentForm); $contentForm.blockEvent(); + $contentForm.$view.remove(); $contentForm.destructor(); if (!isDataChanged) return; delete newFormData["created_at"]; @@ -1515,6 +1562,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { click: () => { const $contentForm = $$(ids.contentForm); $contentForm.blockEvent(); + $contentForm.$view.remove(); $contentForm.destructor(); }, }, @@ -1531,6 +1579,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { }, on: { onHide() { + this.$view.remove(); this.destructor(); }, }, @@ -1951,6 +2000,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { await contentModel.update(updatedData.id, updatedData); } } + await this.refresh(); } // HELPERS From 6b3e0f8436e5970f1fe8f6ae08c1992b0ce706ed Mon Sep 17 00:00:00 2001 From: guyyoo Date: Tue, 24 Dec 2024 08:51:55 +0700 Subject: [PATCH 076/129] Save position when chart is refreshed --- .../viewComponent/ABViewOrgChartTeamsComponent.js | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js index f7e1f4a3..693523a2 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js @@ -816,8 +816,19 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { async refresh() { this.busy(); - await this.pullData(); - await this.displayOrgChart(); + let orgchart = this.__orgchart; + if (orgchart != null) { + const dataPanStart = orgchart.dataset.panStart; + const style = orgchart.getAttribute("style"); + await this.pullData(); + await this.displayOrgChart(); + orgchart = this.__orgchart; + orgchart.dataset.panStart = dataPanStart; + orgchart.setAttribute("style", style); + } else { + await this.pullData(); + await this.displayOrgChart(); + } this.ready(); } From 83ddc343d96a5d88c389e3e5f1c6481cfcf2d424 Mon Sep 17 00:00:00 2001 From: guyyoo Date: Tue, 24 Dec 2024 10:19:22 +0700 Subject: [PATCH 077/129] Sort and update the panel after update --- .../viewComponent/ABViewOrgChartTeamsComponent.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js index 693523a2..1e09f842 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js @@ -426,6 +426,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { for (const key in dataPanelDCs) { const dc = AB.datacollectionByID(key.split(".")[1]); await dc.waitForDataCollectionToInitialize(dc); + await dc.reloadData(); const panelObj = dc.datasource; const contentFieldID = panelObj.connectFields( (field) => field.datasourceLink.id == contentObjID @@ -440,7 +441,16 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { data )}`, css: { overflow: "auto" }, - data: dc.getData(), + // TODO (Guy): Force to sort by firstName. the DC sort setting work but after calling DC.parse() in DC.queuedParse() method the sort is messy. + data: dc.getData().sort((a, b) => { + if (a.firstName < b.firstName) { + return -1; + } + if (a.firstName > b.firstName) { + return 1; + } + return 0; + }), on: { onAfterRender() { callAfterRender(() => { From f118d5a293dcf964aaa810e1d81a72001c067b4e Mon Sep 17 00:00:00 2001 From: guyyoo Date: Tue, 24 Dec 2024 11:20:45 +0700 Subject: [PATCH 078/129] Fix styles --- .../ABViewOrgChartTeamsComponent.js | 134 ++++++------------ 1 file changed, 47 insertions(+), 87 deletions(-) diff --git a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js index 1e09f842..45fb116c 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js @@ -1130,11 +1130,12 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { const linkField = this.AB.definitionByID( this.getSettingField("teamLink").settings.linkColumn ).columnName; + const ids = this.ids; if (!$teamFormPopup) { - const nameField = this.getSettingField("teamName"); - const [strategyField] = this.datacollection.datasource.fields( - (f) => f.id == this.view.settings["teamStrategy"] - ); + const teamObj = this.datacollection.datasource; + const settings = this.settings; + const nameField = teamObj.fieldByID(settings.teamName); + const strategyField = teamObj.fieldByID(settings.teamStrategy); const strategyObj = this.AB.objectByID( strategyField.settings.linkObject ); @@ -1166,39 +1167,23 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { value: o[`${subCol}__relation`].name, }; }); - $teamFormPopup = webix.ui({ view: "popup", - id: this.ids.teamFormPopup, + id: ids.teamFormPopup, close: true, position: "center", - css: { "border-radius": "15px" }, + css: { "border-radius": "10px" }, body: { - type: "clean", rows: [ { view: "toolbar", - id: "myToolbar", - borderless: true, - css: { - background: "#1a3e72", - }, + css: "webix_dark", cols: [ { width: 5 }, { - view: "template", - template: "#value#", - align: "center", - height: 40, - css: { - "font-weight": 400, - "font-size": "32px", - "font-family": `"Jomhuria", sans-serif`, - color: "white", - background: "transparent", - border: "none", - }, - id: this.ids.teamFormTitle, + id: ids.teamFormTitle, + view: "label", + align: "left", }, { view: "icon", @@ -1211,80 +1196,60 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { }, { view: "form", - id: this.ids.teamForm, + id: ids.teamForm, borderless: true, elements: [ { view: "text", - label: nameField.label ?? nameField.columnName, + label: nameField.label, name: nameField.columnName, required: true, }, { view: "richselect", - label: - strategyField.label ?? strategyField.columnName, + label: strategyField.label, name: strategyField.columnName, options: strategyOptions, required: true, }, { view: "switch", - id: this.ids.teamFormInactive, + id: ids.teamFormInactive, name: inactive, label: "Inactive", }, { view: "text", name: "id", hidden: true }, { view: "text", name: linkField, hidden: true }, { - cols: [ - {}, - { - view: "template", - id: this.ids.teamFormSubmit, - template: ``, - data: { disabled: "disabled" }, - borderless: true, - height: 31, - onClick: { - "team-form-button": () => { - const values = $$( - this.ids.teamForm - ).getValues(); - const strategy = strategyOptions.find( - (f) => - f.id === - values[strategyField.columnName] - ); - if (values.id) { - this.teamEdit(values, strategy); - } else { - this.teamAddChild(values, strategy); - } - $teamFormPopup.hide(); - }, - }, - }, - {}, - ], + id: ids.teamFormSubmit, + view: "button", + value: this.label("Save"), + disabled: true, + css: "webix_primary", + click: () => { + const values = $$(ids.teamForm).getValues(); + const strategy = strategyOptions.find( + (f) => + f.id === values[strategyField.columnName] + ); + if (values.id) { + this.teamEdit(values, strategy); + } else { + this.teamAddChild(values, strategy); + } + $teamFormPopup.hide(); + }, }, ], on: { onChange: () => { - const values = $$(this.ids.teamForm).getValues(); + const values = $$(ids.teamForm).getValues(); const valid = !!values[strategyField.columnName] && !!values[nameField.columnName]; - if (valid) - $$(this.ids.teamFormSubmit).setValues({ - disabled: "", - }); - else - $$(this.ids.teamFormSubmit).setValues({ - disabled: "disabled", - }); + const $teamFormSubmit = $$(ids.teamFormSubmit); + if (valid) $teamFormSubmit.enable(); + else $teamFormSubmit.disable(); }, }, }, @@ -1296,17 +1261,13 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { values[linkField] = values.__parentID; delete values.__parentID; } - $$(this.ids.teamFormTitle).setValues({ - value: `${this.label(mode)} Team`, - }); - $$(this.ids.teamForm).setValues(values); - $$(this.ids.teamFormSubmit).setValues({ - disabled: "disabled", - }); + $$(ids.teamFormTitle).setValue(`${this.label(mode)} Team`); + $$(ids.teamForm).setValues(values); + $$(ids.teamFormSubmit).disable(); this.teamCanInactivate(values) - ? $$(this.ids.teamFormInactive).enable() - : $$(this.ids.teamFormInactive).disable(); + ? $$(ids.teamFormInactive).enable() + : $$(ids.teamFormInactive).disable(); if (mode === "Edit") { // Check if we can inactivate } @@ -1566,20 +1527,19 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { rows: [ { view: "toolbar", - id: "myToolbar", css: "webix_dark", cols: [ + { width: 5 }, { view: "label", label: `${this.label("Edit")} ${contentObj.label}`, - align: "center", + align: "left", }, { - view: "button", - value: "X", - width: 60, + view: "icon", + icon: "fa fa-times", align: "right", - css: "webix_transparent", + width: 60, click: () => { const $contentForm = $$(ids.contentForm); $contentForm.blockEvent(); From cb64885c53e62c5dc44a33539fb082869d353612 Mon Sep 17 00:00:00 2001 From: guyyoo Date: Tue, 24 Dec 2024 11:46:57 +0700 Subject: [PATCH 079/129] Add confirm popup for changing content form data --- .../ABViewOrgChartTeamsComponent.js | 81 +++++++++++-------- 1 file changed, 47 insertions(+), 34 deletions(-) diff --git a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js index 45fb116c..495fc94a 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js @@ -1462,11 +1462,11 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { click: async () => { const $contentFormData = $$(ids.contentFormData); if (!$contentFormData.validate()) return; + let isDataChanged = false; const newFormData = this._parseFormValueByType( contentDataRecord, $contentFormData.getValues() ); - let isDataChanged = false; for (const key in newFormData) if ( JSON.stringify(newFormData[key]) !== @@ -1480,40 +1480,53 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { $contentForm.$view.remove(); $contentForm.destructor(); if (!isDataChanged) return; - delete newFormData["created_at"]; - delete newFormData["updated_at"]; - delete newFormData["properties"]; - for (const editContentFieldToCreateNew of editContentFieldsToCreateNew) { - const editContentFieldToCreateNewColumnName = - contentObj.fieldByID(editContentFieldToCreateNew)?.columnName; - if ( - JSON.stringify( - newFormData[editContentFieldToCreateNewColumnName] ?? "" - ) !== - JSON.stringify( - contentDataRecord[editContentFieldToCreateNewColumnName] ?? - "" - ) - ) { - const pendingPromises = []; - const oldData = {}; - oldData[contentDateEndFieldColumnName] = new Date(); - pendingPromises.push( - contentModel.update(newFormData.id, oldData) - ); - newFormData[contentDateStartFieldColumnName] = - oldData[contentDateEndFieldColumnName]; - delete newFormData["id"]; - delete newFormData["uuid"]; - delete newFormData[contentDateEndFieldColumnName]; - pendingPromises.push(contentModel.create(newFormData)); - await Promise.all(pendingPromises); + webix + .confirm({ + title: "Title", + ok: "Yes", + cancel: "No", + text: "You are about to confirm. Are you sure?", + }) + .then(async () => { + delete newFormData["created_at"]; + delete newFormData["updated_at"]; + delete newFormData["properties"]; + for (const editContentFieldToCreateNew of editContentFieldsToCreateNew) { + const editContentFieldToCreateNewColumnName = + contentObj.fieldByID( + editContentFieldToCreateNew + )?.columnName; + if ( + JSON.stringify( + newFormData[editContentFieldToCreateNewColumnName] ?? + "" + ) !== + JSON.stringify( + contentDataRecord[ + editContentFieldToCreateNewColumnName + ] ?? "" + ) + ) { + const pendingPromises = []; + const oldData = {}; + oldData[contentDateEndFieldColumnName] = new Date(); + pendingPromises.push( + contentModel.update(newFormData.id, oldData) + ); + newFormData[contentDateStartFieldColumnName] = + oldData[contentDateEndFieldColumnName]; + delete newFormData["id"]; + delete newFormData["uuid"]; + delete newFormData[contentDateEndFieldColumnName]; + pendingPromises.push(contentModel.create(newFormData)); + await Promise.all(pendingPromises); + await this.refresh(); + return; + } + } + await contentModel.update(newFormData.id, newFormData); await this.refresh(); - return; - } - } - await contentModel.update(newFormData.id, newFormData); - await this.refresh(); + }); }, }); AB.Webix.ui({ From b9895844566ca2f12896d43cc5c21afed43faf64 Mon Sep 17 00:00:00 2001 From: guyyoo Date: Tue, 24 Dec 2024 16:45:35 +0700 Subject: [PATCH 080/129] Fix bug the chart does not load sometimes --- .../ABViewDataSelectComponent.js | 22 +++---- .../ABViewOrgChartTeamsComponent.js | 64 ++++++++++++------- 2 files changed, 51 insertions(+), 35 deletions(-) diff --git a/AppBuilder/platform/views/viewComponent/ABViewDataSelectComponent.js b/AppBuilder/platform/views/viewComponent/ABViewDataSelectComponent.js index 8a22e68a..7455bb43 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewDataSelectComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewDataSelectComponent.js @@ -31,26 +31,24 @@ 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); + const $select = $$(this.ids.select); + $select.define("options", options); + $select.refresh(); + $select.setValue(dc.getCursor().id); } cursorChange(n) { - this.dc.setCursor(n); + this.datacollectio.setCursor(n); } } diff --git a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js index 495fc94a..81eb369d 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js @@ -151,31 +151,45 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { } async onShow() { - this.AB.performance.mark("TeamChart.onShow"); - this.AB.performance.mark("TeamChart.load"); - const loadingJS = this.loadOrgChartJs(); - const loadingData = this.pullData(); super.onShow(); this.busy(); - this.generateStrategyCss(); + this.AB.performance.mark("TeamChart.onShow"); + this.AB.performance.mark("TeamChart.load"); if (this.settings.entityDatacollection) { - this.entityDC = this.AB.datacollectionByID( + const entityDC = (this.entityDC = this.AB.datacollectionByID( this.settings.entityDatacollection - ); + )); if (!this._entityChangeListener) { // Reload the Chart if our Entity changes - this._entityChangeListener = this.entityDC.on( + this._entityChangeListener = entityDC.on( "changeCursor", async () => { + debugger; $$(this.ids.teamFormPopup)?.destructor(); await this.refresh(); } ); } + + // TODO (Guy): Figure out later why calling dc.waitReay() won't resolve on entity select widget if we also call it here. + await new Promise((resolve) => { + const waitEntityDCReady = () => { + if ( + entityDC.dataStatus !== entityDC.dataStatusFlag.initialized + ) { + setTimeout(() => { + waitEntityDCReady(); + }, 1000); + return; + } + resolve(); + }; + waitEntityDCReady(); + }); } - // const loadingContent = + this.generateStrategyCss(); this.loadContentDisplayData(); - await Promise.all([loadingJS, loadingData, this.entityDC.waitReady()]); + await Promise.all([this.loadOrgChartJs(), this.pullData()]); this.AB.performance.measure("TeamChart.load"); this.AB.performance.mark("TeamChart.display"); await this.displayOrgChart(); @@ -805,8 +819,9 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { */ loadContentData() { const contentObj = this.contentObject(); + const entityDC = this.entityDC; const connectField = contentObj.connectFields( - (f) => f.settings.linkObject === this.entityDC.datasource.id + (f) => f.settings.linkObject === entityDC.datasource.id )[0]; const contentDC = this.AB.datacollectionNew({ datasourceID: contentObj.id, @@ -1001,13 +1016,15 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { } async teamAddChild(values, strategy) { + const entityDC = this.entityDC; + // Add the entity value - if (this.entityDC) { - const connection = this.entityDC.datasource.connectFields( - (f) => f.settings.linkObject === this.datacollection.datasource.id + if (entityDC) { + const connection = entityDC.datasource.connectFields( + (f) => f.settings.linkObject === datacollection.datasource.id )[0]; if (connection) { - const entity = this.entityDC.getCursor(); + const entity = entityDC.getCursor(); const cName = this.AB.definitionByID( connection.settings.linkColumn ).columnName; @@ -1139,15 +1156,16 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { const strategyObj = this.AB.objectByID( strategyField.settings.linkObject ); + const entityDC = this.entityDC; const entityLink = strategyObj.connectFields( - (f) => f.settings.linkObject === this.entityDC.datasource.id + (f) => f.settings.linkObject === entityDC.datasource.id )[0]; const cond = { glue: "and", rules: [ { key: entityLink.columnName, - value: this.entityDC.getCursor().id, + value: entityDC.getCursor().id, rule: "equals", }, ], @@ -1589,10 +1607,11 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { .map((r) => r.split(".")[1]) .filter((r) => r != contentID); this.contentDisplayDCs = {}; + const entityDC = this.entityDC; displayedObjects.forEach(async (id) => { const abObj = this.AB.objectByID(id); const connectField = abObj.connectFields( - (f) => f.settings.linkObject === this.entityDC.datasource.id + (f) => f.settings.linkObject === entityDC.datasource.id )[0]; this.contentDisplayDCs[id] = this.AB.datacollectionNew({ id, @@ -1940,14 +1959,13 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { this._parseDataPK(newNodeDataPK); updatedData[contentGroupByFieldColumnName] = this._parseDataPK(newGroupDataPK); - if (this.entityDC) { - const entityLink = this.entityDC?.datasource.connectFields( + 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( - this.entityDC.getCursor() - ); + updatedData[entityCol] = this._parseDataPK(entityDC.getCursor()); } pendingPromises.push( contentModel.create(updatedData), From 0538d1fcfebda4c8058ec793bc06de1c2e24aefb Mon Sep 17 00:00:00 2001 From: guyyoo Date: Tue, 24 Dec 2024 18:47:19 +0700 Subject: [PATCH 081/129] Fix bug typo and some performance improvements --- .../ABViewDataSelectComponent.js | 2 +- .../ABViewOrgChartTeamsComponent.js | 79 +++++++++++-------- 2 files changed, 49 insertions(+), 32 deletions(-) diff --git a/AppBuilder/platform/views/viewComponent/ABViewDataSelectComponent.js b/AppBuilder/platform/views/viewComponent/ABViewDataSelectComponent.js index 7455bb43..57660190 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewDataSelectComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewDataSelectComponent.js @@ -49,6 +49,6 @@ export default class ABViewDataSelectComponent extends ABViewComponent { } cursorChange(n) { - this.datacollectio.setCursor(n); + this.datacollection.setCursor(n); } } diff --git a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js index 81eb369d..3acfa7e4 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js @@ -27,6 +27,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { }; this._chartData = null; this._cachedContentDataRecords = null; + this.contentDisplayDCs = null; } _parseDataPK(dataPK) { @@ -88,8 +89,6 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { } async loadOrgChartJs() { - this.busy(); - const [orgChartLoader] = await Promise.all([ import( /* webpackPrefetch: true */ @@ -152,49 +151,55 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { async onShow() { super.onShow(); - this.busy(); this.AB.performance.mark("TeamChart.onShow"); this.AB.performance.mark("TeamChart.load"); + this.generateStrategyCss(); + this.busy(); + await this.loadOrgChartJs(); if (this.settings.entityDatacollection) { const entityDC = (this.entityDC = this.AB.datacollectionByID( this.settings.entityDatacollection )); + let entityDCReadyResolve = null; if (!this._entityChangeListener) { // Reload the Chart if our Entity changes this._entityChangeListener = entityDC.on( "changeCursor", async () => { - debugger; - $$(this.ids.teamFormPopup)?.destructor(); await this.refresh(); + if (entityDCReadyResolve != null) { + entityDCReadyResolve(); + entityDCReadyResolve = null; + } } ); } // TODO (Guy): Figure out later why calling dc.waitReay() won't resolve on entity select widget if we also call it here. - await new Promise((resolve) => { - const waitEntityDCReady = () => { - if ( - entityDC.dataStatus !== entityDC.dataStatusFlag.initialized - ) { - setTimeout(() => { - waitEntityDCReady(); - }, 1000); - return; - } - resolve(); - }; - waitEntityDCReady(); - }); - } - this.generateStrategyCss(); - this.loadContentDisplayData(); - await Promise.all([this.loadOrgChartJs(), this.pullData()]); + await Promise.all([ + new Promise((resolve) => { + const waitEntityDCReady = () => { + if ( + entityDC.dataStatus !== entityDC.dataStatusFlag.initialized + ) { + setTimeout(() => { + waitEntityDCReady(); + }, 1000); + return; + } + resolve(); + }; + waitEntityDCReady(); + }), + new Promise((resolve) => { + entityDCReadyResolve = resolve; + }), + ]); + } else await this.refresh(); + this.ready(); this.AB.performance.measure("TeamChart.load"); this.AB.performance.mark("TeamChart.display"); - await this.displayOrgChart(); this.AB.performance.measure("TeamChart.display"); - this.ready(); this.AB.performance.measure("TeamChart.onShow"); } @@ -840,7 +845,8 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { } async refresh() { - this.busy(); + $$(this.ids.teamFormPopup)?.destructor(); + this.loadContentDisplayData(); let orgchart = this.__orgchart; if (orgchart != null) { const dataPanStart = orgchart.dataset.panStart; @@ -854,7 +860,6 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { await this.pullData(); await this.displayOrgChart(); } - this.ready(); } get chartData() { @@ -1506,6 +1511,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { text: "You are about to confirm. Are you sure?", }) .then(async () => { + this.busy(); delete newFormData["created_at"]; delete newFormData["updated_at"]; delete newFormData["properties"]; @@ -1539,11 +1545,13 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { pendingPromises.push(contentModel.create(newFormData)); await Promise.all(pendingPromises); await this.refresh(); + this.ready(); return; } } await contentModel.update(newFormData.id, newFormData); await this.refresh(); + this.ready(); }); }, }); @@ -1602,18 +1610,24 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { } loadContentDisplayData() { + let contentDisplayDCs = this.contentDisplayDCs; + if (contentDisplayDCs != null) { + for (const key in contentDisplayDCs) + contentDisplayDCs[key].reloadData(); + return; + } const contentID = this.contentObject().id; const displayedObjects = Object.keys(this.settings.contentDisplayedFields) .map((r) => r.split(".")[1]) .filter((r) => r != contentID); - this.contentDisplayDCs = {}; + contentDisplayDCs = this.contentDisplayDCs = {}; const entityDC = this.entityDC; displayedObjects.forEach(async (id) => { const abObj = this.AB.objectByID(id); const connectField = abObj.connectFields( (f) => f.settings.linkObject === entityDC.datasource.id )[0]; - this.contentDisplayDCs[id] = this.AB.datacollectionNew({ + contentDisplayDCs[id] = this.AB.datacollectionNew({ id, name: id, settings: { @@ -1626,8 +1640,8 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { fixSelect: "", }, }); - await this.contentDisplayDCs[id].init(); - this.contentDisplayDCs[id].loadData(); + contentDisplayDCs[id].init(); + await contentDisplayDCs[id].loadData(); }); } @@ -1686,6 +1700,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { } else if (currentFieldData != null) currentDataPKs.push(currentFieldData); } while (currentDataRecords.length > 0); + await displayDC.waitReady(); currentDataRecords = displayDC.getData((r) => { return currentDataPKs.some((id) => id == r.id); }); @@ -1926,6 +1941,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { if (dataTransfer.getData("isnode") == 1) return; event.stopPropagation(); if (contentFieldLinkColumnName == null) return; + this.busy(); const $group = event.currentTarget; const newGroupDataPK = $group.dataset.pk; const newNodeDataPK = JSON.parse( @@ -2013,6 +2029,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { } } await this.refresh(); + this.ready(); } // HELPERS From e5461ee3a401d93bee497feca8613c1e560e7dce Mon Sep 17 00:00:00 2001 From: guyyoo Date: Thu, 26 Dec 2024 20:50:43 +0700 Subject: [PATCH 082/129] Paging and optimizing the data panel load --- .../ABViewOrgChartTeamsComponent.js | 126 +++++++++++------- 1 file changed, 79 insertions(+), 47 deletions(-) diff --git a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js index 3acfa7e4..24b0026c 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js @@ -204,6 +204,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { } async displayOrgChart() { + const self = this; const baseView = this.view; const AB = this.AB; const chartData = AB.cloneDeep(this.chartData); @@ -424,6 +425,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { chartDom.textContent = ""; chartDom.innerHTML = ""; const ui = { + hidden: true, cols: [ { rows: [ @@ -440,18 +442,17 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { ], }; if (settings.showDataPanel === 1) { - const dataPanelUIs = []; const dataPanelDCs = settings.dataPanelDCs; + const cells = []; for (const key in dataPanelDCs) { const dc = AB.datacollectionByID(key.split(".")[1]); - await dc.waitForDataCollectionToInitialize(dc); - await dc.reloadData(); + const $dc = dc.$dc; const panelObj = dc.datasource; const contentFieldID = panelObj.connectFields( (field) => field.datasourceLink.id == contentObjID )[0].fieldLink.id; - const self = this; - dataPanelUIs.push({ + const DC_OFFSET = 20; + cells.push({ header: dataPanelDCs[key], body: { view: "list", @@ -459,47 +460,76 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { `
${panelObj.displayData( data )}
`, - css: { overflow: "auto" }, - // TODO (Guy): Force to sort by firstName. the DC sort setting work but after calling DC.parse() in DC.queuedParse() method the sort is messy. - data: dc.getData().sort((a, b) => { - if (a.firstName < b.firstName) { - return -1; - } - if (a.firstName > b.firstName) { - return 1; - } - return 0; - }), + css: { overflow: "auto", "max-height": "85%" }, + data: [], on: { - onAfterRender() { - callAfterRender(() => { - const $itemElements = - this.$view.children.item(0).children; - for (const $itemElement of $itemElements) { - $itemElement.setAttribute( - "data-content-linked-field-id", - contentFieldID - ); - $itemElement.setAttribute( - "data-pk", - dc.getData( - (e) => - e.id == - $itemElement.getAttribute( - "webix_l_id" - ) - )[0][panelObj.PK()] - ); - $itemElement.setAttribute("draggable", "true"); - $itemElement.addEventListener( - "dragstart", - (e) => self.fnContentDragStart(e) - ); - $itemElement.addEventListener("dragend", (e) => - self.fnContentDragEnd(e) - ); - } - }); + onViewShow(prevCount = 0) { + (async () => { + await dc.waitReady(); + this.define( + "data", + // TODO (Guy): Force to sort by firstName. the DC sort setting work but after calling DC.parse() in DC.queuedParse() method the sort is messy. + dc.getData().sort((a, b) => { + if (a.firstName < b.firstName) { + return -1; + } + if (a.firstName > b.firstName) { + return 1; + } + return 0; + }) + ); + this.refresh(); + callAfterRender(() => { + const $itemElements = + this.$view.children.item(0).children; + const itemElementsLength = + $itemElements.length; + let count = 0; + while (count < itemElementsLength) { + const $itemElement = $itemElements.item( + count++ + ); + $itemElement.setAttribute( + "data-content-linked-field-id", + contentFieldID + ); + $itemElement.setAttribute( + "data-pk", + dc.getData( + (e) => + e.id == + $itemElement.getAttribute( + "webix_l_id" + ) + )[0][panelObj.PK()] + ); + $itemElement.setAttribute( + "draggable", + "true" + ); + $itemElement.addEventListener( + "dragstart", + (e) => self.fnContentDragStart(e) + ); + $itemElement.addEventListener( + "dragend", + (e) => self.fnContentDragEnd(e) + ); + } + if (count < DC_OFFSET || count === prevCount) + return; + (async () => { + const RECORD_LIMIT = 20; + await dc.loadData( + RECORD_LIMIT * + parseInt(count / RECORD_LIMIT), + RECORD_LIMIT + ); + this.callEvent("onViewShow", [count]); + })(); + }); + })(); }, }, }, @@ -513,10 +543,12 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { type: "bottom", css: "webix_dark", }, - cells: dataPanelUIs, + cells, }); } - const $chartContent = AB.Webix.ui(ui).$view; + const $orgChartTeams = AB.Webix.ui(ui); + $orgChartTeams.show(); + const $chartContent = $orgChartTeams.$view; chartDom.appendChild($chartContent); const $chartContentLayout = $chartContent.children[0]; this.initFilter($chartContentLayout.children[0].children[0]); From 53ca533ddcd2211c8f891e49d60430395652994f Mon Sep 17 00:00:00 2001 From: guyyoo Date: Thu, 2 Jan 2025 14:09:48 +0700 Subject: [PATCH 083/129] Improving performance and refreshing data panels --- .../ABViewOrgChartTeamsComponent.js | 3949 +++++++++-------- 1 file changed, 2027 insertions(+), 1922 deletions(-) diff --git a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js index 24b0026c..d7d9757f 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js @@ -1,5 +1,6 @@ const ABViewComponent = require("./ABViewComponent").default; - +const DC_OFFSET = 20; +const RECORD_LIMIT = 20; module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { constructor(baseView, idBase, ids) { super( @@ -9,6 +10,8 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { { chartView: "", chartDom: "", + chartContent: "", + dataPanel: "", filterPopup: "", filterForm: "", contentForm: "", @@ -22,74 +25,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { ids ) ); - this.__filters = { - inactive: 0, - }; - this._chartData = null; - this._cachedContentDataRecords = null; - this.contentDisplayDCs = null; - } - - _parseDataPK(dataPK) { - const intDataPk = parseInt(dataPK); - return ( - ((isNaN(intDataPk) || intDataPk.toString().length !== dataPK.length) && - dataPK) || - intDataPk - ); - } - - _parseFormValueByType(oldFormData, newFormData) { - for (const key in newFormData) { - const oldValue = oldFormData[key]; - const newValue = newFormData[key]; - switch (typeof oldValue) { - case "boolean": - if (newValue == 0) newFormData[key] = false; - else newFormData[key] = true; - break; - case "number": - newFormData[key] = parseInt(newValue); - break; - case "string": - newFormData[key] = newValue?.toString(); - break; - default: - newFormData[key] = newValue; - break; - } - } - return newFormData; - } - - ui() { - const ids = this.ids; - const _ui = super.ui([ - { - id: ids.chartView, - view: "template", - template: `
`, - css: { - position: "relative", - }, - }, - ]); - - delete _ui.type; - - return _ui; - } - - async init(AB, accessLevel) { - await super.init(AB, accessLevel); - - const $chartView = $$(this.ids.chartView); - if ($chartView) - this.AB.Webix.extend($chartView, this.AB.Webix.ProgressBar); - } - - async loadOrgChartJs() { - const [orgChartLoader] = await Promise.all([ + this._resources = [ import( /* webpackPrefetch: true */ "../../../../js/orgchart-webcomponents.js" @@ -102,942 +38,825 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { /* webpackPrefetch: true */ "../../../../styles/team-widget.css" ), - ]); - const OrgChart = (this.OrgChart = orgChartLoader.default); - const _oldOnDragStart = OrgChart.prototype._onDragStart; - OrgChart.prototype._onDragStart = (event) => { - event.dataTransfer.setData("isnode", 1); - _oldOnDragStart.call(this.__orgchart, event); + ]; + this.__filters = { + inactive: 0, }; - - const ids = this.ids; - - // SKELETON CHART - const orgchart = new OrgChart({ - data: { title: "", children: [{}, {}] }, - chartContainer: `#${ids.chartDom}`, - nodeContent: "title", - createNode: ($node) => { - $node.querySelector(".title").remove(); - $node.querySelector(".content").innerHTML = ""; - - $node.classList.add("team-node-skeleton"); - }, + this._OrgChart = null; + this._resolveInit = null; + this._promiseInit = new Promise((resolve) => { + this._resolveInit = resolve; }); - const chartDom = document.querySelector(`#${ids.chartDom}`); - chartDom.textContent = ""; - chartDom.innerHTML = ""; - const ui = { - cols: [ - { - rows: [ - { - view: "template", - height: 50, - }, - { - view: "template", - scroll: "auto", - }, - ], - }, - ], - }; - const $chartContent = AB.Webix.ui(ui).$view; - chartDom.appendChild($chartContent); - const $chartContentLayout = $chartContent.children[0]; - $chartContentLayout.children[1].children[0].appendChild(orgchart); - } + this._contentDC = null; + this._contentGroupDC = null; + this._contentDisplayDCs = []; + this._dataPanelDCs = []; + this._entityDC = null; + this._chartData = null; - async onShow() { - super.onShow(); - this.AB.performance.mark("TeamChart.onShow"); - this.AB.performance.mark("TeamChart.load"); - this.generateStrategyCss(); - this.busy(); - await this.loadOrgChartJs(); - if (this.settings.entityDatacollection) { - const entityDC = (this.entityDC = this.AB.datacollectionByID( - this.settings.entityDatacollection - )); - let entityDCReadyResolve = null; - if (!this._entityChangeListener) { - // Reload the Chart if our Entity changes - this._entityChangeListener = entityDC.on( - "changeCursor", - async () => { - await this.refresh(); - if (entityDCReadyResolve != null) { - entityDCReadyResolve(); - entityDCReadyResolve = 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; } - - // TODO (Guy): Figure out later why calling dc.waitReay() won't resolve on entity select widget if we also call it here. - await Promise.all([ - new Promise((resolve) => { - const waitEntityDCReady = () => { - if ( - entityDC.dataStatus !== entityDC.dataStatusFlag.initialized - ) { - setTimeout(() => { - waitEntityDCReady(); - }, 1000); - return; - } - resolve(); - }; - waitEntityDCReady(); - }), - new Promise((resolve) => { - entityDCReadyResolve = resolve; - }), - ]); - } else await this.refresh(); - this.ready(); - this.AB.performance.measure("TeamChart.load"); - this.AB.performance.mark("TeamChart.display"); - this.AB.performance.measure("TeamChart.display"); - this.AB.performance.measure("TeamChart.onShow"); - } - - async displayOrgChart() { - const self = this; - const baseView = this.view; - const AB = this.AB; - const chartData = AB.cloneDeep(this.chartData); - const settings = baseView.settings; - const showGroupTitle = settings.showGroupTitle === 1; - const draggable = settings.draggable === 1; - const nodeDC = baseView.datacollection; - const nodeModel = baseView.datacollection.model; - const nodeObj = nodeDC?.datasource; - const nodeObjPK = nodeObj.PK(); - const contentFieldLink = nodeObj.fieldByID( - settings.contentField - )?.fieldLink; - const contentObj = this.contentObject(); - const contentGroupByField = contentObj?.fieldByID( - settings.contentGroupByField - ); - this.AB.performance.mark("loadAssigmentType"); - const contentGroupObj = this.AB.objectByID( - contentGroupByField?.settings.linkObject - ); - const { data: contentGroupOptions } = await contentGroupObj - .model() - .findAll(); - const groupObjPKColumeName = contentGroupObj.PK(); - this.AB.performance.measure("loadAssigmentType"); - this.AB.performance.mark("misc"); - const contentGroupOptionsLength = contentGroupOptions.length; - const contentGroupByFieldColumnName = contentGroupByField?.columnName; - const contentFieldLinkColumnName = contentFieldLink?.columnName; - const contentObjID = contentObj?.id; - const strategyColors = settings.strategyColors; - const ids = this.ids; - const callAfterRender = (callback) => { - requestAnimationFrame(() => { - requestAnimationFrame(callback); - }); + dataTransfer.setData("text/plain", JSON.stringify(data)); + // $eventTarget.style.opacity = "0.5"; }; - - const contentDataRecords = this._cachedContentDataRecords; - this.AB.performance.measure("misc"); - this.AB.performance.mark("createOrgChart"); - const orgchart = new this.OrgChart({ - data: chartData, - direction: baseView.settings.direction, - // depth: baseView.settings.depth, - chartContainer: `#${ids.chartDom}`, - pan: true, // baseView.settings.pan == 1, - zoom: false, //baseView.settings.zoom == 1, - draggable, - // visibleLevel: baseView.settings.visibleLevel, - parentNodeSymbol: false, - exportButton: baseView.settings.export, - exportFilename: baseView.settings.exportFilename, - createNode: async ($node, data) => { - // remove built in icon - $node.querySelector(".title > i")?.remove(); - - // customize - const $content = $node.children.item(1); - $content.innerHTML = ""; - 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 averageHeight = 80 / contentGroupOptionsLength; - const currentNodeDataRecordPK = data._rawData[nodeObjPK]; - const $nodeSpacer = element("div", "spacer"); - $content.appendChild($nodeSpacer); - const nodeSpacerStyle = $nodeSpacer.style; - nodeSpacerStyle.backgroundColor = ""; - const strategyColor = - strategyColors[$node.classList.item(1).replace("strategy-", "")]; - for (const group of contentGroupOptions) { - const $group = element("div", "team-group-section"); - $content.appendChild($group); - const groupStyle = $group.style; - groupStyle["height"] = `${averageHeight}%`; - - // 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 (showGroupTitle) { - const $groupTitle = element("div", "team-group-title"); - const groupTitleStyle = $groupTitle.style; - groupTitleStyle["backgroundColor"] = groupColor; - $groupTitle.appendChild(document.createTextNode(groupText)); - $group.appendChild($groupTitle); + 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 newGroupDataPK = $group.dataset.pk; + const newNodeDataPK = JSON.parse( + $group.parentElement.parentElement.dataset.source + )._rawData[nodeObjPK]; + let { + source: updatedData, + pk: dataPK, + contentLinkedFieldID, + } = JSON.parse(dataTransfer.getData("text/plain")); + const draggedNodes = []; + try { + if (!updatedData) { + // This is a drop from Employee list (new assignment) + const contentLinkedFieldColumnName = + contentObj.fieldByID(contentLinkedFieldID).columnName; + const pendingPromises = []; + const newDate = new Date(); + const $contentRecords = + document.getElementsByClassName("team-group-record"); + for (const $contentRecord of $contentRecords) { + const contentData = JSON.parse($contentRecord.dataset.source); + if (contentData[contentLinkedFieldColumnName] == dataPK) { + contentData[contentDateEndFieldColumnName] = newDate; + pendingPromises.push( + contentModel.update(contentData.id, contentData) + ); + draggedNodes.push($contentRecord); + } } - const $groupContent = element("div", "team-group-content"); - $group.appendChild($groupContent); - if (draggable) { - $group.addEventListener("dragover", this.fnContentDragOver); - $group.addEventListener("drop", (e) => this.fnContentDrop(e)); + 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() + ); } - let contentDataRecordIndex = 0; - while ( - contentDataRecordIndex < contentDataRecords.length && - contentDataRecords.length > 0 - ) { - const contentDataRecord = - contentDataRecords[contentDataRecordIndex]; - if ( - contentDataRecord[contentFieldLinkColumnName] != - currentNodeDataRecordPK || - contentDataRecord[contentGroupByFieldColumnName] != - group.id - ) { - contentDataRecordIndex++; - continue; - } - contentDataRecords.splice(contentDataRecordIndex, 1); - for (const key in contentDataRecord) - key.includes("__relation") && - delete contentDataRecord[key]; - const $rowData = await this.contentRecordUI( - contentDataRecord, - strategyColor + pendingPromises.push( + contentModel.create(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); + + // 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"]; + if (dropContentToCreate) { + const pendingPromises = []; + + // TODO (Guy): Force update Date End with a current date. + updatedData[contentDateEndFieldColumnName] = new Date(); + pendingPromises.push( + contentModel.update(updatedData.id, updatedData) ); - $groupContent.appendChild($rowData); + updatedData[contentDateStartFieldColumnName] = + updatedData[contentDateEndFieldColumnName]; + delete updatedData["id"]; + delete updatedData["uuid"]; + delete updatedData[contentDateEndFieldColumnName]; + updatedData[contentFieldLinkColumnName] = newNodeDataPK; + updatedData[contentGroupByFieldColumnName] = newGroupDataPK; + pendingPromises.push(contentModel.create(updatedData)); + await Promise.all(pendingPromises); + } else { + updatedData[contentFieldLinkColumnName] = newNodeDataPK; + updatedData[contentGroupByFieldColumnName] = newGroupDataPK; + await contentModel.update(updatedData.id, updatedData); } } - 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); - 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); + } catch (err) { + // TODO (Guy): The update data error. + console.log(err); + } + try { + 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); + } + this.ready(); + await this.refresh(); + draggedNodes.forEach(($draggedNode) => { + $draggedNode.remove(); + }); + }; + 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 averageHeight = 80 / contentGroupOptionsLength; + 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["height"] = `${averageHeight}%`; + + // 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); } - if (this.__filters.inactive && 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); + const $groupContent = element("div", "team-group-content"); + $group.appendChild($groupContent); + if (settings.draggable === 1) { + $group.addEventListener("dragover", this._fnContentDragOver); + $group.addEventListener("drop", this._fnContentDrop); } - callAfterRender(() => { - const groupHeightThreshold = (246.5 * averageHeight) / 100; - const groupSections = $node.querySelectorAll( - ".team-group-section" - ); - let isOverflow = false; - for (const $groupSection of groupSections) - if ( - $groupSection.getBoundingClientRect().height > - groupHeightThreshold - ) { - isOverflow = true; - break; - } - if (!isOverflow) { - $node.style.height = "300px"; - return; - } - groupSections.forEach(($groupSection) => { - const groupHeight = - $groupSection.getBoundingClientRect().height; - const groupStyle = $groupSection.style; - groupStyle.height = - (groupHeight < groupHeightThreshold && - `${groupHeightThreshold}px`) || - `${groupHeight}px`; - }); - const nodeChildren = $node.children; - nodeChildren.item(0).style.height = "43.5px"; - const $content = nodeChildren.item(1); - $content.style.top = "-22.5px"; - $content.children.item(0).style.height = "24.65px"; - }); - }, - nodeContent: "description", - }); - this.AB.performance.measure("createOrgChart"); - this.__orgchart = orgchart; - if (draggable) { - // On drop update the parent (dropZone) of the node - orgchart.addEventListener("nodedropped.orgchart", 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; - await nodeModel.update(dragedRecord.id, dragedRecord); - }); - } - const chartDom = document.querySelector(`#${ids.chartDom}`); - if (chartDom) { - chartDom.textContent = ""; - chartDom.innerHTML = ""; - const ui = { - hidden: true, - cols: [ - { - rows: [ - { - view: "template", - height: 50, - }, - { - view: "template", - scroll: "auto", - }, - ], - }, - ], + } + 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 }); }; - if (settings.showDataPanel === 1) { - const dataPanelDCs = settings.dataPanelDCs; - const cells = []; - for (const key in dataPanelDCs) { - const dc = AB.datacollectionByID(key.split(".")[1]); - const $dc = dc.$dc; - const panelObj = dc.datasource; - const contentFieldID = panelObj.connectFields( - (field) => field.datasourceLink.id == contentObjID - )[0].fieldLink.id; - const DC_OFFSET = 20; - cells.push({ - header: dataPanelDCs[key], - body: { - view: "list", - template: (data) => - `
${panelObj.displayData( - data - )}
`, - css: { overflow: "auto", "max-height": "85%" }, - data: [], - on: { - onViewShow(prevCount = 0) { - (async () => { - await dc.waitReady(); - this.define( - "data", - // TODO (Guy): Force to sort by firstName. the DC sort setting work but after calling DC.parse() in DC.queuedParse() method the sort is messy. - dc.getData().sort((a, b) => { - if (a.firstName < b.firstName) { - return -1; - } - if (a.firstName > b.firstName) { - return 1; - } - return 0; - }) - ); - this.refresh(); - callAfterRender(() => { - const $itemElements = - this.$view.children.item(0).children; - const itemElementsLength = - $itemElements.length; - let count = 0; - while (count < itemElementsLength) { - const $itemElement = $itemElements.item( - count++ - ); - $itemElement.setAttribute( - "data-content-linked-field-id", - contentFieldID - ); - $itemElement.setAttribute( - "data-pk", - dc.getData( - (e) => - e.id == - $itemElement.getAttribute( - "webix_l_id" - ) - )[0][panelObj.PK()] - ); - $itemElement.setAttribute( - "draggable", - "true" - ); - $itemElement.addEventListener( - "dragstart", - (e) => self.fnContentDragStart(e) - ); - $itemElement.addEventListener( - "dragend", - (e) => self.fnContentDragEnd(e) - ); - } - if (count < DC_OFFSET || count === prevCount) - return; - (async () => { - const RECORD_LIMIT = 20; - await dc.loadData( - RECORD_LIMIT * - parseInt(count / RECORD_LIMIT), - RECORD_LIMIT - ); - this.callEvent("onViewShow", [count]); - })(); - }); - })(); - }, - }, - }, - }); - } - ui.cols.push({ - view: "tabview", - width: 450, - tabbar: { - height: 60, - type: "bottom", - css: "webix_dark", - }, - cells, - }); + $editButton.onclick = () => 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); } - const $orgChartTeams = AB.Webix.ui(ui); - $orgChartTeams.show(); - const $chartContent = $orgChartTeams.$view; - chartDom.appendChild($chartContent); - const $chartContentLayout = $chartContent.children[0]; - this.initFilter($chartContentLayout.children[0].children[0]); - $chartContentLayout.children[1].children[0].appendChild(orgchart); - } - } - - /** - * load the data and format it for display - */ - async pullData() { - const filters = this.__filters; - const settings = this.view.settings; - const dc = this.view.datacollection; - await dc?.waitReady(dc); - let topNode = dc?.getCursor(); - if (settings.topTeam) { - const topNodeColumn = this.getSettingField("topTeam").columnName; - const topFromFeild = dc.getData((e) => e[topNodeColumn] === 1)[0]; - topNode = topFromFeild ? topFromFeild : topNode; - } - if (!topNode) return null; - const teamLink = this.getSettingField("teamLink").columnName; - const teamName = this.getSettingField("teamName").columnName; - const teamInactive = this.getSettingField("teamInactive").columnName; - const strategyField = this.getSettingField("teamStrategy").columnName; - const strategyCode = this.getSettingField("strategyCode").columnName; - const self = this; - const contentField = dc?.datasource.fieldByID(settings.contentField); - const contentFieldColumnName = contentField?.columnName; - const contentObj = contentField?.fieldLink?.object; - const contentObjPK = contentObj.PK(); - const contentFieldFilter = JSON.parse(settings.contentFieldFilter); - const parsedContentFilterRules = [ - // TODO (Guy): Hardcode date start filter. - { - key: contentObj?.fieldByID(settings.contentFieldDateStart)?.id, - rule: "is_not_null", - value: "", - }, - { - glue: "or", - rules: - (contentFieldFilter.rules?.length > 0 && [ - contentFieldFilter, - - // TODO (Guy): Hardcode date end filter. - { - key: contentObj?.fieldByID(settings.contentFieldDateEnd) - ?.id, - rule: "is_null", - value: "", - }, - ]) || - [], - }, - ]; - const contentDisplayedFields = settings.contentDisplayedFields; - const contentDisplayFieldKeys = Object.keys(contentDisplayedFields); - const contentDisplayedFieldFilters = - settings.contentDisplayedFieldFilters; - const contentDataRecords = (this._cachedContentDataRecords = []); - let isContentFiltered = false; - - // TODO (Guy): Now, this approch is having so many requests. - this.AB.performance.mark("loadFilteredContent"); - for (const key in contentDisplayedFieldFilters) { - const [filterAtDisplay, filterObjID, filterFieldID, isFiltered] = - key.split("."); - const filterValue = filters[key]; - const contentAtDisplayFieldKeys = [contentDisplayFieldKeys.shift()]; - let currentAtDisplayFieldKey = contentAtDisplayFieldKeys[0]; - while ( - filterAtDisplay === currentAtDisplayFieldKey[0] && - contentDisplayedFields[currentAtDisplayFieldKey] !== - filterFieldID && - contentDisplayFieldKeys.length > 0 - ) { - currentAtDisplayFieldKey = contentDisplayFieldKeys.shift(); - contentAtDisplayFieldKeys.push(currentAtDisplayFieldKey); + 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); } - if ( - isFiltered != 1 || - typeof filterValue !== "string" || - filterValue === "" || - contentAtDisplayFieldKeys.length === 0 - ) - continue; - contentAtDisplayFieldKeys.pop(); - const contentAtDisplayFieldKeysLength = - contentAtDisplayFieldKeys.length; - let filterObj = AB.objectByID(filterObjID); - const getContentDataRecords = async (obj, filterRule) => { - switch (obj.fieldByID(filterRule.key)?.key) { - case "connectObject": - case "user": - break; - default: - const dataRecords = ( - await obj.model().findAll({ - where: { - glue: "and", - rules: [filterRule, ...parsedContentFilterRules], - }, - populate: true, - }) - ).data; - for (const dataRecord of dataRecords) { - if ( - contentDataRecords.findIndex( - (contentDataRecord) => - contentDataRecord.id == dataRecord.id - ) > -1 - ) - continue; - contentDataRecords.push(dataRecord); - } - isContentFiltered = true; - break; + }; + 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(); + try { + await dc.model.update(dragedRecord.id, dragedRecord); + } catch (err) { + // TODO (Guy): The update data error. + console.error(err); + } + try { + await Promise.all([ + this._reloadDCData(dc), + this._reloadDCData(this._contentDC), + ]); + // await this._reloadAllDC(); + } catch (err) { + // TODO (Guy): The reload DCs error. + console.error(err); + } + this.ready(); + await this.refresh(); + }; + this._fnPageData = async (dc, callback) => { + this._initDC(dc); + await this._waitDCReady(dc); + let records = dc.getData(); + try { + if (records.length < DC_OFFSET) { + await dc.loadData(); + + // TODO (Guy): There was some delays before patching DC imcoming data. + await new Promise((resolve) => { + setTimeout(resolve, 500); + }); + records = dc.getData(); } - }; - if (contentAtDisplayFieldKeysLength === 0) { - await getContentDataRecords(filterObj, { - key: filterFieldID, - rule: "contains", - value: filterValue, + if (records.length < DC_OFFSET) throw null; + if ((records.length - DC_OFFSET) % RECORD_LIMIT > 0) throw null; + await dc.loadData( + RECORD_LIMIT * parseInt(records.length / RECORD_LIMIT), + RECORD_LIMIT + ); + + // TODO (Guy): There was some delays before patching DC imcoming data. + await new Promise((resolve) => { + setTimeout(resolve, 500); }); - continue; + 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)); + } catch { + callback && (await callback(records, true, dc)); } - const prevDisplayFieldKey = - contentAtDisplayFieldKeys[contentAtDisplayFieldKeysLength - 1]; - const prevContentDisplayLinkedField = - (prevDisplayFieldKey !== "" && - AB.objectByID(prevDisplayFieldKey.split(".")[1]).fieldByID( - contentDisplayedFields[prevDisplayFieldKey] - )?.fieldLink) || - null; - let filteredRecordDataPKs = ( - await filterObj.model().findAll({ - where: { - glue: "and", - rules: [ - { - key: filterFieldID, - rule: "contains", - value: filterValue, - }, - ], - }, - populate: true, - }) - ).data.flatMap( - (record) => record[prevContentDisplayLinkedField.columnName] + }; + this._fnRefresh = async () => { + await this.refresh(); + }; + this._fnResizeTeamNode = async ($teamNode) => { + const contentGroupDC = this._contentGroupDC; + await this._waitDCReady(contentGroupDC); + const groupHeightThreshold = + (246.5 * contentGroupDC.getData().length) / 100; + const groupSections = $teamNode.querySelectorAll( + ".team-group-section" ); - while ( - contentAtDisplayFieldKeys.length > 1 && - filteredRecordDataPKs.length > 0 - ) { - filterObj = AB.objectByID( - contentAtDisplayFieldKeys.pop().split(".")[1] - ); - const filterObjPK = filterObj.PK(); - filteredRecordDataPKs = ( - await filterObj.model().findAll({ - where: { - glue: "and", - rules: [ - { - key: filterObjPK, - rule: "in", - value: filteredRecordDataPKs, - }, - ], - }, - populate: true, - }) - ).data.map((record) => record[filterObjPK]); + let isOverflow = false; + for (const $groupSection of groupSections) + if ( + $groupSection.getBoundingClientRect().height > + groupHeightThreshold + ) { + isOverflow = true; + break; + } + if (!isOverflow) { + $teamNode.style.height = "300px"; + return; } - if (filteredRecordDataPKs.length === 0) continue; - filterObj = AB.objectByID( - contentAtDisplayFieldKeys.pop().split(".")[1] - ); - await getContentDataRecords(filterObj, { - key: filterObj.PK(), - rule: "in", - value: filteredRecordDataPKs, + groupSections.forEach(($groupSection) => { + const groupHeight = $groupSection.getBoundingClientRect().height; + const groupStyle = $groupSection.style; + groupStyle.height = + (groupHeight < groupHeightThreshold && + `${groupHeightThreshold}px`) || + `${groupHeight}px`; }); - isContentFiltered = true; - } - this.AB.performance.measure("loadFilteredContent"); - const contentDataRecordPKs = []; - const chartData = (this._chartData = {}); - chartData.name = topNode[teamName] ?? ""; - chartData.id = this.teamNodeID(topNode.id); - const topNodeStrategy = topNode[`${strategyField}__relation`]; - const topNodeCode = topNodeStrategy?.[strategyCode]; - chartData.className = `strategy-${topNodeCode}`; - chartData.isInactive = topNode[teamInactive]; - chartData._rawData = topNode; - const topNodeContentFieldData = topNode[contentFieldColumnName]; - if (!isContentFiltered) { - if (Array.isArray(topNodeContentFieldData)) - contentDataRecordPKs.push(...topNodeContentFieldData); - else contentDataRecordPKs.push(topNodeContentFieldData); - } - chartData.filteredOut = isContentFiltered - ? self.filterTeam( - filters, - chartData, - topNodeCode, - topNodeContentFieldData - ) - : self.filterTeam(filters, chartData, topNodeCode); - const maxDepth = 10; // prevent inifinite loop - /** - * Recursive function to prepare child node data - * @param {object} node the current node - * @param {number} [depth=0] a count of how many times we have recursed - */ - function pullChildData(node, depth = 0) { - if (depth >= maxDepth) return; - node.children = []; - node._rawData[teamLink].forEach((id) => { - const childData = dc.getData((e) => e.id == id)[0]; - // Don't show inactive teams - if ( - !childData || - (filters?.inactive !== 1 && childData[teamInactive]) - ) - return; - const strategy = childData[`${strategyField}__relation`]; - const code = strategy?.[strategyCode]; - const child = { - name: childData[teamName], - id: self.teamNodeID(id), - className: `strategy-${code}`, - isInactive: childData[teamInactive], - _rawData: childData, - }; - const childContentFieldData = childData[contentFieldColumnName]; - if (!isContentFiltered) { - if (Array.isArray(childContentFieldData)) - contentDataRecordPKs.push(...childContentFieldData); - else contentDataRecordPKs.push(childContentFieldData); - } - - child.filteredOut = isContentFiltered - ? self.filterTeam(filters, child, code, childContentFieldData) - : self.filterTeam(filters, child, code); - if (child.name === "External Support") - child.className = `strategy-external`; - if (childData[teamLink].length > 0) { - pullChildData(child, depth + 1); - } - // If this node is filtered we still need it if it has children - // that pass - if (!child.filteredOut || child.children?.length > 0) { - node.children.push(child); + const nodeChildren = $teamNode.children; + nodeChildren.item(0).style.height = "43.5px"; + const $content = nodeChildren.item(1); + $content.style.top = "-22.5px"; + $content.children.item(0).style.height = "24.65px"; + }; + this._fnShowContentForm = (event) => { + const contentDC = this._contentDC; + const contentObj = contentDC.datasource; + const contentModel = contentDC.model; + const settings = this.settings; + const editContentFieldsToCreateNew = + settings.editContentFieldsToCreateNew; + const contentDateStartFieldColumnName = this.getSettingField( + "contentFieldDateStart" + )?.columnName; + const contentDateEndFieldColumnName = this.getSettingField( + "contentFieldDateEnd" + )?.columnName; + const contentDataRecord = JSON.parse( + event.currentTarget.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. + rules[fieldName] = () => true; + const fieldLabel = field.label; + const settings = field.settings; + switch (fieldKey) { + case "boolean": + return { + view: "checkbox", + name: fieldName, + label: fieldLabel, + labelWidth, + }; + case "number": + return { + view: "counter", + name: fieldName, + label: fieldLabel, + labelWidth, + type: "number", + }; + 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, + })), + }; + 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, + 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 { + // TODO (Guy): Add spinner. + this.define( + "options", + (await fieldLinkObj.model().findAll()).data.map( + (e) => ({ + id: e.id, + value: fieldLinkObj.displayData(e), + }) + ) + ); + this.hideProgress(); + this.enable(); + await this.refresh(); + } catch { + // Close popup before response or possily response fail + } + }; + return field.linkType() === "one" + ? { + view: "combo", + name: fieldName, + label: fieldLabel, + disabled: true, + labelWidth, + options: [], + on: { + onViewShow, + }, + } + : { + view: "multicombo", + name: fieldName, + label: fieldLabel, + labelWidth, + stringResult: false, + labelAlign: "left", + options: [], + on: { + onViewShow, + }, + }; + case "date": + case "datetime": + return { + view: "datepicker", + name: fieldName, + label: fieldLabel, + labelWidth, + timepicker: fieldKey === "datetime", + }; + case "file": + case "image": + // TODO (Guy): Add logic + return { + // view: "", + name: fieldName, + label: fieldLabel, + labelWidth, + }; + // case "json": + // case "LongText": + // case "string": + // case "email": + default: + return { + view: "text", + name: fieldName, + label: fieldLabel, + labelWidth, + }; + } } - }); - if (node.children.length === 0) { - delete node.children; - } else { - // sort children alphaetically - node.children = node.children.sort((a, b) => - a.name > b.name ? 1 : -1 - ); - } - return; - } - pullChildData(chartData); - if (contentField == null) return; - if ( - Object.keys(chartData).length > 0 && - contentField != null && - contentObj != null && - !isContentFiltered - ) { - this.AB.performance.mark("loadAssignments"); - contentDataRecords.push( - ...( - await contentObj.model().findAll({ - where: { - glue: "and", - rules: [ - { - key: contentObjPK, - rule: "in", - value: contentDataRecordPKs, - }, - ...parsedContentFilterRules, - ], - }, - populate: true, - }) - ).data ); - this.AB.performance.measure("loadAssignments"); - } - } - - /** - * creates a datacollection to hold the content data linked to the current - * enitity; - */ - loadContentData() { - const contentObj = this.contentObject(); - const entityDC = this.entityDC; - const connectField = contentObj.connectFields( - (f) => f.settings.linkObject === entityDC.datasource.id - )[0]; - const contentDC = this.AB.datacollectionNew({ - datasourceID: contentObj.id, - loadAll: true, - linkDatacollectionID: this.settings.entityDatacollection, - linkFieldID: connectField.id, - }); - this.contentDC = contentDC; - return contentDC.init(); - } - - contentObject() { - return this.AB.objectByID( - this.getSettingField("contentField").settings.linkObject - ); - } - - async refresh() { - $$(this.ids.teamFormPopup)?.destructor(); - this.loadContentDisplayData(); - let orgchart = this.__orgchart; - if (orgchart != null) { - const dataPanStart = orgchart.dataset.panStart; - const style = orgchart.getAttribute("style"); - await this.pullData(); - await this.displayOrgChart(); - orgchart = this.__orgchart; - orgchart.dataset.panStart = dataPanStart; - orgchart.setAttribute("style", style); - } else { - await this.pullData(); - await this.displayOrgChart(); - } - } - - get chartData() { - if (this._chartData == null) { - this._chartData = {}; - } - return this._chartData; - } - - /** Add the filter button to the UI */ - initFilter(domNode) { - const filterButton = document.createElement("button"); - filterButton.innerHTML = ` Filter`; - filterButton.classList.add("filter-button"); - filterButton.onclick = () => this.filterWindow(filterButton); - domNode.append(filterButton); - } - - /** Display the filter UI (popup) **/ - async filterWindow(buttonNode) { - const contentDisplayedFieldFilters = - this.settings.contentDisplayedFieldFilters; - let $popup = $$(this.ids.filterPopup); - if (!$popup) { - const strategyID = - this.getSettingField("teamStrategy").settings.linkObject; - const strategyObj = this.AB.objectByID(strategyID); - const strategyCodeFieldID = this.getSettingField("strategyCode").id; - const strategyCodeField = strategyObj.fields( - (f) => f.id === strategyCodeFieldID - )[0]; - const strategyOptions = await strategyCodeField.getOptions(); - - $popup = webix.ui({ - view: "popup", - css: "filter-popup", - id: this.ids.filterPopup, - body: { - view: "form", - borderless: true, - id: this.ids.filterForm, - elements: [ - { - view: "text", - label: this.label("Team Name"), - labelWidth: 90, - name: "teamName", - clear: true, - }, - { - view: "combo", - label: this.label("Strategy"), - labelWidth: 90, - options: strategyOptions.map(fieldToOption), - name: "strategy", - clear: "replace", - }, - { - view: "checkbox", - name: "inactive", - labelRight: this.label("Show Inactive Teams"), - labelWidth: 0, - }, - ...(() => { - const contentDisplayedFieldFilterViews = []; - for (const contentDisplayedFieldFilterKey in contentDisplayedFieldFilters) { - if (contentDisplayedFieldFilterKey.split(".")[3] == 1) { - contentDisplayedFieldFilterViews.push({ - view: "text", - label: contentDisplayedFieldFilters[ - contentDisplayedFieldFilterKey - ], - labelWidth: 90, - name: contentDisplayedFieldFilterKey, - clear: true, - }); + contentFormElements.push({ + view: "button", + value: this.label("Save"), + css: "webix_primary", + click: async () => { + const $contentFormData = $$(ids.contentFormData); + if (!$contentFormData.validate()) return; + let isDataChanged = false; + const newFormData = this._parseFormValueByType( + contentDataRecord, + $contentFormData.getValues() + ); + for (const key in newFormData) + if ( + JSON.stringify(newFormData[key]) !== + JSON.stringify(contentDataRecord[key]) + ) { + isDataChanged = true; + break; + } + const $contentForm = $$(ids.contentForm); + $contentForm.blockEvent(); + $contentForm.$view.remove(); + $contentForm.destructor(); + if (!isDataChanged) return; + webix + .confirm({ + title: "Warning", + ok: "Yes", + cancel: "No", + text: "You are about to confirm. Are you sure?", + }) + .then(async () => { + this.busy(); + 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"]; + for (const editContentFieldToCreateNew of editContentFieldsToCreateNew) { + const editContentFieldToCreateNewColumnName = + contentObj.fieldByID( + editContentFieldToCreateNew + )?.columnName; + if ( + JSON.stringify( + newFormData[ + editContentFieldToCreateNewColumnName + ] ?? "" + ) !== + JSON.stringify( + contentDataRecord[ + editContentFieldToCreateNewColumnName + ] ?? "" + ) + ) { + const pendingPromises = []; + const oldData = {}; + + oldData[contentDateEndFieldColumnName] = new Date(); + 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 { + await Promise.all([ + this._reloadDCData(teamDC), + this._reloadDCData(contentDC), + ]); + // await this._reloadAllDC(); + } catch (err) { + // TODO (Guy): The reload DCs error. + console.error(err); + } + this.ready(); + await this.refresh(); + $contentNode.remove(); + return; } } - return contentDisplayedFieldFilterViews; - })(), + try { + await contentModel.update(dataID, newFormData); + } catch (err) { + // TODO (Guy): The update data error. + console.error(err); + } + try { + await Promise.all([ + this._reloadDCData(teamDC), + this._reloadDCData(contentDC), + ]); + // await this._reloadAllDC(); + } catch (err) { + // TODO (Guy): The reload DCs error. + console.error(err); + } + this.ready(); + await this.refresh(); + $contentNode.remove(); + }); + }, + }); + AB.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-check", - css: "filter-apply", - click: () => this.filterApply(), + 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, + }, ], }, - }); - } - $popup.show(buttonNode); - } - - async filterApply() { - $$(this.ids.filterPopup).hide(); - this.__filters = $$(this.ids.filterForm).getValues(); - await this.refresh(); - } + on: { + onHide() { + this.$view.remove(); + this.destructor(); + }, + }, + }).show(); + const $contentFormData = $$(ids.contentFormData); + $contentFormData.setValues(contentDataRecord); + $contentFormData.show(); + }; + this._fnShowFilterPopup = async (event) => { + const contentDisplayedFieldFilters = + this.settings.contentDisplayedFieldFilters; + let $popup = $$(this.ids.filterPopup); + if (!$popup) { + const strategyID = + this.getSettingField("teamStrategy").settings.linkObject; + const strategyObj = this.AB.objectByID(strategyID); + const strategyCodeFieldID = this.getSettingField("strategyCode").id; + const strategyCodeField = strategyObj.fields( + (f) => f.id === strategyCodeFieldID + )[0]; + const strategyOptions = await strategyCodeField.getOptions(); - filterTeam(filters, team, code, contentFieldData) { - // Apply filters (match using or) - if (filters.strategy || filters.teamName || contentFieldData != null) { - let filter = true; - if (filters.strategy !== "" && filters.strategy == code) { - filter = false; - } - if ( - filters.teamName !== "" && - team.name.toLowerCase().includes(filters.teamName.toLowerCase()) - ) { - filter = false; - } - const contentObjPK = this.view.datacollection?.datasource - .fieldByID(this.settings.contentField) - ?.fieldLink?.object.PK(); - if ( - this._cachedContentDataRecords.findIndex((contentDataRecord) => { - if (Array.isArray(contentFieldData)) - return ( - contentFieldData.indexOf(contentDataRecord[contentObjPK]) > - -1 - ); - return contentFieldData == contentDataRecord[contentObjPK]; - }) > -1 - ) { - filter = false; + $popup = webix.ui({ + view: "popup", + css: "filter-popup", + id: this.ids.filterPopup, + body: { + view: "form", + borderless: true, + id: this.ids.filterForm, + elements: [ + { + view: "text", + label: this.label("Team Name"), + labelWidth: 90, + name: "teamName", + clear: true, + }, + { + view: "combo", + label: this.label("Strategy"), + labelWidth: 90, + options: strategyOptions.map(fieldToOption), + name: "strategy", + clear: "replace", + }, + { + view: "checkbox", + name: "inactive", + labelRight: this.label("Show Inactive Teams"), + labelWidth: 0, + }, + ...(() => { + const contentDisplayedFieldFilterViews = []; + for (const contentDisplayedFieldFilterKey in contentDisplayedFieldFilters) { + if ( + contentDisplayedFieldFilterKey.split(".")[3] == 1 + ) { + contentDisplayedFieldFilterViews.push({ + view: "text", + label: contentDisplayedFieldFilters[ + contentDisplayedFieldFilterKey + ], + labelWidth: 90, + name: contentDisplayedFieldFilterKey, + clear: true, + }); + } + } + return contentDisplayedFieldFilterViews; + })(), + { + cols: [ + {}, + { + view: "icon", + icon: "fa fa-check", + css: "filter-apply", + click: () => this.filterApply(), + }, + ], + }, + ], + }, + }); } - return filter; - } - } - - /** - * Get the ABField from settings - * @param {string} setting key in this.view.settings - should be an id for an - * ABField - */ - getSettingField(setting) { - return this.AB.definitionByID(this.view.settings[setting]); - } + $popup.show(event.currentTarget); + }; - generateStrategyCss() { + // Generate strategy css const css = [ "org-chart .strategy-external .title{background:#989898 !important;}", ]; @@ -1050,1018 +869,1304 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { const style = document.createElement("style"); style.innerHTML = css.join(""); document.getElementsByTagName("head")[0].appendChild(style); + this.on("pageData", this._fnPageData); } - async teamAddChild(values, strategy) { - const entityDC = this.entityDC; + _addContentRecordToGroup($teamNode, contentRecord) { + const contentNodeID = this.contentNodeID(contentRecord.id); + let $contentNode = document.getElementById(contentNodeID); + while ($contentNode != null) { + $contentNode.remove(); + $contentNode = document.getElementById(contentNodeID); + } + (async () => { + $teamNode + .querySelector( + `.team-group-section[data-pk="${ + contentRecord[ + this.getSettingField("contentGroupByField").columnName + ] + }"] > .team-group-content` + ) + ?.appendChild( + await this._createUIContentRecord( + contentRecord, + this.settings.strategyColors[ + $teamNode.classList.item(1).replace("strategy-", "") + ] + ) + ); + })(); + // await this._callAfterRender(this._fnResizeTeamNode, $teamNode); + } - // Add the entity value - if (entityDC) { - const connection = entityDC.datasource.connectFields( - (f) => f.settings.linkObject === datacollection.datasource.id - )[0]; - if (connection) { - const entity = entityDC.getCursor(); - const cName = this.AB.definitionByID( - connection.settings.linkColumn - ).columnName; - values[cName] = entity; - } + 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("click", this._fnShowContentForm); + if (this.settings.draggable === 1) { + $ui.setAttribute("draggable", "true"); + $ui.addEventListener("dragstart", this._fnContentDragStart); + $ui.addEventListener("dragend", this._fnContentDragEnd); } - const _rawData = await this.datacollection.model.create(values); - const id = _rawData.id; - const linkField = this.AB.definitionByID( - this.getSettingField("teamLink").settings.linkColumn - ).columnName; - const nameField = this.getSettingField("teamName").columnName; - const parent = document.querySelector( - `#${this.teamNodeID(values[linkField])}` + // 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"), + ]; + 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 strategyLink = this.getSettingField("teamStrategy").columnName; - const strategyField = this.getSettingField("strategyCode").columnName; - const strategyCode = _rawData[`${strategyLink}__relation`][strategyField]; - - const hasChild = parent.parentNode.colSpan > 1; - const newChild = { - name: values[nameField], - id: this.teamNodeID(id), - relationship: hasChild ? "110" : "100", - className: `strategy-${strategyCode}`, - _rawData, - }; - - // Need to add differently if the node already has child nodes - if (hasChild) { - const sibling = this.closest(parent, (el) => el.nodeName === "TABLE") - .querySelector(".nodes") - .querySelector(".node"); - this.__orgchart.addSiblings(sibling, { siblings: [newChild] }); - } else { - this.__orgchart.addChildren(parent, { children: [newChild] }); - } - } - - teamCanInactivate(values) { - const isInactive = this.getSettingField("teamInactive").columnName; - if (values[isInactive]) return true; // Allow activating inactive teams - const canInactive = this.getSettingField("teamCanInactivate").columnName; - if (!values[canInactive]) return false; - const children = this.getSettingField("teamLink").columnName; - if ( - values[children].some( - (c) => - this.datacollection.getData((r) => r.id == c)[0]?.[isInactive] == - false + // TODO (Guy): Now we are hardcoding for each display. + // $rowData.appendChild($currentDisplay); + switch (currentDisplayIndex) { + case 0: + hardcodedDisplays[0].appendChild($currentDisplay); + break; + case 1: + 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 displayedFieldColumnName = displayedField.columnName; + const contentDisplayedFieldTypePrefix = `${displayedFieldKey}.${displayedFieldID}`; + const contentDisplayedFieldMappingDataObj = + JSON.parse( + this.settings.contentDisplayedFieldMappingData?.[ + contentDisplayedFieldTypePrefix + ] || null + ) || {}; + if ( + this.settings.contentDisplayedFieldTypes[ + `${contentDisplayedFieldTypePrefix}.0` + ] != null ) - ) - return false; - // @TODO check for active assignment - // if (hasActiveAssignment) return false; - return true; - } + $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 + ) + ); + } + break; + } + currentField = null; + } - teamCanDelete(values) { - const canInactive = this.getSettingField("teamCanInactivate").columnName; - if (!values[canInactive]) return false; - const children = this.getSettingField("teamLink").columnName; - if (values[children].length > 0) return false; - // @TODO check for any assignment - // if (hasAssignment) return false; - return true; + // 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"); + } + return $ui; } - teamDelete(values) { - if (!this.teamCanDelete(values)) { - this.AB.Webix.message({ - text: "This team cannot be deleted", - type: "error", - expire: 1001, + async _callAfterRender(callback, ...params) { + await new Promise((resolve, reject) => { + requestAnimationFrame(() => { + requestAnimationFrame(async () => { + try { + await callback(...params); + resolve(); + } catch (err) { + reject(err); + } + }); }); - return; - } - return this.AB.Webix.confirm({ - text: "This can't be undone, are you sure?", - }).then(() => { - this.datacollection.model.delete(values.id); - const nodeID = this.teamNodeID(values.id); - this.__orgchart.removeNodes(document.querySelector(`#${nodeID}`)); }); } - async teamEdit(values, strategy) { - const strategyLink = this.getSettingField("teamStrategy").columnName; - const strategyField = this.getSettingField("strategyCode").columnName; - const strategyCode = strategy[strategyField]; - values[strategyLink] = strategy.id; - delete values[`${strategyLink}__relation`]; - await this.datacollection.model.update(values.id, values).catch((err) => { - //TODO - }); - const nodeID = this.teamNodeID(values.id); - const node = document.querySelector(`#${nodeID}`); - const currentStrategy = node.classList?.value?.match(/strategy-\S+/)[0]; - const newStrategy = `strategy-${strategyCode}`; - if (currentStrategy !== newStrategy) { - node.classList?.remove(currentStrategy); - node.classList?.add(newStrategy); + _initDC(dc) { + if (dc.dataStatus === dc.dataStatusFlag.notInitial) { + dc.init(); + dc.loadData(); } - - const inactive = this.getSettingField("teamInactive").columnName; - // Remove inactive node from display, unless the filter setting to show - // inctive nodes is on. - if (this.__filters?.inactive !== 1 && values[inactive] === 1) { - this.__orgchart.removeNodes(node); - } - const nameCol = this.getSettingField("teamName").columnName; - node.querySelector(".title").innerHTML = values[nameCol]; } - async teamForm(mode, values) { - let $teamFormPopup = $$(this.ids.teamFormPopup); - const inactive = this.getSettingField("teamInactive").columnName; - const linkField = this.AB.definitionByID( - this.getSettingField("teamLink").settings.linkColumn - ).columnName; + _pageData() { const ids = this.ids; - if (!$teamFormPopup) { - const teamObj = this.datacollection.datasource; - const settings = this.settings; - const nameField = teamObj.fieldByID(settings.teamName); - const strategyField = teamObj.fieldByID(settings.teamStrategy); - const strategyObj = this.AB.objectByID( - strategyField.settings.linkObject - ); - const entityDC = this.entityDC; - const entityLink = strategyObj.connectFields( - (f) => f.settings.linkObject === entityDC.datasource.id - )[0]; - const cond = { - glue: "and", - rules: [ - { - key: entityLink.columnName, - value: entityDC.getCursor().id, - rule: "equals", - }, - ], - }; - const subCol = this.getSettingField("subStrategy").columnName; - this.entitySrategyOptions = await strategyField.getOptions( - cond, - null, - null, - null, - [subCol] - ); + const settings = this.settings; + const teamDC = this.datacollection; + const teamObj = teamDC.datasource; + const dataPanelDCs = this._dataPanelDCs; + const contentField = teamObj.fieldByID(settings.contentField); + const contentFieldColumnName = contentField.columnName; + const linkedContentFieldColumnName = contentField.fieldLink.columnName; + const contentDC = this._contentDC; + const contentRecordPK = contentDC.datasource.PK(); + const contentDisplayDCs = this._contentDisplayDCs; + const teamCallback = async (teamRecords, isTeamDone) => { + 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; + } + const contentRecords = contentDC.getData( + (contentRecord) => + teamRecord[contentFieldColumnName].indexOf( + contentRecord[contentRecordPK] + ) > -1 + ); + for (const contentRecord of contentRecords) + this._addContentRecordToGroup($teamNode, contentRecord); + } + !isTeamDone && this.emit(teamDC, teamCallback); + }; + const contentCallback = (contentRecords, isContentDone) => { + for (const contentRecord of contentRecords) { + const $teamNode = document.getElementById( + this.teamNodeID(contentRecord[linkedContentFieldColumnName]) + ); + if ($teamNode == null) continue; + this._addContentRecordToGroup($teamNode, contentRecord); + } - const strategyOptions = this.entitySrategyOptions.map((o) => { - return { - id: o.id, - value: o[`${subCol}__relation`].name, - }; - }); - $teamFormPopup = webix.ui({ - view: "popup", - id: ids.teamFormPopup, - close: true, - position: "center", - css: { "border-radius": "10px" }, - body: { - rows: [ - { - view: "toolbar", - css: "webix_dark", - cols: [ - { width: 5 }, - { - id: ids.teamFormTitle, - view: "label", - align: "left", - }, - { - view: "icon", - icon: "fa fa-times", - align: "right", - width: 60, - click: () => $teamFormPopup.hide(), - }, - ], - }, - { - view: "form", - id: ids.teamForm, - borderless: true, - elements: [ - { - view: "text", - label: nameField.label, - name: nameField.columnName, - required: true, - }, - { - view: "richselect", - label: strategyField.label, - name: strategyField.columnName, - options: strategyOptions, - required: true, - }, - { - view: "switch", - id: ids.teamFormInactive, - name: inactive, - label: "Inactive", - }, - { view: "text", name: "id", hidden: true }, - { view: "text", name: linkField, hidden: true }, - { - id: ids.teamFormSubmit, - view: "button", - value: this.label("Save"), - disabled: true, - css: "webix_primary", - click: () => { - const values = $$(ids.teamForm).getValues(); - const strategy = strategyOptions.find( - (f) => - f.id === values[strategyField.columnName] - ); - if (values.id) { - this.teamEdit(values, strategy); - } else { - this.teamAddChild(values, strategy); - } - $teamFormPopup.hide(); - }, - }, - ], - on: { - onChange: () => { - const values = $$(ids.teamForm).getValues(); - const valid = - !!values[strategyField.columnName] && - !!values[nameField.columnName]; - const $teamFormSubmit = $$(ids.teamFormSubmit); - if (valid) $teamFormSubmit.enable(); - else $teamFormSubmit.disable(); - }, - }, - }, - ], - }, - }); - } - if (values.__parentID) { - values[linkField] = values.__parentID; - delete values.__parentID; - } - $$(ids.teamFormTitle).setValue(`${this.label(mode)} Team`); - $$(ids.teamForm).setValues(values); - $$(ids.teamFormSubmit).disable(); + // TODO (Guy): Hardcode data panel DCs for Employee. + $$(ids.dataPanel) + .getChildViews()[1] + .getChildViews() + .forEach(($childView) => $childView.callEvent("onViewShow")); + !isContentDone && this.emit("pageData", contentDC, contentCallback); + }; + const contentDisplayCallback = ( + contentDisplayRecords, + isContentDisplayDone, + contentDisplayDC + ) => { + contentCallback(contentDC.getData(), true); + !isContentDisplayDone && + this.emit("pageData", contentDisplayDC, contentDisplayCallback); + }; + this.emit("pageData", teamDC, teamCallback); + this.emit("pageData", contentDC, contentCallback); + contentDisplayDCs.forEach((contentDisplayDC) => { + this.emit("pageData", contentDisplayDC, contentDisplayCallback); + }); + } + + _parseDataPK(dataPK) { + const intDataPk = parseInt(dataPK); + return ( + ((isNaN(intDataPk) || intDataPk.toString().length !== dataPK.length) && + dataPK) || + intDataPk + ); + } - this.teamCanInactivate(values) - ? $$(ids.teamFormInactive).enable() - : $$(ids.teamFormInactive).disable(); - if (mode === "Edit") { - // Check if we can inactivate + _parseFormValueByType(oldFormData, newFormData) { + for (const key in newFormData) { + const oldValue = oldFormData[key]; + const newValue = newFormData[key]; + switch (typeof oldValue) { + case "boolean": + if (newValue == 0) newFormData[key] = false; + else newFormData[key] = true; + break; + case "number": + newFormData[key] = parseInt(newValue); + break; + case "string": + newFormData[key] = newValue?.toString(); + break; + default: + newFormData[key] = newValue; + break; + } } - $teamFormPopup.show(); + return newFormData; + } + + async _reloadAllDC() { + const teamDC = this.datacollection; + const teamDCID = teamDC.id; + const contentDC = this._contentDC; + const contentObjID = contentDC.datasource.id; + await Promise.all([ + this._reloadDCData(teamDC), + ...this._dataPanelDCs + .map((dataPanelDC) => this._reloadDCData(dataPanelDC)) + .this._reloadDCData(contentDC), + ...this._contentDisplayDCs.filter( + (contentDisplayDC) => + contentDisplayDC.datasource.id !== contentObjID || + contentDisplayDC.id !== teamDCID + ), + ]); } - async showContentForm(contentDataRecord) { - const contentObj = this.contentObject(); - const contentModel = contentObj?.model(); + async _reloadDCData(dc) { + if (dc.dataStatus === dc.dataStatusFlag.initializing) + await this._waitDCReady(dc); + dc.clearAll(); + this._initDC(dc); + await this._waitDCReady(dc); + } + + _showDataPanel() { + this.AB.Webix.ui(this._uiDataPanel(), $$(this.ids.dataPanel)).show(); + } + + _showOrgChart() { const settings = this.settings; - const editContentFieldsToCreateNew = - settings.editContentFieldsToCreateNew; - const contentDateStartFieldColumnName = this.getSettingField( - "contentFieldDateStart" - )?.columnName; - const contentDateEndFieldColumnName = this.getSettingField( - "contentFieldDateEnd" - )?.columnName; - - const rules = {}; - const labelWidth = 200; + const AB = this.AB; + const draggable = settings.draggable === 1; 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. - rules[fieldName] = () => true; - const fieldLabel = field.label; - const settings = field.settings; - switch (fieldKey) { - case "boolean": - return { - view: "checkbox", - name: fieldName, - label: fieldLabel, - labelWidth, - }; - case "number": - return { - view: "counter", - name: fieldName, - label: fieldLabel, - labelWidth, - type: "number", - }; - 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, - })), - }; - case "user": - case "connectObject": - const abWebix = this.AB.Webix; - const fieldLinkObj = field.datasourceLink; + const orgchart = new this._OrgChart({ + data: AB.cloneDeep(this._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", + }); - // TODO (Guy): Hardcode for the employee field - if (fieldLabel === "NS Employee Record") - return { - view: "text", - label: "Name", - disabled: true, - labelWidth, - 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] - ) + // 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.children[0].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 dataPanelDCID = key.split(".")[1]; + + // 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": "85%" }, + data: [], + }, + }); + else { + const panelObj = _dataPanelDC.datasource; + cells.push({ + header, + body: { + view: "list", + template: (data) => + `
${panelObj.displayData( + data + )}
`, + css: { overflow: "auto", "max-height": "85%" }, + data: [], + on: { + onViewShow() { + (async () => { + await self._waitDCReady(_dataPanelDC); + const contentLinkedField = panelObj.connectFields( + (field) => field.datasourceLink.id == contentObjID + )[0].fieldLink; + const contentLinkedFieldColumnName = + contentLinkedField.columnName; + this.clearAll(); + this.define( + "data", + // _dataPanelDC.getData() + // TODO (Guy): Hardcode Employee DC. + _dataPanelDC + .getData((panelRecord) => + header === "Unassigned" + ? contentDC.getData( + (contentRecord) => + contentRecord[ + contentLinkedFieldColumnName + ] == panelRecord.id + )[0] == null + : contentDC.getData( + (contentRecord) => + contentRecord[ + contentLinkedFieldColumnName + ] == panelRecord.id + )[0] != null + ) + .sort((a, b) => { + if (a.firstName < b.firstName) { + return -1; + } + if (a.firstName > b.firstName) { + return 1; + } + return 0; + }) + ); + await self._callAfterRender(() => { + const $itemElements = + this.$view.children.item(0).children; + const itemElementsLength = $itemElements.length; + const contentFieldID = contentLinkedField.id; + let count = 0; + while (count < itemElementsLength) { + const $itemElement = $itemElements.item( + count++ + ); + $itemElement.setAttribute( + "data-content-linked-field-id", + contentFieldID + ); + $itemElement.setAttribute( + "data-pk", + _dataPanelDC.getData( + (e) => + e.id == + $itemElement.getAttribute( + "webix_l_id" + ) + )[0][panelObj.PK()] + ); + $itemElement.setAttribute("draggable", "true"); + $itemElement.addEventListener( + "dragstart", + self._fnContentDragStart + ); + $itemElement.addEventListener( + "dragend", + self._fnContentDragEnd ); - 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 { - // TODO (Guy): Add spinner. - this.define( - "options", - (await fieldLinkObj.model().findAll()).data.map( - (e) => ({ - id: e.id, - value: fieldLinkObj.displayData(e), - }) - ) - ); - this.hideProgress(); - this.enable(); - await this.refresh(); - } catch { - // Close popup before response or possily response fail - } - }; - return field.linkType() === "one" - ? { - view: "combo", - name: fieldName, - label: fieldLabel, - disabled: true, - labelWidth, - options: [], - on: { - onViewShow, - }, - } - : { - view: "multicombo", - name: fieldName, - label: fieldLabel, - labelWidth, - stringResult: false, - labelAlign: "left", - options: [], - on: { - onViewShow, - }, - }; - case "date": - case "datetime": - return { - view: "datepicker", - name: fieldName, - label: fieldLabel, - labelWidth, - timepicker: fieldKey === "datetime", - }; - case "file": - case "image": - // TODO (Guy): Add logic - return { - // view: "", - name: fieldName, - label: fieldLabel, - labelWidth, - }; - // case "json": - // case "LongText": - // case "string": - // case "email": - default: - return { - view: "text", - name: fieldName, - label: fieldLabel, - labelWidth, - }; - } + }); + })(); + }, + }, + }, + }); } - ); - contentFormElements.push({ - view: "button", - value: this.label("Save"), - css: "webix_primary", - click: async () => { - const $contentFormData = $$(ids.contentFormData); - if (!$contentFormData.validate()) return; - let isDataChanged = false; - const newFormData = this._parseFormValueByType( - contentDataRecord, - $contentFormData.getValues() - ); - for (const key in newFormData) - if ( - JSON.stringify(newFormData[key]) !== - JSON.stringify(contentDataRecord[key]) - ) { - isDataChanged = true; - break; - } - const $contentForm = $$(ids.contentForm); - $contentForm.blockEvent(); - $contentForm.$view.remove(); - $contentForm.destructor(); - if (!isDataChanged) return; - webix - .confirm({ - title: "Title", - ok: "Yes", - cancel: "No", - text: "You are about to confirm. Are you sure?", - }) - .then(async () => { - this.busy(); - delete newFormData["created_at"]; - delete newFormData["updated_at"]; - delete newFormData["properties"]; - for (const editContentFieldToCreateNew of editContentFieldsToCreateNew) { - const editContentFieldToCreateNewColumnName = - contentObj.fieldByID( - editContentFieldToCreateNew - )?.columnName; - if ( - JSON.stringify( - newFormData[editContentFieldToCreateNewColumnName] ?? - "" - ) !== - JSON.stringify( - contentDataRecord[ - editContentFieldToCreateNewColumnName - ] ?? "" - ) - ) { - const pendingPromises = []; - const oldData = {}; - oldData[contentDateEndFieldColumnName] = new Date(); - pendingPromises.push( - contentModel.update(newFormData.id, oldData) - ); - newFormData[contentDateStartFieldColumnName] = - oldData[contentDateEndFieldColumnName]; - delete newFormData["id"]; - delete newFormData["uuid"]; - delete newFormData[contentDateEndFieldColumnName]; - pendingPromises.push(contentModel.create(newFormData)); - await Promise.all(pendingPromises); - await this.refresh(); - this.ready(); - return; - } - } - await contentModel.update(newFormData.id, newFormData); - await this.refresh(); - this.ready(); - }); + } + return { + id: this.ids.dataPanel, + hidden: true, + view: "tabview", + width: 450, + tabbar: { + height: 60, + type: "bottom", + css: "webix_dark", }, - }); - AB.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(); + cells, + }; + } + + // TODO (Guy): Some DC.waitReady() won't be resolved. + async _waitDCReady(dc) { + if (dc.dataStatus !== dc.dataStatusFlag.initialized) + await new Promise((resolve) => { + dc.once("initializedData", resolve); + }); + } + + ui() { + const self = this; + const ids = self.ids; + const AB = self.AB; + const Webix = AB.Webix; + const _ui = super.ui([ + { + id: ids.chartView, + view: "template", + template: `
`, + css: { + position: "relative", + }, + on: { + onAfterRender() { + Webix.extend(this, Webix.ProgressBar); + const chartDom = document.querySelector(`#${ids.chartDom}`); + chartDom.textContent = ""; + chartDom.innerHTML = ""; + const $chartDomComponents = AB.Webix.ui({ + cols: [ + { + rows: [ + { + view: "template", + height: 50, + }, + { + id: ids.chartContent, + view: "template", + scroll: "auto", + }, + ], }, - }, - ], - }, - { - view: "form", - id: ids.contentFormData, - hidden: true, - elements: contentFormElements, - rules, + self._uiDataPanel(), + ], + }).$view; + chartDom.appendChild($chartDomComponents); + + // Add the filter button to the UI + const $filterButton = document.createElement("button"); + $filterButton.innerHTML = ` Filter`; + $filterButton.classList.add("filter-button"); + $filterButton.onclick = self._fnShowFilterPopup; + $chartDomComponents.children[0].children[0].children[0].append( + $filterButton + ); + $$(ids.dataPanel).show(); }, - ], - }, - on: { - onHide() { - this.$view.remove(); - this.destructor(); }, }, - }).show(); - const $contentFormData = $$(ids.contentFormData); - $contentFormData.setValues(contentDataRecord); - $contentFormData.show(); + ]); + delete _ui.type; + return _ui; } - loadContentDisplayData() { - let contentDisplayDCs = this.contentDisplayDCs; - if (contentDisplayDCs != null) { - for (const key in contentDisplayDCs) - contentDisplayDCs[key].reloadData(); - return; + async init(AB, accessLevel) { + await super.init(AB, accessLevel); + const settings = this.settings; + this._resources = await Promise.all(this._resources); + this._OrgChart = + this._OrgChart || + (this._OrgChart = (() => { + const OrgChart = this._resources[0].default; + const _oldOnDragStart = OrgChart.prototype._onDragStart; + OrgChart.prototype._onDragStart = (event) => { + event.dataTransfer.setData("isnode", 1); + _oldOnDragStart.call(this.__orgchart, event); + }; + return OrgChart; + })()); + + // Preparing for the entity DC and wait for setting a cursor. + if (settings.entityDatacollection) { + const entityDC = + this._entityDC || + (this._entityDC = this.AB.datacollectionByID( + settings.entityDatacollection + )); + this._initDC(entityDC); + await Promise.all([ + this._waitDCReady(entityDC), + new Promise((resolve) => { + const CHANGE_CURSOR = "changeCursor"; + entityDC.off(CHANGE_CURSOR, this._fnRefresh); + if (entityDC.getCursor() != null) { + entityDC.on(CHANGE_CURSOR, this._fnRefresh); + resolve(); + } else + entityDC.once(CHANGE_CURSOR, () => { + entityDC.on(CHANGE_CURSOR, this._fnRefresh); + resolve(); + }); + }), + ]); } - const contentID = this.contentObject().id; - const displayedObjects = Object.keys(this.settings.contentDisplayedFields) - .map((r) => r.split(".")[1]) - .filter((r) => r != contentID); - contentDisplayDCs = this.contentDisplayDCs = {}; - const entityDC = this.entityDC; - displayedObjects.forEach(async (id) => { - const abObj = this.AB.objectByID(id); - const connectField = abObj.connectFields( - (f) => f.settings.linkObject === entityDC.datasource.id - )[0]; - contentDisplayDCs[id] = this.AB.datacollectionNew({ - id, - name: id, - settings: { - datasourceID: id, - // loadAll: true, - linkDatacollectionID: connectField - ? this.settings.entityDatacollection - : undefined, - linkFieldID: connectField?.id, - fixSelect: "", - }, + + // Preparing for the data panel DCs. + if (settings.showDataPanel === 1) { + const _dataPanelDCs = this._dataPanelDCs; + const dataPanelDCs = settings.dataPanelDCs; + for (const key in dataPanelDCs) { + const [, dataPanelDCID] = key.split("."); + const _dataPanelDC = AB.datacollectionByID(dataPanelDCID); + if ( + _dataPanelDCs.findIndex( + (_dataPanelDC) => _dataPanelDC.id === dataPanelDCID + ) < 0 + ) { + this._initDC(_dataPanelDC); + _dataPanelDCs.push(_dataPanelDC); + } + } + } + + // Preparing for the content DC. + const contentDC = + this._contentDC || + (this._contentDC = (() => { + const contentObj = this.AB.objectByID( + this.getSettingField("contentField").settings.linkObject + ); + const contentObjID = contentObj.id; + const contentFieldFilter = JSON.parse(settings.contentFieldFilter); + const dcSettings = { + id: `dc.${contentObjID}`, + settings: { + datasourceID: contentObjID, + linkDatacollectionID: null, + linkFieldID: null, + objectWorkspace: { + filterConditions: { + glue: "and", + rules: [ + // TODO (Guy): Hardcode date start filter. + { + key: contentObj.fieldByID( + settings.contentFieldDateStart + )?.id, + rule: "is_not_null", + value: "", + }, + { + glue: "or", + rules: + (contentFieldFilter.rules?.length > 0 && [ + contentFieldFilter, + + // TODO (Guy): Hardcode date end filter. + { + key: contentObj.fieldByID( + settings.contentFieldDateEnd + )?.id, + rule: "is_null", + value: "", + }, + ]) || + [], + }, + ], + }, + }, + }, + }; + if (this._entityDC) { + const entityDC = this._entityDC; + const entityObjID = entityDC.datasource.id; + dcSettings.linkDatacollectionID = entityDC.id; + dcSettings.linkFieldID = contentObj.connectFields( + (f) => f.settings.linkObject === entityObjID + )[0]?.id; + } + return AB.datacollectionNew(dcSettings); + })()); + this._initDC(contentDC); + + // Preparing for the content group DC. + this._initDC( + this._contentGroupDC || + (this._contentGroupDC = (() => { + const contentGroupObjID = contentDC.datasource.fieldByID( + settings.contentGroupByField + ).settings.linkObject; + return this.AB.datacollectionNew({ + id: `dc.${contentGroupObjID}`, + settings: { + datasourceID: contentGroupObjID, + }, + }); + })()) + ); + + // Prepare display DCs. + const contentDisplayedFieldKeys = Object.keys( + settings.contentDisplayedFields + ); + if (contentDisplayedFieldKeys.length > 0) { + const contentID = contentDC.datasource.id; + const contentDisplayDCSettings = { + datasourceID: null, + linkDatacollectionID: null, + linkFieldID: null, + fixSelect: "", + }; + let entityObjID = null; + if (this._entityDC) { + const entityDC = this._entityDC; + contentDisplayDCSettings.linkDatacollectionID = entityDC.id; + entityObjID = entityDC.datasource.id; + } + const contentDisplayDCs = this._contentDisplayDCs; + let [, objID] = contentDisplayedFieldKeys.pop().split("."); + while (contentDisplayedFieldKeys.length > 0) { + if (objID === entityObjID) { + const entityDC = this._entityDC; + const entityDCID = entityDC.id; + if ( + contentDisplayDCs.findIndex( + (contentDisplayDC) => contentDisplayDC.id === entityDCID + ) < 0 + ) + contentDisplayDCs.push(entityDC); + this._initDC(entityDC); + } else if (objID === contentID) { + const contentDCID = contentDC.id; + if ( + contentDisplayDCs.findIndex( + (contentDisplayDC) => contentDisplayDC.id === contentDCID + ) < 0 + ) + contentDisplayDCs.push(contentDC); + this._initDC(contentDC); + } else { + const contentDisplayDCID = `dc.${objID}`; + this._initDC( + contentDisplayDCs.find( + (contentDisplayDC) => + contentDisplayDC.id === contentDisplayDCID + ) || + (() => { + contentDisplayDCSettings.datasourceID = objID; + if (entityObjID) + contentDisplayDCSettings.linkFieldID = AB.objectByID( + objID + ).connectFields( + (f) => f.settings.linkObject === entityObjID + )[0]?.id; + const contentDisplayDC = AB.datacollectionNew({ + id: contentDisplayDCID, + settings: contentDisplayDCSettings, + }); + contentDisplayDCs.push(contentDisplayDC); + return contentDisplayDC; + })() + ); + } + [, objID] = contentDisplayedFieldKeys.pop().split("."); + } + } + this._resolveInit(); + } + + async onShow() { + this.busy(); + this.AB.performance.mark("TeamChart.onShow"); + await this._promiseInit; + this.ready(); + super.onShow(); + this.AB.performance.mark("TeamChart.load"); + await this.refresh(); + this.AB.performance.measure("TeamChart.load"); + this.AB.performance.measure("TeamChart.onShow"); + } + + /** + * load the data and format it for display + */ + async pullData() { + const dc = this.datacollection; + if (dc == null) return; + const filters = this.__filters; + const settings = this.settings; + await Promise.all([ + this._waitDCReady(dc), + // this._waitDCReady(this._contentDC), + // this._waitDCReady(this._contentGroupDC), + // ...this._contentDisplayDCs.map(async (_contentDisplayDC) => + // this._waitDCReady(_contentDisplayDC) + // ), + // ...this._dataPanelDCs.map(async (_dataPanelDC) => + // this._waitDCReady(_dataPanelDC) + // ), + ]); + let topNode = dc.getCursor(); + const topNodeColumn = this.getSettingField("topTeam").columnName; + if (settings.topTeam) { + const topFromField = dc.getData((e) => e[topNodeColumn] == 1)[0]; + topNode = topFromField ? topFromField : topNode; + } + if (!topNode) return null; + const teamLink = this.getSettingField("teamLink").columnName; + const teamName = this.getSettingField("teamName").columnName; + const teamInactive = this.getSettingField("teamInactive").columnName; + const strategyField = this.getSettingField("teamStrategy").columnName; + const strategyCode = this.getSettingField("strategyCode").columnName; + const MAX_DEPTH = 10; // prevent inifinite loop + + /** + * Recursive function to prepare child node data + * @param {object} node the current node + * @param {number} [depth=0] a count of how many times we have recursed + */ + const pullChildData = (node, depth = 0) => { + if (depth >= MAX_DEPTH) return; + node.children = []; + node._rawData[teamLink].forEach((id) => { + const childData = dc.getData((e) => e.id == id)[0]; + // Don't show inactive teams + if ( + !childData || + (filters?.inactive !== 1 && childData[teamInactive]) + ) + return; + const strategy = childData[`${strategyField}__relation`]; + const code = strategy?.[strategyCode]; + const child = { + name: childData[teamName], + id: this.teamNodeID(id), + className: `strategy-${code}`, + isInactive: childData[teamInactive], + _rawData: childData, + }; + + child.filteredOut = this.filterTeam(filters, child, code); + if (child.name === "External Support") + child.className = `strategy-external`; + if (childData[teamLink].length > 0) { + pullChildData(child, depth + 1); + } + // If this node is filtered we still need it if it has children + // that pass + if (!child.filteredOut || child.children?.length > 0) { + node.children.push(child); + } }); - contentDisplayDCs[id].init(); - await contentDisplayDCs[id].loadData(); + if (node.children.length === 0) { + delete node.children; + } else { + // sort children alphaetically + node.children = node.children.sort((a, b) => + a.name > b.name ? 1 : -1 + ); + } + }; + const topNodeCode = topNode[`${strategyField}__relation`]?.[strategyCode]; + const chartData = (this._chartData = { + id: this.teamNodeID(topNode.id), + name: topNode[teamName] ?? "", + className: `strategy-${topNodeCode}`, + isInactive: topNode[teamInactive], + _rawData: topNode, + filteredOut: false, }); + chartData.filteredOut = this.filterTeam(filters, chartData, topNodeCode); + pullChildData(chartData); } - async contentRecordUI(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("click", async () => { - await this.showContentForm(data); - }); - 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"), - ]; - const $hardcodedSpecialDisplay = element( - "div", - "team-group-record-display" - ); - let currentDataRecords = []; - let currentField = null; - let currentDisplayIndex = 0; - const contentObj = this.contentObject(); - const contentDisplayedFields = this.settings.contentDisplayedFields; - const contentDisplayedFieldsKeys = Object.keys(contentDisplayedFields); + async refresh() { + this.busy(); + const ids = this.ids; + $$(ids.teamFormPopup)?.destructor(); + $$(ids.contentForm)?.destructor(); + await this.pullData(); + this._showDataPanel(); + this._showOrgChart(); + this._pageData(); + this.ready(); + } - 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 = this.contentDisplayDCs[objID]; - switch (objID) { - case contentObj.id: - 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 displayDC.waitReady(); - currentDataRecords = displayDC.getData((r) => { - return currentDataPKs.some((id) => id == r.id); - }); - // await displayedObj.model().findAll({ - // where: { - // glue: "and", - // rules: [ - // { - // key: displayedObj.PK(), - // rule: "in", - // value: currentDataPKs, - // }, - // ], - // }, - // populate: true, - // }) - // ).data; - } - break; - } - if (contentDisplayedFieldsKeys[j + 1]?.split(".")[0] === atDisplay) { - currentField = displayedField; - continue; - } - const $currentDisplay = element("div", "team-group-record-display"); + async filterApply() { + $$(this.ids.filterPopup).hide(); + this.__filters = $$(this.ids.filterForm).getValues(); + await this.refresh(); + } - // TODO (Guy): Now we are hardcoding for each display. - // $rowData.appendChild($currentDisplay); - switch (currentDisplayIndex) { - case 0: - hardcodedDisplays[0].appendChild($currentDisplay); - break; - case 1: - 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; + filterTeam(filters, team, code, contentFieldData) { + // Apply filters (match using or) + if (filters.strategy || filters.teamName || contentFieldData != null) { + let filter = true; + if (filters.strategy !== "" && filters.strategy == code) { + filter = false; } - currentDisplayIndex++; - const displayedFieldColumnName = displayedField.columnName; - 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` - ] + filters.teamName !== "" && + team.name.toLowerCase().includes(filters.teamName.toLowerCase()) ) { - 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 - ) - ); - } - break; + filter = false; } - currentField = null; + const contentObjPK = this._contentDC.datasource.PK(); + if ( + this._contentDC.getData((contentDataRecord) => { + if (Array.isArray(contentFieldData)) + return ( + contentFieldData.indexOf(contentDataRecord[contentObjPK]) > + -1 + ); + return contentFieldData == contentDataRecord[contentObjPK]; + }).length > 0 + ) + filter = false; + return filter; } - // 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; + } + + /** + * Get the ABField from settings + * @param {string} setting key in this.view.settings - should be an id for an + * ABField + */ + getSettingField(setting) { + return this.AB.definitionByID(this.settings[setting]); + } + + async teamAddChild(values, isServerSideUpdate = true) { + const entityDC = this._entityDC; + + // Add the entity value + if (entityDC) { + const connection = + isServerSideUpdate && + entityDC.datasource.connectFields( + (f) => f.settings.linkObject === datacollection.datasource.id + )[0]; + if (connection) { + const entity = entityDC.getCursor(); + const cName = this.AB.definitionByID( + connection.settings.linkColumn + ).columnName; + values[cName] = entity; } - 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"); } - return $ui; - } + const _rawData = + (isServerSideUpdate && + (await this.datacollection.model.create(values))) || + values; + const id = _rawData.id; + const linkField = this.AB.definitionByID( + this.getSettingField("teamLink").settings.linkColumn + ).columnName; + const nameField = this.getSettingField("teamName").columnName; + const parent = document.querySelector( + `#${this.teamNodeID(values[linkField])}` + ); + if (parent == null) return; + const strategyLink = this.getSettingField("teamStrategy").columnName; + const strategyField = this.getSettingField("strategyCode").columnName; + const strategyCode = _rawData[`${strategyLink}__relation`][strategyField]; + const hasChild = parent.parentNode.colSpan > 1; + const newChild = { + name: values[nameField], + id: this.teamNodeID(id), + relationship: hasChild ? "110" : "100", + className: `strategy-${strategyCode}`, + _rawData, + }; - // DRAG EVENTS - 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; + // Need to add differently if the node already has child nodes + if (hasChild) { + const sibling = this.closest(parent, (el) => el.nodeName === "TABLE") + .querySelector(".nodes") + .querySelector(".node"); + this.__orgchart.addSiblings(sibling, { siblings: [newChild] }); + } else { + this.__orgchart.addChildren(parent, { children: [newChild] }); } - dataTransfer.setData("text/plain", JSON.stringify(data)); - // $eventTarget.style.opacity = "0.5"; } - fnContentDragOver(event) { - event.preventDefault(); - event.stopPropagation(); + + teamCanInactivate(values) { + const isInactive = this.getSettingField("teamInactive").columnName; + if (values[isInactive]) return true; // Allow activating inactive teams + const canInactive = this.getSettingField("teamCanInactivate").columnName; + if (!values[canInactive]) return false; + const children = this.getSettingField("teamLink").columnName; + if ( + values[children].some( + (c) => + this.datacollection.getData((r) => r.id == c)[0]?.[isInactive] == + false + ) + ) + return false; + // @TODO check for active assignment + // if (hasActiveAssignment) return false; + return true; } - fnContentDragEnd(event) { - // event.target.style.opacity = "1"; + teamCanDelete(values) { + const canInactive = this.getSettingField("teamCanInactivate").columnName; + if (!values[canInactive]) return false; + const children = this.getSettingField("teamLink").columnName; + if (values[children].length > 0) return false; + // @TODO check for any assignment + // if (hasAssignment) return false; + return true; } - async fnContentDrop(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 newGroupDataPK = $group.dataset.pk; - const newNodeDataPK = JSON.parse( - $group.parentElement.parentElement.dataset.source - )._rawData[nodeObjPK]; - let { - source: updatedData, - pk: dataPK, - contentLinkedFieldID, - } = JSON.parse(dataTransfer.getData("text/plain")); - const orgchart = this.__orgchart; - if (!updatedData) { - // This is a drop from Employee list (new assignment) - const contentLinkedFieldColumnName = - contentObj.fieldByID(contentLinkedFieldID).columnName; - const pendingPromises = []; - const newDate = new Date(); - orgchart.querySelectorAll(".team-group-record").forEach((e) => { - const contentData = JSON.parse(e.dataset.source); - if (contentData[contentLinkedFieldColumnName] == dataPK) { - contentData[contentDateEndFieldColumnName] = newDate; - pendingPromises.push( - contentModel.update(contentData.id, contentData) - ); - } + teamDelete(values) { + if (!this.teamCanDelete(values)) { + this.AB.Webix.message({ + text: "This team cannot be deleted", + type: "error", + expire: 1001, }); - 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()); - } - pendingPromises.push( - contentModel.create(updatedData), - (async () => { - $group - .querySelector(".team-group-content") - .appendChild(await this.contentRecordUI(updatedData, "grey")); - })() - ); - await Promise.all(pendingPromises); - } else { - updatedData = JSON.parse(updatedData); + return; + } + return this.AB.Webix.confirm({ + text: "This can't be undone, are you sure?", + }).then(() => { + this.datacollection.model.delete(values.id); + const nodeID = this.teamNodeID(values.id); + this.__orgchart.removeNodes(document.querySelector(`#${nodeID}`)); + }); + } + + async teamEdit(values, strategy, isServerSideUpdate = true) { + const strategyLink = this.getSettingField("teamStrategy").columnName; + const strategyField = this.getSettingField("strategyCode").columnName; + const strategyCode = strategy[strategyField]; + values[strategyLink] = strategy.id; + delete values[`${strategyLink}__relation`]; + isServerSideUpdate && + (await this.datacollection.model + .update(values.id, values) + .catch((err) => { + //TODO + })); + const nodeID = this.teamNodeID(values.id); + const node = document.querySelector(`#${nodeID}`); + const currentStrategy = node.classList?.value?.match(/strategy-\S+/)[0]; + const newStrategy = `strategy-${strategyCode}`; + if (currentStrategy !== newStrategy) { + node.classList?.remove(currentStrategy); + node.classList?.add(newStrategy); + } + + const inactive = this.getSettingField("teamInactive").columnName; + // Remove inactive node from display, unless the filter setting to show + // inctive nodes is on. + if (this.__filters?.inactive !== 1 && values[inactive] === 1) { + this.__orgchart.removeNodes(node); + } + const nameCol = this.getSettingField("teamName").columnName; + node.querySelector(".title").innerHTML = values[nameCol]; + } - // This is move form another team node - // Move the child node to the target - const dragged = document.querySelector( - `#${this.contentNodeID(updatedData.id)}` + async teamForm(mode, values) { + let $teamFormPopup = $$(this.ids.teamFormPopup); + const inactive = this.getSettingField("teamInactive").columnName; + const linkField = this.AB.definitionByID( + this.getSettingField("teamLink").settings.linkColumn + ).columnName; + const ids = this.ids; + if (!$teamFormPopup) { + const teamObj = this.datacollection.datasource; + const settings = this.settings; + const nameField = teamObj.fieldByID(settings.teamName); + const strategyField = teamObj.fieldByID(settings.teamStrategy); + const strategyObj = this.AB.objectByID( + strategyField.settings.linkObject ); - dragged.parentNode.removeChild(dragged); - $group.querySelector(".team-group-content").appendChild(dragged); - delete updatedData["created_at"]; - delete updatedData["updated_at"]; - delete updatedData["properties"]; - if (dropContentToCreate) { - const pendingPromises = []; - - // TODO (Guy): Force update Date End with a current date. - updatedData[contentDateEndFieldColumnName] = new Date(); - pendingPromises.push( - contentModel.update(updatedData.id, updatedData) - ); - updatedData[contentDateStartFieldColumnName] = - updatedData[contentDateEndFieldColumnName]; - delete updatedData["id"]; - delete updatedData["uuid"]; - delete updatedData[contentDateEndFieldColumnName]; - updatedData[contentFieldLinkColumnName] = newNodeDataPK; - updatedData[contentGroupByFieldColumnName] = newGroupDataPK; - pendingPromises.push(contentModel.create(updatedData)); - await Promise.all(pendingPromises); - } else { - updatedData[contentFieldLinkColumnName] = newNodeDataPK; - updatedData[contentGroupByFieldColumnName] = newGroupDataPK; - await contentModel.update(updatedData.id, updatedData); - } + const entityDC = this._entityDC; + const entityLink = strategyObj.connectFields( + (f) => f.settings.linkObject === entityDC.datasource.id + )[0]; + const cond = { + glue: "and", + rules: [ + { + key: entityLink.columnName, + value: entityDC.getCursor().id, + rule: "equals", + }, + ], + }; + const subCol = this.getSettingField("subStrategy").columnName; + this.entitySrategyOptions = await strategyField.getOptions( + cond, + null, + null, + null, + [subCol] + ); + + const strategyOptions = this.entitySrategyOptions.map((o) => { + return { + id: o.id, + value: o[`${subCol}__relation`].name, + }; + }); + $teamFormPopup = webix.ui({ + view: "popup", + id: ids.teamFormPopup, + close: true, + position: "center", + css: { "border-radius": "10px" }, + body: { + rows: [ + { + view: "toolbar", + css: "webix_dark", + cols: [ + { width: 5 }, + { + id: ids.teamFormTitle, + view: "label", + align: "left", + }, + { + view: "icon", + icon: "fa fa-times", + align: "right", + width: 60, + click: () => $teamFormPopup.hide(), + }, + ], + }, + { + view: "form", + id: ids.teamForm, + borderless: true, + elements: [ + { + view: "text", + label: nameField.label, + name: nameField.columnName, + required: true, + }, + { + view: "richselect", + label: strategyField.label, + name: strategyField.columnName, + options: strategyOptions, + required: true, + }, + { + view: "switch", + id: ids.teamFormInactive, + name: inactive, + label: "Inactive", + }, + { view: "text", name: "id", hidden: true }, + { view: "text", name: linkField, hidden: true }, + { + id: ids.teamFormSubmit, + view: "button", + value: this.label("Save"), + disabled: true, + css: "webix_primary", + click: () => { + const values = $$(ids.teamForm).getValues(); + const strategy = strategyOptions.find( + (f) => + f.id === values[strategyField.columnName] + ); + if (values.id) { + this.teamEdit(values, strategy); + } else { + this.teamAddChild(values, strategy); + } + $teamFormPopup.hide(); + }, + }, + ], + on: { + onChange: () => { + const values = $$(ids.teamForm).getValues(); + const valid = + !!values[strategyField.columnName] && + !!values[nameField.columnName]; + const $teamFormSubmit = $$(ids.teamFormSubmit); + if (valid) $teamFormSubmit.enable(); + else $teamFormSubmit.disable(); + }, + }, + }, + ], + }, + }); } - await this.refresh(); - this.ready(); + if (values.__parentID) { + values[linkField] = values.__parentID; + delete values.__parentID; + } + $$(ids.teamFormTitle).setValue(`${this.label(mode)} Team`); + $$(ids.teamForm).setValues(values); + $$(ids.teamFormSubmit).disable(); + + this.teamCanInactivate(values) + ? $$(ids.teamFormInactive).enable() + : $$(ids.teamFormInactive).disable(); + if (mode === "Edit") { + // Check if we can inactivate + } + $teamFormPopup.show(); } // HELPERS @@ -2107,14 +2212,14 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { busy() { const $chartView = $$(this.ids.chartView); - $chartView?.disable?.(); - $chartView?.showProgress?.({ type: "icon" }); + $chartView.disable(); + $chartView.showProgress({ type: "icon" }); } ready() { const $chartView = $$(this.ids.chartView); - $chartView?.enable?.(); - $chartView?.hideProgress?.(); + $chartView.enable(); + $chartView.hideProgress(); } }; From 107c9bab6abd19b02b32db30da76dff19ba05091 Mon Sep 17 00:00:00 2001 From: guyyoo Date: Thu, 2 Jan 2025 14:51:23 +0700 Subject: [PATCH 084/129] Fix css --- .../ABViewOrgChartTeamsComponent.js | 39 +------------------ styles/team-widget.css | 19 ++------- 2 files changed, 4 insertions(+), 54 deletions(-) diff --git a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js index d7d9757f..f44fb55c 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js @@ -248,7 +248,6 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { return; } const settings = this.settings; - const averageHeight = 80 / contentGroupOptionsLength; const $nodeSpacer = element("div", "spacer"); $content.appendChild($nodeSpacer); const nodeSpacerStyle = $nodeSpacer.style; @@ -257,7 +256,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { const $group = element("div", "team-group-section"); $content.appendChild($group); const groupStyle = $group.style; - groupStyle["height"] = `${averageHeight}%`; + groupStyle["minHeight"] = `${225 / contentGroupOptionsLength}px`; // TODO: should this be a config option const groupColor = group.name === "Leader" ? "#003366" : "#DDDDDD"; @@ -379,41 +378,6 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { this._fnRefresh = async () => { await this.refresh(); }; - this._fnResizeTeamNode = async ($teamNode) => { - const contentGroupDC = this._contentGroupDC; - await this._waitDCReady(contentGroupDC); - const groupHeightThreshold = - (246.5 * contentGroupDC.getData().length) / 100; - const groupSections = $teamNode.querySelectorAll( - ".team-group-section" - ); - let isOverflow = false; - for (const $groupSection of groupSections) - if ( - $groupSection.getBoundingClientRect().height > - groupHeightThreshold - ) { - isOverflow = true; - break; - } - if (!isOverflow) { - $teamNode.style.height = "300px"; - return; - } - groupSections.forEach(($groupSection) => { - const groupHeight = $groupSection.getBoundingClientRect().height; - const groupStyle = $groupSection.style; - groupStyle.height = - (groupHeight < groupHeightThreshold && - `${groupHeightThreshold}px`) || - `${groupHeight}px`; - }); - const nodeChildren = $teamNode.children; - nodeChildren.item(0).style.height = "43.5px"; - const $content = nodeChildren.item(1); - $content.style.top = "-22.5px"; - $content.children.item(0).style.height = "24.65px"; - }; this._fnShowContentForm = (event) => { const contentDC = this._contentDC; const contentObj = contentDC.datasource; @@ -897,7 +861,6 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { ) ); })(); - // await this._callAfterRender(this._fnResizeTeamNode, $teamNode); } async _createUIContentRecord(data, color) { diff --git a/styles/team-widget.css b/styles/team-widget.css index 67f78307..29bc44f0 100644 --- a/styles/team-widget.css +++ b/styles/team-widget.css @@ -16,7 +16,7 @@ org-chart .node { org-chart .node .spacer { border: none; width: 100%; - height: 10%; + height: 15px; } org-chart .node .title { @@ -28,7 +28,6 @@ org-chart .node .title { background: #ef3340; border-radius: 15px; width: 100%; - height: 15%; font-weight: 400; font-size: 32px; color: #ffffff; @@ -37,9 +36,9 @@ org-chart .node .title { org-chart .node .content { position: relative; - top: -7.5%; + top: -15px; width: 100%; - height: 85%; + min-height: 225px; border-radius: 0 0 15px 15px; display: flex; flex-direction: column; @@ -79,11 +78,9 @@ org-chart tr.lines .downLine { .team-group-title { width: 100%; - height: 20%; } .team-group-content { - height: 80%; width: 100%; display: flex; flex-direction: column; @@ -265,13 +262,3 @@ org-chart tr.lines .downLine { 50% { background-color: #eee; } 100% { background-color: #ddd} } - -.team-node-skeleton .content { - /* background: grey; */ - border-radius: 15px !important; - height: 100px !important; - /* width: 325px; */ - animation-name: skeleton; - animation-duration: 2s; - animation-iteration-count: infinite; -} From 19901c8620f065ad831fb22ad1d12e9fc0b2a1df Mon Sep 17 00:00:00 2001 From: guyyoo Date: Fri, 3 Jan 2025 18:51:46 +0700 Subject: [PATCH 085/129] Optimize and fix paging bugs, the entity cursor --- .../ABViewOrgChartTeamsComponent.js | 594 ++++++++++-------- 1 file changed, 340 insertions(+), 254 deletions(-) diff --git a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js index f44fb55c..c0ad33de 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js @@ -47,6 +47,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { this._promiseInit = new Promise((resolve) => { this._resolveInit = resolve; }); + this._promisePageData = null; this._contentDC = null; this._contentGroupDC = null; this._contentDisplayDCs = []; @@ -342,39 +343,115 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { this.ready(); await this.refresh(); }; - this._fnPageData = async (dc, callback) => { - this._initDC(dc); + 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._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 { - if (records.length < DC_OFFSET) { - await dc.loadData(); - - // TODO (Guy): There was some delays before patching DC imcoming data. - await new Promise((resolve) => { - setTimeout(resolve, 500); - }); - records = dc.getData(); - } - if (records.length < DC_OFFSET) throw null; - if ((records.length - DC_OFFSET) % RECORD_LIMIT > 0) throw null; - await dc.loadData( - RECORD_LIMIT * parseInt(records.length / RECORD_LIMIT), - RECORD_LIMIT - ); - - // TODO (Guy): There was some delays before patching DC imcoming data. - await new Promise((resolve) => { - setTimeout(resolve, 500); - }); + 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)); + callback && (await callback(records, false, dc, resolve)); } catch { - callback && (await callback(records, true, dc)); + 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; + } + const contentRecords = contentDC.getData( + (contentRecord) => + teamRecord[contentFieldColumnName].indexOf( + contentRecord[contentRecordPK] + ) > -1 + ); + for (const contentRecord of contentRecords) + this._addContentRecordToGroup($teamNode, contentRecord); + } + if (isTeamDone) resolve(); + else + this._callPagingEvent( + this.datacollection, + this._fnPageTeamCallback, + resolve + ); + }; this._fnRefresh = async () => { await this.refresh(); }; @@ -1109,6 +1186,10 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { }); } + _callPagingEvent(dc, callback, resolve) { + this.emit("pageData", dc, callback, resolve); + } + _initDC(dc) { if (dc.dataStatus === dc.dataStatusFlag.notInitial) { dc.init(); @@ -1117,67 +1198,54 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { } _pageData() { - const ids = this.ids; - const settings = this.settings; + if (this._promisePageData != null) return; + let resolvePageData = null; + this._promisePageData = new Promise((resolve) => { + resolvePageData = resolve; + }); const teamDC = this.datacollection; - const teamObj = teamDC.datasource; - const dataPanelDCs = this._dataPanelDCs; - const contentField = teamObj.fieldByID(settings.contentField); - const contentFieldColumnName = contentField.columnName; - const linkedContentFieldColumnName = contentField.fieldLink.columnName; const contentDC = this._contentDC; - const contentRecordPK = contentDC.datasource.PK(); - const contentDisplayDCs = this._contentDisplayDCs; - const teamCallback = async (teamRecords, isTeamDone) => { - 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; - } - const contentRecords = contentDC.getData( - (contentRecord) => - teamRecord[contentFieldColumnName].indexOf( - contentRecord[contentRecordPK] - ) > -1 - ); - for (const contentRecord of contentRecords) - this._addContentRecordToGroup($teamNode, contentRecord); - } - !isTeamDone && this.emit(teamDC, teamCallback); - }; - const contentCallback = (contentRecords, isContentDone) => { - for (const contentRecord of contentRecords) { - const $teamNode = document.getElementById( - this.teamNodeID(contentRecord[linkedContentFieldColumnName]) - ); - if ($teamNode == null) continue; - this._addContentRecordToGroup($teamNode, contentRecord); + (async () => { + try { + await Promise.all([ + new Promise((resolve) => { + this._callPagingEvent( + teamDC, + this._fnPageTeamCallback, + resolve + ); + }), + new Promise((resolve) => { + this._callPagingEvent( + contentDC, + this._fnPageContentCallback, + resolve + ); + }), + ...this._contentDisplayDCs + .filter( + (contentDisplayDC) => + contentDisplayDC !== teamDC && + contentDisplayDC !== contentDC + ) + .map( + (contentDisplayDC) => + new Promise((resolve) => { + this._callPagingEvent( + contentDisplayDC, + this._fnPageContentDisplayCallback, + resolve + ); + }) + ), + ]); + } catch (err) { + // TODO (Guy): The paging error. + console.error(err); } - - // TODO (Guy): Hardcode data panel DCs for Employee. - $$(ids.dataPanel) - .getChildViews()[1] - .getChildViews() - .forEach(($childView) => $childView.callEvent("onViewShow")); - !isContentDone && this.emit("pageData", contentDC, contentCallback); - }; - const contentDisplayCallback = ( - contentDisplayRecords, - isContentDisplayDone, - contentDisplayDC - ) => { - contentCallback(contentDC.getData(), true); - !isContentDisplayDone && - this.emit("pageData", contentDisplayDC, contentDisplayCallback); - }; - this.emit("pageData", teamDC, teamCallback); - this.emit("pageData", contentDC, contentCallback); - contentDisplayDCs.forEach((contentDisplayDC) => { - this.emit("pageData", contentDisplayDC, contentDisplayCallback); - }); + resolvePageData(); + this._promisePageData = null; + })(); } _parseDataPK(dataPK) { @@ -1213,24 +1281,26 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { } async _reloadAllDC() { + await this._promisePageData; const teamDC = this.datacollection; - const teamDCID = teamDC.id; const contentDC = this._contentDC; - const contentObjID = contentDC.datasource.id; await Promise.all([ this._reloadDCData(teamDC), - ...this._dataPanelDCs - .map((dataPanelDC) => this._reloadDCData(dataPanelDC)) - .this._reloadDCData(contentDC), - ...this._contentDisplayDCs.filter( - (contentDisplayDC) => - contentDisplayDC.datasource.id !== contentObjID || - contentDisplayDC.id !== teamDCID + ...this._dataPanelDCs.map((dataPanelDC) => + this._reloadDCData(dataPanelDC) ), + this._reloadDCData(contentDC), + ...this._contentDisplayDCs + .filter( + (contentDisplayDC) => + contentDisplayDC !== teamDC && contentDisplayDC !== contentDC + ) + .map((contentDisplayDC) => this._reloadDCData(contentDisplayDC)), ]); } async _reloadDCData(dc) { + await this._promisePageData; if (dc.dataStatus === dc.dataStatusFlag.initializing) await this._waitDCReady(dc); dc.clearAll(); @@ -1299,7 +1369,6 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { ).datasource.id ); const contentDC = this._contentDC; - const header = dataPanelDCs[key]; if (_dataPanelDC == null) cells.push({ @@ -1323,81 +1392,75 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { css: { overflow: "auto", "max-height": "85%" }, data: [], on: { - onViewShow() { - (async () => { - await self._waitDCReady(_dataPanelDC); - const contentLinkedField = panelObj.connectFields( - (field) => field.datasourceLink.id == contentObjID - )[0].fieldLink; - const contentLinkedFieldColumnName = - contentLinkedField.columnName; - this.clearAll(); - this.define( - "data", - // _dataPanelDC.getData() - // TODO (Guy): Hardcode Employee DC. - _dataPanelDC - .getData((panelRecord) => - header === "Unassigned" - ? contentDC.getData( - (contentRecord) => - contentRecord[ - contentLinkedFieldColumnName - ] == panelRecord.id - )[0] == null - : contentDC.getData( - (contentRecord) => - contentRecord[ - contentLinkedFieldColumnName - ] == panelRecord.id - )[0] != null - ) - .sort((a, b) => { - if (a.firstName < b.firstName) { - return -1; - } - if (a.firstName > b.firstName) { - return 1; - } - return 0; - }) - ); - await self._callAfterRender(() => { - const $itemElements = - this.$view.children.item(0).children; - const itemElementsLength = $itemElements.length; - const contentFieldID = contentLinkedField.id; - let count = 0; - while (count < itemElementsLength) { - const $itemElement = $itemElements.item( - count++ - ); - $itemElement.setAttribute( - "data-content-linked-field-id", - contentFieldID - ); - $itemElement.setAttribute( - "data-pk", - _dataPanelDC.getData( - (e) => - e.id == - $itemElement.getAttribute( - "webix_l_id" - ) - )[0][panelObj.PK()] - ); - $itemElement.setAttribute("draggable", "true"); - $itemElement.addEventListener( - "dragstart", - self._fnContentDragStart - ); - $itemElement.addEventListener( - "dragend", - self._fnContentDragEnd - ); - } - }); - })(); + async onViewShow() { + await self._waitDCReady(_dataPanelDC); + const contentLinkedField = panelObj.connectFields( + (field) => field.datasourceLink.id == contentObjID + )[0].fieldLink; + const contentLinkedFieldColumnName = + contentLinkedField.columnName; + this.clearAll(); + this.define( + "data", + // _dataPanelDC.getData() + // TODO (Guy): Hardcode Employee DC. + _dataPanelDC + .getData((panelRecord) => + header === "Unassigned" + ? contentDC.getData( + (contentRecord) => + contentRecord[ + contentLinkedFieldColumnName + ] == panelRecord.id + )[0] == null + : contentDC.getData( + (contentRecord) => + contentRecord[ + contentLinkedFieldColumnName + ] == panelRecord.id + )[0] != null + ) + .sort((a, b) => { + if (a.firstName < b.firstName) { + return -1; + } + if (a.firstName > b.firstName) { + return 1; + } + return 0; + }) + ); + await self._callAfterRender(() => { + const $itemElements = + this.$view.children.item(0).children; + const itemElementsLength = $itemElements.length; + const contentFieldID = contentLinkedField.id; + let count = 0; + while (count < itemElementsLength) { + const $itemElement = $itemElements.item(count++); + $itemElement.setAttribute( + "data-content-linked-field-id", + contentFieldID + ); + $itemElement.setAttribute( + "data-pk", + _dataPanelDC.getData( + (e) => + e.id == + $itemElement.getAttribute("webix_l_id") + )[0][panelObj.PK()] + ); + $itemElement.setAttribute("draggable", "true"); + $itemElement.addEventListener( + "dragstart", + self._fnContentDragStart + ); + $itemElement.addEventListener( + "dragend", + self._fnContentDragEnd + ); + } + }); }, }, }, @@ -1486,8 +1549,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { await super.init(AB, accessLevel); const settings = this.settings; this._resources = await Promise.all(this._resources); - this._OrgChart = - this._OrgChart || + this._OrgChart || (this._OrgChart = (() => { const OrgChart = this._resources[0].default; const _oldOnDragStart = OrgChart.prototype._onDragStart; @@ -1499,29 +1561,36 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { })()); // Preparing for the entity DC and wait for setting a cursor. - if (settings.entityDatacollection) { - const entityDC = - this._entityDC || - (this._entityDC = this.AB.datacollectionByID( + const entityDC = + (() => { + const entityDC = this._entityDC; + if (entityDC != null) this._initDC(entityDC); + return entityDC; + })() || + (this._entityDC = await (async () => { + const entityDC = this.AB.datacollectionByID( settings.entityDatacollection - )); - this._initDC(entityDC); - await Promise.all([ - this._waitDCReady(entityDC), - new Promise((resolve) => { - const CHANGE_CURSOR = "changeCursor"; - entityDC.off(CHANGE_CURSOR, this._fnRefresh); - if (entityDC.getCursor() != null) { - entityDC.on(CHANGE_CURSOR, this._fnRefresh); - resolve(); - } else - entityDC.once(CHANGE_CURSOR, () => { - entityDC.on(CHANGE_CURSOR, this._fnRefresh); - resolve(); - }); - }), - ]); - } + ); + if (entityDC != null) { + this._initDC(entityDC); + await Promise.all([ + this._waitDCReady(entityDC), + new Promise((resolve) => { + const CHANGE_CURSOR = "changeCursor"; + entityDC.off(CHANGE_CURSOR, this._fnRefresh); + if (entityDC.getCursor() != null) { + entityDC.on(CHANGE_CURSOR, this._fnRefresh); + resolve(); + } else + entityDC.once(CHANGE_CURSOR, () => { + entityDC.on(CHANGE_CURSOR, this._fnRefresh); + resolve(); + }); + }), + ]); + } + return entityDC; + })()); // Preparing for the data panel DCs. if (settings.showDataPanel === 1) { @@ -1530,14 +1599,10 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { for (const key in dataPanelDCs) { const [, dataPanelDCID] = key.split("."); const _dataPanelDC = AB.datacollectionByID(dataPanelDCID); - if ( - _dataPanelDCs.findIndex( - (_dataPanelDC) => _dataPanelDC.id === dataPanelDCID - ) < 0 - ) { - this._initDC(_dataPanelDC); - _dataPanelDCs.push(_dataPanelDC); - } + _dataPanelDCs.findIndex( + (_dataPanelDC) => _dataPanelDC.id === dataPanelDCID + ) < 0 && _dataPanelDCs.push(_dataPanelDC); + this._initDC(_dataPanelDC); } } @@ -1590,16 +1655,16 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { }, }, }; - if (this._entityDC) { - const entityDC = this._entityDC; + if (entityDC != null) { const entityObjID = entityDC.datasource.id; dcSettings.linkDatacollectionID = entityDC.id; - dcSettings.linkFieldID = contentObj.connectFields( + (dcSettings.linkFieldID = contentObj.connectFields( (f) => f.settings.linkObject === entityObjID - )[0]?.id; + )[0]?.id) && (dcSettings.linkDatacollectionID = entityDC.id); } return AB.datacollectionNew(dcSettings); })()); + contentDC.$dc.__prevLinkDcCursor = entityDC?.getCursor()?.id?.toString(); this._initDC(contentDC); // Preparing for the content group DC. @@ -1609,12 +1674,28 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { const contentGroupObjID = contentDC.datasource.fieldByID( settings.contentGroupByField ).settings.linkObject; - return this.AB.datacollectionNew({ + const dcSettings = { + datasourceID: contentGroupObjID, + linkDatacollectionID: null, + linkFieldID: null, + }; + if (entityDC != null) { + const entityObjID = entityDC.datasource.id; + dcSettings.linkDatacollectionID = entityDC.id; + (dcSettings.linkFieldID = this.AB.objectByID( + contentGroupObjID + ).connectFields( + (f) => f.settings.linkObject === entityObjID + )[0]?.id) && (dcSettings.linkDatacollectionID = entityDC.id); + } + const contentGroupDC = this.AB.datacollectionNew({ id: `dc.${contentGroupObjID}`, - settings: { - datasourceID: contentGroupObjID, - }, + settings: dcSettings, }); + contentGroupDC.$dc.__prevLinkDcCursor = entityDC + ?.getCursor() + ?.id?.toString(); + return contentGroupDC; })()) ); @@ -1623,64 +1704,69 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { settings.contentDisplayedFields ); if (contentDisplayedFieldKeys.length > 0) { - const contentID = contentDC.datasource.id; + const teamDC = this.datacollection; + const teamObjID = teamDC.datasource.id; + const contentObjID = contentDC.datasource.id; const contentDisplayDCSettings = { datasourceID: null, linkDatacollectionID: null, linkFieldID: null, fixSelect: "", }; - let entityObjID = null; - if (this._entityDC) { - const entityDC = this._entityDC; - contentDisplayDCSettings.linkDatacollectionID = entityDC.id; - entityObjID = entityDC.datasource.id; - } const contentDisplayDCs = this._contentDisplayDCs; let [, objID] = contentDisplayedFieldKeys.pop().split("."); while (contentDisplayedFieldKeys.length > 0) { - if (objID === entityObjID) { - const entityDC = this._entityDC; - const entityDCID = entityDC.id; - if ( + switch (objID) { + case teamObjID: contentDisplayDCs.findIndex( - (contentDisplayDC) => contentDisplayDC.id === entityDCID - ) < 0 - ) - contentDisplayDCs.push(entityDC); - this._initDC(entityDC); - } else if (objID === contentID) { - const contentDCID = contentDC.id; - if ( + (contentDisplayDC) => contentDisplayDC.id === teamObjID + ) < 0 && contentDisplayDCs.push(teamDC); + this._initDC(teamDC); + break; + case contentObjID: contentDisplayDCs.findIndex( - (contentDisplayDC) => contentDisplayDC.id === contentDCID - ) < 0 - ) - contentDisplayDCs.push(contentDC); - this._initDC(contentDC); - } else { - const contentDisplayDCID = `dc.${objID}`; - this._initDC( - contentDisplayDCs.find( - (contentDisplayDC) => - contentDisplayDC.id === contentDisplayDCID - ) || - (() => { - contentDisplayDCSettings.datasourceID = objID; - if (entityObjID) - contentDisplayDCSettings.linkFieldID = AB.objectByID( - objID - ).connectFields( - (f) => f.settings.linkObject === entityObjID - )[0]?.id; - const contentDisplayDC = AB.datacollectionNew({ - id: contentDisplayDCID, - settings: contentDisplayDCSettings, - }); - contentDisplayDCs.push(contentDisplayDC); - return contentDisplayDC; - })() - ); + (contentDisplayDC) => contentDisplayDC === contentDC + ) < 0 && contentDisplayDCs.push(contentDC); + this._initDC(contentDC); + break; + default: + if (entityDC?.datasource.id === objID) { + contentDisplayDCs.findIndex( + (contentDisplayDC) => contentDisplayDC === entityDC + ) < 0 && contentDisplayDCs.push(entityDC); + this._initDC(entityDC); + } else { + const contentDisplayDCID = `dc.${objID}`; + this._initDC( + contentDisplayDCs.find( + (contentDisplayDC) => + contentDisplayDC.id === contentDisplayDCID + ) || + (() => { + contentDisplayDCSettings.datasourceID = objID; + if (entityDC != null) { + const entityObjID = entityDC.datasource.id; + (contentDisplayDCSettings.linkFieldID = + AB.objectByID(objID).connectFields( + (f) => + f.settings.linkObject === entityObjID + )[0]?.id) && + (contentDisplayDCSettings.linkDatacollectionID = + entityDC.id); + } + const contentDisplayDC = AB.datacollectionNew({ + id: contentDisplayDCID, + settings: contentDisplayDCSettings, + }); + contentDisplayDC.$dc.__prevLinkDcCursor = entityDC + ?.getCursor() + ?.id?.toString(); + contentDisplayDCs.push(contentDisplayDC); + return contentDisplayDC; + })() + ); + } + break; } [, objID] = contentDisplayedFieldKeys.pop().split("."); } From 7f386f66f3a0b123399d8da341f2e04b38fa4242 Mon Sep 17 00:00:00 2001 From: guyyoo Date: Sun, 5 Jan 2025 18:08:26 +0700 Subject: [PATCH 086/129] Optimize render and refresh and the changing cursor --- .../ABViewOrgChartTeamsComponent.js | 457 ++++++++++-------- 1 file changed, 265 insertions(+), 192 deletions(-) diff --git a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js index c0ad33de..c7bca957 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js @@ -224,11 +224,11 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { // TODO (Guy): The reload DCs error. console.error(err); } - this.ready(); await this.refresh(); draggedNodes.forEach(($draggedNode) => { $draggedNode.remove(); }); + this.ready(); }; this._fnCreateNode = async ($node, data) => { // remove built in icon @@ -340,8 +340,8 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { // TODO (Guy): The reload DCs error. console.error(err); } - this.ready(); await this.refresh(); + this.ready(); }; this._fnPageContentCallback = ( contentRecords, @@ -373,27 +373,43 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { resolve ); }; - this._fnPageContentDisplayCallback = ( - contentDisplayRecords, - isContentDisplayDone, - contentDisplayDC, + (this._fnPageContentGroupCallback = async ( + contentGroupRecords, + isContentGroupDone, + contentGroupDC, resolve ) => { - const contentDC = this._contentDC; - this._fnPageContentCallback( - contentDC.getData(), - true, - contentDC, - () => {} - ); - if (isContentDisplayDone) resolve(); + const teamDC = this.datacollection; + this._fnPageTeamCallback(teamDC.getData(), true, teamDC, () => {}); + if (isContentGroupDone) resolve(); else this._callPagingEvent( - contentDisplayDC, - this._fnPageContentDisplayCallback, + 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(); @@ -434,7 +450,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { await this.teamAddChild(teamRecord, false); $teamNode = document.getElementById(teamNodeID); if ($teamNode == null) continue; - } + } else await this.teamEdit(teamRecord, null, false); const contentRecords = contentDC.getData( (contentRecord) => teamRecord[contentFieldColumnName].indexOf( @@ -453,7 +469,35 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { ); }; 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; @@ -581,9 +625,9 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { }) ) ); - this.hideProgress(); + this.refresh(); this.enable(); - await this.refresh(); + this.hideProgress(); } catch { // Close popup before response or possily response fail } @@ -735,9 +779,9 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { // TODO (Guy): The reload DCs error. console.error(err); } - this.ready(); await this.refresh(); $contentNode.remove(); + this.ready(); return; } } @@ -757,9 +801,9 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { // TODO (Guy): The reload DCs error. console.error(err); } - this.ready(); await this.refresh(); $contentNode.remove(); + this.ready(); }); }, }); @@ -914,22 +958,29 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { } _addContentRecordToGroup($teamNode, contentRecord) { - const contentNodeID = this.contentNodeID(contentRecord.id); - let $contentNode = document.getElementById(contentNodeID); - while ($contentNode != null) { - $contentNode.remove(); - $contentNode = document.getElementById(contentNodeID); - } + 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 () => { - $teamNode - .querySelector( - `.team-group-section[data-pk="${ - contentRecord[ - this.getSettingField("contentGroupByField").columnName - ] - }"] > .team-group-content` - ) - ?.appendChild( + 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[ @@ -937,6 +988,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { ] ) ); + }); })(); } @@ -1191,10 +1243,8 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { } _initDC(dc) { - if (dc.dataStatus === dc.dataStatusFlag.notInitial) { - dc.init(); - dc.loadData(); - } + dc.init(); + if (dc.dataStatus === dc.dataStatusFlag.notInitial) dc.loadData(); } _pageData() { @@ -1203,8 +1253,10 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { 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([ @@ -1222,11 +1274,20 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { resolve ); }), + new Promise((resolve) => { + this._callPagingEvent( + contentGroupDC, + this._fnPageContentGroupCallback, + resolve + ); + }), ...this._contentDisplayDCs .filter( (contentDisplayDC) => + contentDisplayDC !== entityDC && contentDisplayDC !== teamDC && - contentDisplayDC !== contentDC + contentDisplayDC !== contentDC && + contentDisplayDC !== contentGroupDC ) .map( (contentDisplayDC) => @@ -1281,9 +1342,10 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { } async _reloadAllDC() { - await this._promisePageData; + 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) => @@ -1293,7 +1355,10 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { ...this._contentDisplayDCs .filter( (contentDisplayDC) => - contentDisplayDC !== teamDC && contentDisplayDC !== contentDC + contentDisplayDC !== entityDC && + contentDisplayDC !== teamDC && + contentDisplayDC !== contentDC && + contentDisplayDC !== contentGroupDC ) .map((contentDisplayDC) => this._reloadDCData(contentDisplayDC)), ]); @@ -1336,7 +1401,6 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { // 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; @@ -1481,12 +1545,32 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { }; } + async _waitDCPending(dc) { + switch (dc.dataStatus) { + case dc.dataStatusFlag.notInitial: + case dc.dataStatusFlag.initialized: + await new Promise((resolve) => { + dc.once("initializingData", resolve); + }); + break; + default: + break; + } + } + // TODO (Guy): Some DC.waitReady() won't be resolved. async _waitDCReady(dc) { - if (dc.dataStatus !== dc.dataStatusFlag.initialized) - await new Promise((resolve) => { - dc.once("initializedData", resolve); - }); + const dataStatusFlag = dc.dataStatusFlag; + switch (dc.dataStatus) { + case dataStatusFlag.notInitial: + case dataStatusFlag.initializing: + await new Promise((resolve) => { + dc.once("initializedData", resolve); + }); + break; + default: + break; + } } ui() { @@ -1615,89 +1699,92 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { ); const contentObjID = contentObj.id; const contentFieldFilter = JSON.parse(settings.contentFieldFilter); - const dcSettings = { - id: `dc.${contentObjID}`, - settings: { - datasourceID: contentObjID, - linkDatacollectionID: null, - linkFieldID: null, - objectWorkspace: { - filterConditions: { - glue: "and", - rules: [ - // TODO (Guy): Hardcode date start filter. - { - key: contentObj.fieldByID( - settings.contentFieldDateStart - )?.id, - rule: "is_not_null", - value: "", - }, - { - glue: "or", - rules: - (contentFieldFilter.rules?.length > 0 && [ - contentFieldFilter, - - // TODO (Guy): Hardcode date end filter. - { - key: contentObj.fieldByID( - settings.contentFieldDateEnd - )?.id, - rule: "is_null", - value: "", - }, - ]) || - [], - }, - ], - }, + const contentDCSettings = { + datasourceID: contentObjID, + linkDatacollectionID: null, + linkFieldID: null, + objectWorkspace: { + filterConditions: { + glue: "and", + rules: [ + // TODO (Guy): Hardcode date start filter. + { + key: contentObj.fieldByID( + settings.contentFieldDateStart + )?.id, + rule: "is_not_null", + value: "", + }, + { + glue: "or", + rules: + (contentFieldFilter.rules?.length > 0 && [ + contentFieldFilter, + + // TODO (Guy): Hardcode date end filter. + { + key: contentObj.fieldByID( + settings.contentFieldDateEnd + )?.id, + rule: "is_null", + value: "", + }, + ]) || + [], + }, + ], }, }, }; if (entityDC != null) { const entityObjID = entityDC.datasource.id; - dcSettings.linkDatacollectionID = entityDC.id; - (dcSettings.linkFieldID = contentObj.connectFields( + (contentDCSettings.linkFieldID = contentObj.connectFields( (f) => f.settings.linkObject === entityObjID - )[0]?.id) && (dcSettings.linkDatacollectionID = entityDC.id); + )[0]?.id) && + (contentDCSettings.linkDatacollectionID = entityDC.id); } - return AB.datacollectionNew(dcSettings); + const contentDC = AB.datacollectionNew({ + id: `dc.${contentObjID}`, + settings: contentDCSettings, + }); + contentDC.$dc.__prevLinkDcCursor = entityDC + ?.getCursor() + ?.id?.toString(); + this._initDC(contentDC); + return contentDC; })()); - contentDC.$dc.__prevLinkDcCursor = entityDC?.getCursor()?.id?.toString(); - this._initDC(contentDC); // Preparing for the content group DC. - this._initDC( + const contentGroupDC = this._contentGroupDC || - (this._contentGroupDC = (() => { - const contentGroupObjID = contentDC.datasource.fieldByID( - settings.contentGroupByField - ).settings.linkObject; - const dcSettings = { - datasourceID: contentGroupObjID, - linkDatacollectionID: null, - linkFieldID: null, - }; - if (entityDC != null) { - const entityObjID = entityDC.datasource.id; - dcSettings.linkDatacollectionID = entityDC.id; - (dcSettings.linkFieldID = this.AB.objectByID( - contentGroupObjID - ).connectFields( - (f) => f.settings.linkObject === entityObjID - )[0]?.id) && (dcSettings.linkDatacollectionID = entityDC.id); - } - const contentGroupDC = this.AB.datacollectionNew({ - id: `dc.${contentGroupObjID}`, - settings: dcSettings, - }); - contentGroupDC.$dc.__prevLinkDcCursor = entityDC - ?.getCursor() - ?.id?.toString(); - return contentGroupDC; - })()) - ); + (this._contentGroupDC = (() => { + const contentGroupObjID = contentDC.datasource.fieldByID( + settings.contentGroupByField + ).settings.linkObject; + const contentGroupDCSettings = { + datasourceID: contentGroupObjID, + linkDatacollectionID: null, + linkFieldID: null, + }; + if (entityDC != null) { + const entityObjID = entityDC.datasource.id; + (contentGroupDCSettings.linkFieldID = this.AB.objectByID( + contentGroupObjID + ).connectFields( + (f) => f.settings.linkObject === entityObjID + )[0]?.id) && + (contentGroupDCSettings.linkDatacollectionID = entityDC.id); + } + const contentGroupDC = this.AB.datacollectionNew({ + id: `dc.${contentGroupObjID}`, + settings: contentGroupDCSettings, + }); + contentGroupDC.$dc.__prevLinkDcCursor = entityDC + ?.getCursor() + ?.id?.toString(); + this._initDC(contentGroupDC); + return contentGroupDC; + })()); // Prepare display DCs. const contentDisplayedFieldKeys = Object.keys( @@ -1707,6 +1794,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { const teamDC = this.datacollection; const teamObjID = teamDC.datasource.id; const contentObjID = contentDC.datasource.id; + const contentGroupObjID = contentGroupDC.datasource.id; const contentDisplayDCSettings = { datasourceID: null, linkDatacollectionID: null, @@ -1716,58 +1804,51 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { const contentDisplayDCs = this._contentDisplayDCs; let [, objID] = contentDisplayedFieldKeys.pop().split("."); while (contentDisplayedFieldKeys.length > 0) { - switch (objID) { - case teamObjID: - contentDisplayDCs.findIndex( - (contentDisplayDC) => contentDisplayDC.id === teamObjID - ) < 0 && contentDisplayDCs.push(teamDC); - this._initDC(teamDC); - break; - case contentObjID: - contentDisplayDCs.findIndex( - (contentDisplayDC) => contentDisplayDC === contentDC - ) < 0 && contentDisplayDCs.push(contentDC); - this._initDC(contentDC); - break; - default: - if (entityDC?.datasource.id === objID) { - contentDisplayDCs.findIndex( - (contentDisplayDC) => contentDisplayDC === entityDC - ) < 0 && contentDisplayDCs.push(entityDC); - this._initDC(entityDC); - } else { - const contentDisplayDCID = `dc.${objID}`; - this._initDC( - contentDisplayDCs.find( - (contentDisplayDC) => - contentDisplayDC.id === contentDisplayDCID - ) || - (() => { - contentDisplayDCSettings.datasourceID = objID; - if (entityDC != null) { - const entityObjID = entityDC.datasource.id; - (contentDisplayDCSettings.linkFieldID = - AB.objectByID(objID).connectFields( - (f) => - f.settings.linkObject === entityObjID - )[0]?.id) && - (contentDisplayDCSettings.linkDatacollectionID = - entityDC.id); - } - const contentDisplayDC = AB.datacollectionNew({ - id: contentDisplayDCID, - settings: contentDisplayDCSettings, - }); - contentDisplayDC.$dc.__prevLinkDcCursor = entityDC - ?.getCursor() - ?.id?.toString(); - contentDisplayDCs.push(contentDisplayDC); - return contentDisplayDC; - })() - ); - } - break; - } + if ( + contentDisplayDCs.findIndex( + (contentDisplayDC) => contentDisplayDC.datasource.id === objID + ) < 0 + ) + switch (objID) { + case teamObjID: + this._initDC(teamDC); + contentDisplayDCs.push(teamDC); + break; + case contentObjID: + this._initDC(contentDC); + contentDisplayDCs.push(contentDC); + break; + case contentGroupObjID: + contentDisplayDCs.push(contentGroupDC); + this._initDC(contentGroupDC); + break; + default: + if (entityDC?.datasource.id === objID) { + this._initDC(entityDC); + contentDisplayDCs.push(entityDC); + } else { + contentDisplayDCSettings.datasourceID = objID; + if (entityDC != null) { + const entityObjID = entityDC.datasource.id; + (contentDisplayDCSettings.linkFieldID = + AB.objectByID(objID).connectFields( + (f) => f.settings.linkObject === entityObjID + )[0]?.id) && + (contentDisplayDCSettings.linkDatacollectionID = + entityDC.id); + } + const contentDisplayDC = AB.datacollectionNew({ + id: `dc.${objID}`, + settings: contentDisplayDCSettings, + }); + contentDisplayDC.$dc.__prevLinkDcCursor = entityDC + ?.getCursor() + ?.id?.toString(); + this._initDC(contentDisplayDC); + contentDisplayDCs.push(contentDisplayDC); + } + break; + } [, objID] = contentDisplayedFieldKeys.pop().split("."); } } @@ -1778,12 +1859,12 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { this.busy(); this.AB.performance.mark("TeamChart.onShow"); await this._promiseInit; - this.ready(); super.onShow(); this.AB.performance.mark("TeamChart.load"); await this.refresh(); this.AB.performance.measure("TeamChart.load"); this.AB.performance.measure("TeamChart.onShow"); + this.ready(); } /** @@ -1794,17 +1875,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { if (dc == null) return; const filters = this.__filters; const settings = this.settings; - await Promise.all([ - this._waitDCReady(dc), - // this._waitDCReady(this._contentDC), - // this._waitDCReady(this._contentGroupDC), - // ...this._contentDisplayDCs.map(async (_contentDisplayDC) => - // this._waitDCReady(_contentDisplayDC) - // ), - // ...this._dataPanelDCs.map(async (_dataPanelDC) => - // this._waitDCReady(_dataPanelDC) - // ), - ]); + await this._waitDCReady(dc); let topNode = dc.getCursor(); const topNodeColumn = this.getSettingField("topTeam").columnName; if (settings.topTeam) { @@ -1880,7 +1951,6 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { } async refresh() { - this.busy(); const ids = this.ids; $$(ids.teamFormPopup)?.destructor(); $$(ids.contentForm)?.destructor(); @@ -1888,13 +1958,15 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { this._showDataPanel(); this._showOrgChart(); this._pageData(); - this.ready(); } async filterApply() { - $$(this.ids.filterPopup).hide(); - this.__filters = $$(this.ids.filterForm).getValues(); + this.busy(); + const ids = this.ids; + $$(ids.filterPopup).hide(); + this.__filters = $$(ids.filterForm).getValues(); await this.refresh(); + this.ready(); } filterTeam(filters, team, code, contentFieldData) { @@ -2039,8 +2111,8 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { async teamEdit(values, strategy, isServerSideUpdate = true) { const strategyLink = this.getSettingField("teamStrategy").columnName; const strategyField = this.getSettingField("strategyCode").columnName; - const strategyCode = strategy[strategyField]; - values[strategyLink] = strategy.id; + const strategyCode = strategy?.[strategyField]; + values[strategyLink] = strategy?.id || values[strategyLink]; delete values[`${strategyLink}__relation`]; isServerSideUpdate && (await this.datacollection.model @@ -2051,7 +2123,8 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { const nodeID = this.teamNodeID(values.id); const node = document.querySelector(`#${nodeID}`); const currentStrategy = node.classList?.value?.match(/strategy-\S+/)[0]; - const newStrategy = `strategy-${strategyCode}`; + const newStrategy = + (strategyCode && `strategy-${strategyCode}`) || currentStrategy; if (currentStrategy !== newStrategy) { node.classList?.remove(currentStrategy); node.classList?.add(newStrategy); From b19c89118f3d9796d92be30b631716d958e4d24e Mon Sep 17 00:00:00 2001 From: guyyoo Date: Mon, 6 Jan 2025 17:16:59 +0700 Subject: [PATCH 087/129] Refactor the filter feature --- .../ABViewOrgChartTeamsComponent.js | 262 +++++++++++++----- 1 file changed, 198 insertions(+), 64 deletions(-) diff --git a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js index c7bca957..5857ffba 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js @@ -1,6 +1,7 @@ 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( @@ -460,8 +461,10 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { for (const contentRecord of contentRecords) this._addContentRecordToGroup($teamNode, contentRecord); } - if (isTeamDone) resolve(); - else + if (isTeamDone) { + await this.pullData(); + resolve(); + } else this._callPagingEvent( this.datacollection, this._fnPageTeamCallback, @@ -1873,7 +1876,6 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { async pullData() { const dc = this.datacollection; if (dc == null) return; - const filters = this.__filters; const settings = this.settings; await this._waitDCReady(dc); let topNode = dc.getCursor(); @@ -1883,12 +1885,6 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { topNode = topFromField ? topFromField : topNode; } if (!topNode) return null; - const teamLink = this.getSettingField("teamLink").columnName; - const teamName = this.getSettingField("teamName").columnName; - const teamInactive = this.getSettingField("teamInactive").columnName; - const strategyField = this.getSettingField("teamStrategy").columnName; - const strategyCode = this.getSettingField("strategyCode").columnName; - const MAX_DEPTH = 10; // prevent inifinite loop /** * Recursive function to prepare child node data @@ -1896,38 +1892,48 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { * @param {number} [depth=0] a count of how many times we have recursed */ const pullChildData = (node, depth = 0) => { - if (depth >= MAX_DEPTH) return; + if (depth >= TEAM_CHART_MAX_DEPTH) return; node.children = []; - node._rawData[teamLink].forEach((id) => { - const childData = dc.getData((e) => e.id == id)[0]; - // Don't show inactive teams - if ( - !childData || - (filters?.inactive !== 1 && childData[teamInactive]) - ) - return; - const strategy = childData[`${strategyField}__relation`]; - const code = strategy?.[strategyCode]; - const child = { - name: childData[teamName], - id: this.teamNodeID(id), - className: `strategy-${code}`, - isInactive: childData[teamInactive], - _rawData: childData, - }; - - child.filteredOut = this.filterTeam(filters, child, code); - if (child.name === "External Support") - child.className = `strategy-external`; - if (childData[teamLink].length > 0) { - pullChildData(child, depth + 1); - } - // If this node is filtered we still need it if it has children - // that pass - if (!child.filteredOut || child.children?.length > 0) { - node.children.push(child); + node._rawData[this.getSettingField("teamLink").columnName].forEach( + (id) => { + const childData = dc.getData((e) => e.id == id)[0]; + // Don't show inactive teams + if ( + !childData || + (this.__filters?.inactive !== 1 && + childData[this.getSettingField("teamInactive").columnName]) + ) + return; + const child = { + name: childData[this.getSettingField("teamName").columnName], + id: this.teamNodeID(id), + className: `strategy-${ + childData[ + `${ + this.getSettingField("teamStrategy").columnName + }__relation` + ]?.[this.getSettingField("strategyCode").columnName] + }`, + isInactive: + childData[this.getSettingField("teamInactive").columnName], + _rawData: childData, + }; + child.filteredOut = this.filterTeam(child); + if (child.name === "External Support") + child.className = `strategy-external`; + if ( + childData[this.getSettingField("teamLink").columnName] + .length > 0 + ) { + pullChildData(child, depth + 1); + } + // If this node is filtered we still need it if it has children + // that pass + if (!child.filteredOut || child.children?.length > 0) { + node.children.push(child); + } } - }); + ); if (node.children.length === 0) { delete node.children; } else { @@ -1937,16 +1943,19 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { ); } }; - const topNodeCode = topNode[`${strategyField}__relation`]?.[strategyCode]; const chartData = (this._chartData = { id: this.teamNodeID(topNode.id), - name: topNode[teamName] ?? "", - className: `strategy-${topNodeCode}`, - isInactive: topNode[teamInactive], + name: topNode[this.getSettingField("teamName").columnName] ?? "", + className: `strategy-${ + topNode[ + `${this.getSettingField("teamStrategy").columnName}__relation` + ]?.[this.getSettingField("strategyCode").columnName] + }`, + isInactive: topNode[this.getSettingField("teamInactive").columnName], _rawData: topNode, filteredOut: false, }); - chartData.filteredOut = this.filterTeam(filters, chartData, topNodeCode); + chartData.filteredOut = this.filterTeam(chartData); pullChildData(chartData); } @@ -1962,40 +1971,162 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { async filterApply() { this.busy(); + await this._promisePageData; const ids = this.ids; $$(ids.filterPopup).hide(); this.__filters = $$(ids.filterForm).getValues(); - await this.refresh(); + this.__orgchart?.remove(); + this.__orgchart = null; + await this.pullData(); + this._showOrgChart(); + await this._callAfterRender(() => { + const contentDC = this._contentDC; + this._fnPageContentCallback( + contentDC.getData(), + true, + contentDC, + () => {} + ); + }); this.ready(); } - filterTeam(filters, team, code, contentFieldData) { + filterTeam(team) { + const filters = this.__filters; + let filter = false; + filters.strategy = filters.strategy ?? ""; + filters.teamName = filters.teamName ?? ""; + // Apply filters (match using or) - if (filters.strategy || filters.teamName || contentFieldData != null) { - let filter = true; - if (filters.strategy !== "" && filters.strategy == code) { + if (filters.strategy || filters.teamName) { + filter = true; + if ( + filters.strategy !== "" && + filters.strategy == team.className.replace("strategy-", "") + ) filter = false; - } if ( filters.teamName !== "" && team.name.toLowerCase().includes(filters.teamName.toLowerCase()) - ) { + ) filter = false; + if (!filter) return filter; + } + const AB = this.AB; + const settings = this.settings; + const contentDisplayFieldFilters = settings.contentDisplayedFieldFilters; + for (const key in contentDisplayFieldFilters) { + filters[key] = filters[key] ?? ""; + if (filters[key] !== "") filter = true; + } + if (!filter) return filter; + const contentField = settings.contentField; + const teamObj = this.datacollection.datasource; + const contentFieldLinkColumnName = teamObj.connectFields( + (connectField) => connectField.id === contentField + )[0].fieldLink.columnName; + const contentDC = this._contentDC; + const contentObj = contentDC.datasource; + const contentObjID = contentObj.id; + const contentObjPK = contentObj.PK(); + const teamRecordPK = team._rawData[teamObj.PK()]; + const contentDisplayedFields = settings.contentDisplayedFields; + const contentDisplayedFieldKeys = Object.keys(contentDisplayedFields); + const contentDisplayDCs = this._contentDisplayDCs; + let currentContentDisplayFieldKey = null; + let currentContentDisplayDC = null; + let currentContentDisplayObjID = null; + let currentContentDisplayObjPK = null; + let currentContentDisplayFieldColumnName = null; + let currentContentDisplayFilterValue = null; + let currentContentDisplayRecords = []; + while (contentDisplayedFieldKeys.length > 0) { + currentContentDisplayFieldKey = contentDisplayedFieldKeys.pop(); + currentContentDisplayObjID = + currentContentDisplayFieldKey.split(".")[1]; + currentContentDisplayFilterValue = + filters[ + `${currentContentDisplayFieldKey}.${contentDisplayedFields[currentContentDisplayFieldKey]}.0` + ]; + if (currentContentDisplayFilterValue == null) + currentContentDisplayFilterValue = + filters[ + `${currentContentDisplayFieldKey}.${contentDisplayedFields[currentContentDisplayFieldKey]}.1` + ]; + if (currentContentDisplayFilterValue != null) { + if (currentContentDisplayFilterValue === "") continue; + currentContentDisplayFilterValue = currentContentDisplayFilterValue + .toString() + .toLowerCase(); + currentContentDisplayFieldColumnName = AB.definitionByID( + contentDisplayedFields[currentContentDisplayFieldKey] + ).columnName; + currentContentDisplayDC = contentDisplayDCs.find( + (contentDisplayDC) => + contentDisplayDC.datasource.id === currentContentDisplayObjID + ); + currentContentDisplayObjPK = + currentContentDisplayDC.datasource.PK(); + currentContentDisplayRecords = currentContentDisplayDC + .getData( + (contentDisplayRecord) => + contentDisplayRecord[currentContentDisplayFieldColumnName] + .toString() + .toLowerCase() + .indexOf(currentContentDisplayFilterValue) > -1 + ) + .map((contentDisplayRecord) => + contentDisplayRecord[currentContentDisplayObjPK].toString() + ); + } else if (currentContentDisplayRecords.length > 0) { + currentContentDisplayFieldColumnName = AB.definitionByID( + contentDisplayedFields[currentContentDisplayFieldKey] + ).columnName; + currentContentDisplayDC = contentDisplayDCs.find( + (contentDisplayDC) => + contentDisplayDC.datasource.id === currentContentDisplayObjID + ); + currentContentDisplayObjPK = + currentContentDisplayDC.datasource.PK(); + currentContentDisplayRecords = currentContentDisplayDC + .getData((contentDisplayRecord) => { + const contentDisplayRecordData = + contentDisplayRecord[currentContentDisplayFieldColumnName]; + return Array.isArray(contentDisplayRecordData) + ? contentDisplayRecordData.findIndex( + (e) => + currentContentDisplayRecords.indexOf( + e.toString() + ) > -1 + ) > -1 + : currentContentDisplayRecords.indexOf( + contentDisplayRecordData.toString() + ) > -1; + }) + .map((contentDisplayRecord) => + contentDisplayRecord[currentContentDisplayObjPK].toString() + ); } - const contentObjPK = this._contentDC.datasource.PK(); if ( - this._contentDC.getData((contentDataRecord) => { - if (Array.isArray(contentFieldData)) - return ( - contentFieldData.indexOf(contentDataRecord[contentObjPK]) > - -1 - ); - return contentFieldData == contentDataRecord[contentObjPK]; - }).length > 0 - ) + currentContentDisplayObjID === contentObjID && + currentContentDisplayRecords.length > 0 && + contentDC + .getData( + (contentRecord) => + contentRecord[contentFieldLinkColumnName] == teamRecordPK + ) + .findIndex( + (contentRecord) => + currentContentDisplayRecords.indexOf( + contentRecord[contentObjPK].toString() + ) > -1 + ) > -1 + ) { filter = false; - return filter; + break; + } } + return filter; } /** @@ -2040,7 +2171,8 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { if (parent == null) return; const strategyLink = this.getSettingField("teamStrategy").columnName; const strategyField = this.getSettingField("strategyCode").columnName; - const strategyCode = _rawData[`${strategyLink}__relation`][strategyField]; + const strategyCode = + _rawData[`${strategyLink}__relation`]?.[strategyField]; const hasChild = parent.parentNode.colSpan > 1; const newChild = { name: values[nameField], @@ -2113,7 +2245,9 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { const strategyField = this.getSettingField("strategyCode").columnName; const strategyCode = strategy?.[strategyField]; values[strategyLink] = strategy?.id || values[strategyLink]; - delete values[`${strategyLink}__relation`]; + + // TODO (Guy): Fix the values later. + // delete values[`${strategyLink}__relation`]; isServerSideUpdate && (await this.datacollection.model .update(values.id, values) From 3a7fb097ee3af95155ef166b6701f97a6877cb83 Mon Sep 17 00:00:00 2001 From: nh758 <7259@pm.me> Date: Wed, 8 Jan 2025 14:23:36 +0700 Subject: [PATCH 088/129] apply styles to the data panel --- .../ABViewOrgChartTeamsComponent.js | 89 ++++++++++--- styles/team-widget.css | 125 ++++++++++++++++++ 2 files changed, 195 insertions(+), 19 deletions(-) diff --git a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js index f44fb55c..7462c097 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js @@ -12,6 +12,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { chartDom: "", chartContent: "", dataPanel: "", + dataPanelPopup: "", filterPopup: "", filterForm: "", contentForm: "", @@ -1238,8 +1239,19 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { await this._waitDCReady(dc); } - _showDataPanel() { - this.AB.Webix.ui(this._uiDataPanel(), $$(this.ids.dataPanel)).show(); + _showDataPanel(ev) { + 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, + }); + } + $panel.show(ev.currentTarget, { x: -30, y: -35 }); } _showOrgChart() { @@ -1306,7 +1318,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { header, body: { view: "list", - css: { overflow: "auto", "max-height": "85%" }, + css: { overflow: "auto", "max-height": "90%" }, data: [], }, }); @@ -1317,10 +1329,11 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { body: { view: "list", template: (data) => - `
${panelObj.displayData( + `
${panelObj.displayData( data )}
`, - css: { overflow: "auto", "max-height": "85%" }, + borderless: true, + css: "data-panel-employee-list", data: [], on: { onViewShow() { @@ -1405,16 +1418,40 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { } } return { - id: this.ids.dataPanel, - hidden: true, - view: "tabview", - width: 450, - tabbar: { - height: 60, - type: "bottom", - css: "webix_dark", - }, - cells, + height: 600, + type: "clean", + rows: [ + { + view: "template", + borderless: true, + template: `
+ ${this.label("Staff Assignment")} + +
`, + height: 35, + onClick: { + "data-panel-close": () => { + $$(this.ids.dataPanelPopup).hide(); + return false; + }, + }, + }, + { + id: this.ids.dataPanel, + view: "tabview", + css: "data-panel-tabview", + width: 250, + borderless: true, + tabbar: { + height: 25, + // width: 300, + align: "left", + // type: "bottom", + css: "data-panel-tabbar", + }, + cells, + }, + ], }; } @@ -1460,7 +1497,6 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { }, ], }, - self._uiDataPanel(), ], }).$view; chartDom.appendChild($chartDomComponents); @@ -1470,10 +1506,25 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { $filterButton.innerHTML = ` Filter`; $filterButton.classList.add("filter-button"); $filterButton.onclick = self._fnShowFilterPopup; + const $dataPanelButton = document.createElement("div"); + $dataPanelButton.innerHTML = ` + +
+ ${self.label("Staff Assignment")} + +
`; + $dataPanelButton.classList.add("data-panel-button"); + $dataPanelButton.querySelector(".data-panel-open").onclick = ( + ev + ) => self._showDataPanel(ev); $chartDomComponents.children[0].children[0].children[0].append( - $filterButton + $filterButton, + $dataPanelButton ); - $$(ids.dataPanel).show(); }, }, }, @@ -1799,7 +1850,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { $$(ids.teamFormPopup)?.destructor(); $$(ids.contentForm)?.destructor(); await this.pullData(); - this._showDataPanel(); + // this._showDataPanel(); this._showOrgChart(); this._pageData(); this.ready(); diff --git a/styles/team-widget.css b/styles/team-widget.css index 29bc44f0..41396e81 100644 --- a/styles/team-widget.css +++ b/styles/team-widget.css @@ -262,3 +262,128 @@ org-chart tr.lines .downLine { 50% { background-color: #eee; } 100% { background-color: #ddd} } + +/* -- DATA PANEL -- */ +.data-panel-button { + display: flex; + border-radius: 10px; + background: #fff; + box-shadow: 0px 4px 4px 0px rgba(0, 0, 0, 0.25); + float: right; + padding: 5px; + width: 230px; + margin-right: 25px; +} + +.data-panel-close { + background: #2F27CE; + color: #FFF; + border-radius: 5px; + padding: 4px 5px; + float: right; +} + +.data-panel-close:hover { + background: #FFF; + color: #2F27CE; + border: 1px solid #2F27CE; +} + +.data-panel-employee { + margin-top: 3px; + text-align: center; + border-radius: 8px; + border: 2px solid #868686; + height: 22px; + font-family: Jomhuria; + font-size: 18px; + line-height: 25px; +} + +.data-panel-employee-list .webix_list_item { + border-bottom: unset; + background: transparent; + overflow: auto; + max-height: 95%; +} + +.data-panel-open { + background: #FFF; + color: #2F27CE; + border-radius: 5px; + padding: 4px; + float: right; +} + +.data-panel-open:hover { + background: #2F27CE; + color: #FFF !important; +} + +.data-panel-popup { + border-radius: 10px; + background: #fff; + box-shadow: 0px 4px 4px 0px rgba(0, 0, 0, 0.25); + padding: 10px; +} + +.data-panel-tabbar { + border: solid black; + border-width: 1px 0px 1px 0px !important; + padding: 5px; +} + +.data-panel-tabbar .webix_all_tabs { + float: left; + display: flex; + gap: 10px; + height: 25px; +} + +.data-panel-tabbar .webix_all_tabs .webix_item_tab { + color: #000; + border-radius: 7px; + background: #FFF; + font-family: "Jomhuria", sans-serif; + font-size:20px; + line-height: normal; + box-shadow: none; + border: none; + display: flex; + width: 54px !important; + height: 24px; + justify-content: center; + align-items: center; +} + +.data-panel-tabbar .webix_all_tabs .webix_selected { + color: #FFF; + background: #000 !important; +} + +.data-panel-tabbar .webix_all_tabs .webix_item_tab:hover { + color: #000; + background: rgba(0, 0, 0, 0.20) !important; + box-shadow: none; +} + +.data-panel-tabbar .webix_all_tabs .webix_item_tab:focus { + color: #FFF; + background: #222 !important; + box-shadow: none; +} + +.data-panel-tabview .webix_multiview { + margin-top: 0px !important; +} + +.data-panel-tabview .webix_multiview .webix_list { + height: 530px !important; +} + +/* following a comment here: https://docs.webix.com/api__refs__ui.popup.html + * so that the popup doesn't close. Not sure if we want to do it this way */ +.webix_modal { + width: 0; + height: 0; +} From 6767c64eb06c8d6f89e7dac10c74ac2e8e947d33 Mon Sep 17 00:00:00 2001 From: nh758 <7259@pm.me> Date: Wed, 8 Jan 2025 14:42:23 +0700 Subject: [PATCH 089/129] resize widget when screen changes --- .../platform/views/viewComponent/ABViewOrgChartTeamsComponent.js | 1 + 1 file changed, 1 insertion(+) diff --git a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js index 7462c097..fdeae461 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js @@ -1472,6 +1472,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { { id: ids.chartView, view: "template", + responsive: true, template: `
`, css: { position: "relative", From b8abc55572b6ea205b87cb411ec34f4ccfe0fb75 Mon Sep 17 00:00:00 2001 From: nh758 <7259@pm.me> Date: Wed, 8 Jan 2025 14:59:00 +0700 Subject: [PATCH 090/129] fix populating filter complex ui with in query filters --- AppBuilder/platform/FilterComplex.js | 7 +++++++ 1 file changed, 7 insertions(+) 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") { From f14fbe30ecd6071bd5ebcdb2dc6af0e06f65379a Mon Sep 17 00:00:00 2001 From: nh758 <7259@pm.me> Date: Wed, 8 Jan 2025 14:59:00 +0700 Subject: [PATCH 091/129] fix populating filter complex ui with in query filters --- AppBuilder/platform/FilterComplex.js | 7 +++++++ 1 file changed, 7 insertions(+) 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") { From 45fb6084ee2e5eb20cf6405b72fd6380a53a58d9 Mon Sep 17 00:00:00 2001 From: guyyoo Date: Wed, 8 Jan 2025 20:04:09 +0700 Subject: [PATCH 092/129] Fix the typo bug, End Date validation, form type convertion and warning popup --- .../ABViewOrgChartTeamsComponent.js | 333 +++++++++++++----- styles/orgchart-webcomponents.css | 8 + 2 files changed, 247 insertions(+), 94 deletions(-) diff --git a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js index 5857ffba..44d36f14 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js @@ -536,7 +536,16 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { const fieldName = field.columnName; // TODO (Guy): Add validators. - rules[fieldName] = () => true; + let invalidMessage = ""; + switch (fieldName) { + case contentDateEndFieldColumnName: + invalidMessage = `The ${field.label} must be today or earlier.`; + rules[fieldName] = (value) => value <= new Date(); + break; + default: + rules[fieldName] = () => true; + break; + } const fieldLabel = field.label; const settings = field.settings; switch (fieldKey) { @@ -546,6 +555,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { name: fieldName, label: fieldLabel, labelWidth, + invalidMessage, }; case "number": return { @@ -554,6 +564,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { label: fieldLabel, labelWidth, type: "number", + invalidMessage, }; case "list": return { @@ -567,6 +578,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { id: option.id, value: option.text, })), + invalidMessage, }; case "user": case "connectObject": @@ -580,6 +592,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { label: "Name", disabled: true, labelWidth, + invalidMessage, on: { async onViewShow() { abWebix.extend(this, abWebix.ProgressBar); @@ -642,6 +655,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { label: fieldLabel, disabled: true, labelWidth, + invalidMessage, options: [], on: { onViewShow, @@ -654,6 +668,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { labelWidth, stringResult: false, labelAlign: "left", + invalidMessage, options: [], on: { onViewShow, @@ -666,6 +681,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { name: fieldName, label: fieldLabel, labelWidth, + invalidMessage, timepicker: fieldKey === "datetime", }; case "file": @@ -676,6 +692,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { name: fieldName, label: fieldLabel, labelWidth, + invalidMessage, }; // case "json": // case "LongText": @@ -687,10 +704,12 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { name: fieldName, label: fieldLabel, labelWidth, + invalidMessage, }; } } ); + const Webix = AB.Webix; contentFormElements.push({ view: "button", value: this.label("Save"), @@ -716,78 +735,95 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { $contentForm.$view.remove(); $contentForm.destructor(); if (!isDataChanged) return; - webix - .confirm({ - title: "Warning", - ok: "Yes", - cancel: "No", - text: "You are about to confirm. Are you sure?", - }) - .then(async () => { - this.busy(); - 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"]; - for (const editContentFieldToCreateNew of editContentFieldsToCreateNew) { - const editContentFieldToCreateNewColumnName = - contentObj.fieldByID( - editContentFieldToCreateNew - )?.columnName; - if ( - JSON.stringify( - newFormData[ - editContentFieldToCreateNewColumnName - ] ?? "" - ) !== - JSON.stringify( - contentDataRecord[ - editContentFieldToCreateNewColumnName - ] ?? "" - ) - ) { - const pendingPromises = []; - const oldData = {}; - - oldData[contentDateEndFieldColumnName] = new Date(); - 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 { - 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; + 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"]; + for (const editContentFieldToCreateNew of editContentFieldsToCreateNew) { + const editContentFieldToCreateNewColumnName = + contentObj.fieldByID( + editContentFieldToCreateNew + )?.columnName; + if ( + JSON.stringify( + newFormData[editContentFieldToCreateNewColumnName] ?? "" + ) !== + JSON.stringify( + contentDataRecord[ + editContentFieldToCreateNewColumnName + ] ?? "" + ) + ) { + 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(); + 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 { + 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) { @@ -808,9 +844,31 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { $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 { + 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(); }, }); - AB.Webix.ui({ + Webix.ui({ view: "popup", id: ids.contentForm, close: true, @@ -860,7 +918,9 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { }, }).show(); const $contentFormData = $$(ids.contentFormData); - $contentFormData.setValues(contentDataRecord); + $contentFormData.setValues( + this._convertToFormValueByType(structuredClone(contentDataRecord)) + ); $contentFormData.show(); }; this._fnShowFilterPopup = async (event) => { @@ -995,6 +1055,33 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { })(); } + _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)); @@ -1322,22 +1409,80 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { } _parseFormValueByType(oldFormData, newFormData) { - for (const key in newFormData) { - const oldValue = oldFormData[key]; - const newValue = newFormData[key]; - switch (typeof oldValue) { - case "boolean": - if (newValue == 0) newFormData[key] = false; - else newFormData[key] = true; - break; - case "number": - newFormData[key] = parseInt(newValue); + const contentAllFields = this._contentDC.datasource.fields(); + for (const field of contentAllFields) { + const fieldKey = field.key; + const columnName = field.columnName; + const oldValue = oldFormData[columnName]; + const newValue = newFormData[columnName]; + switch (fieldKey) { + case "date": + case "datetime": + if (oldValue === undefined && newValue == null) + delete newFormData[columnName]; + try { + newValue instanceof Date && + (newFormData[columnName] = newValue.toISOString()); + } catch { + delete newFormData[columnName]; + } break; - case "string": - newFormData[key] = newValue?.toString(); + case "connectObject": + switch (typeof oldValue) { + case "number": + newFormData[columnName] = parseInt(newValue) || null; + break; + default: + newFormData[columnName] = newValue?.toString() || null; + break; + } break; default: - newFormData[key] = newValue; + 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; } } @@ -2071,12 +2216,12 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { .getData( (contentDisplayRecord) => contentDisplayRecord[currentContentDisplayFieldColumnName] - .toString() + ?.toString() .toLowerCase() .indexOf(currentContentDisplayFilterValue) > -1 ) .map((contentDisplayRecord) => - contentDisplayRecord[currentContentDisplayObjPK].toString() + contentDisplayRecord[currentContentDisplayObjPK]?.toString() ); } else if (currentContentDisplayRecords.length > 0) { currentContentDisplayFieldColumnName = AB.definitionByID( @@ -2100,7 +2245,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { ) > -1 ) > -1 : currentContentDisplayRecords.indexOf( - contentDisplayRecordData.toString() + contentDisplayRecordData?.toString() ) > -1; }) .map((contentDisplayRecord) => @@ -2140,13 +2285,15 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { async teamAddChild(values, isServerSideUpdate = true) { const entityDC = this._entityDC; + const teamDC = this.datacollection; + const teamObjID = teamDC.datasource.id; // Add the entity value if (entityDC) { const connection = isServerSideUpdate && entityDC.datasource.connectFields( - (f) => f.settings.linkObject === datacollection.datasource.id + (f) => f.settings.linkObject === teamObjID )[0]; if (connection) { const entity = entityDC.getCursor(); @@ -2157,9 +2304,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { } } const _rawData = - (isServerSideUpdate && - (await this.datacollection.model.create(values))) || - values; + (isServerSideUpdate && (await teamDC.model.create(values))) || values; const id = _rawData.id; const linkField = this.AB.definitionByID( this.getSettingField("teamLink").settings.linkColumn diff --git a/styles/orgchart-webcomponents.css b/styles/orgchart-webcomponents.css index e1c4ab88..d8cd1b19 100644 --- a/styles/orgchart-webcomponents.css +++ b/styles/orgchart-webcomponents.css @@ -515,3 +515,11 @@ org-chart .slide-left { org-chart.l2r .node.slide-left, org-chart.r2l .node.slide-left { left: -40px; } + +.orgchart-teams-edit-content-confirm-popup { + min-width: 450px; +} + +.orgchart-teams-edit-content-confirm-popup .webix_popup_button { + padding: 0px 10px; +} From df70f4959d350e4d971bef45596858fb374becc8 Mon Sep 17 00:00:00 2001 From: guyyoo Date: Thu, 9 Jan 2025 10:38:49 +0700 Subject: [PATCH 093/129] Fix the new data panel list render bugs --- .../views/viewComponent/ABViewOrgChartTeamsComponent.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js index 7ae5365b..3434c6d2 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js @@ -364,7 +364,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { // TODO (Guy): Hardcode data panel DCs for Employee. $$(this.ids.dataPanel) - .getChildViews()[1] + ?.getChildViews()[1] .getChildViews() .forEach(($childView) => $childView.callEvent("onViewShow")); if (isContentDone) resolve(); @@ -1535,6 +1535,10 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { }); } $panel.show(ev.currentTarget, { x: -30, y: -35 }); + $$(this.ids.dataPanel) + .getChildViews()[1] + .getChildViews() + .forEach(($childView) => $childView.callEvent("onViewShow")); } _showOrgChart() { From efea240c36fa3d463b1c88e56629400376664eee Mon Sep 17 00:00:00 2001 From: guyyoo Date: Thu, 9 Jan 2025 11:03:47 +0700 Subject: [PATCH 094/129] Hide inactive data --- .../views/viewComponent/ABViewOrgChartTeamsComponent.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js index 3434c6d2..dced675c 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js @@ -1631,10 +1631,10 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { this.clearAll(); this.define( "data", - // _dataPanelDC.getData() // TODO (Guy): Hardcode Employee DC. _dataPanelDC .getData((panelRecord) => + panelRecord.isinactive !== "T" && header === "Unassigned" ? contentDC.getData( (contentRecord) => From 9062cbd08670f269494c9ab52f52da9c486cead4 Mon Sep 17 00:00:00 2001 From: guyyoo Date: Thu, 9 Jan 2025 12:57:35 +0700 Subject: [PATCH 095/129] Fix empty chartData error, Change data panel filter by tab label -> tab index --- .../ABViewOrgChartTeamsComponent.js | 27 ++++++++++++++----- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js index dced675c..c2b53f65 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js @@ -498,7 +498,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { this._waitDCPending(contentDisplayDC) ), ]); - this.__orgchart.remove(); + this.__orgchart?.remove(); this.__orgchart = null; await this.refresh(); this.ready(); @@ -1546,8 +1546,13 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { 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(this._chartData), + data: AB.cloneDeep(chartData), direction: settings.direction, // depth: settings.depth, chartContainer: `#${ids.chartDom}`, @@ -1583,7 +1588,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { const contentObjID = this._contentDC?.datasource?.id; const cells = []; for (const key in dataPanelDCs) { - const dataPanelDCID = key.split(".")[1]; + const [tabIndex, dataPanelDCID] = key.split("."); // TODO (Guy): Hardcode data panel DCs for Employee. // const _dataPanelDC = _dataPanelDCs.find( @@ -1635,19 +1640,26 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { _dataPanelDC .getData((panelRecord) => panelRecord.isinactive !== "T" && - header === "Unassigned" + tabIndex === "0" ? contentDC.getData( (contentRecord) => contentRecord[ contentLinkedFieldColumnName ] == panelRecord.id )[0] == null - : contentDC.getData( + : tabIndex === "1" + ? contentDC.getData( (contentRecord) => contentRecord[ contentLinkedFieldColumnName ] == panelRecord.id )[0] != null + : _dataPanelDCs + .find( + (dataPanelDC) => + dataPanelDC.id === dataPanelDCID + ) + .getData() ) .sort((a, b) => { if (a.firstName < b.firstName) { @@ -1843,7 +1855,8 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { const _oldOnDragStart = OrgChart.prototype._onDragStart; OrgChart.prototype._onDragStart = (event) => { event.dataTransfer.setData("isnode", 1); - _oldOnDragStart.call(this.__orgchart, event); + this.__orgchart != null && + _oldOnDragStart.call(this.__orgchart, event); }; return OrgChart; })()); @@ -2085,7 +2098,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { const topFromField = dc.getData((e) => e[topNodeColumn] == 1)[0]; topNode = topFromField ? topFromField : topNode; } - if (!topNode) return null; + if (!topNode) return; /** * Recursive function to prepare child node data From a200f5b7521225676a9c3e9933b4a620a25125bc Mon Sep 17 00:00:00 2001 From: guyyoo Date: Thu, 9 Jan 2025 15:33:56 +0700 Subject: [PATCH 096/129] Hot fix for lost data DC --- .../ABViewOrgChartTeamsComponent.js | 75 +++++++++++-------- 1 file changed, 42 insertions(+), 33 deletions(-) diff --git a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js index c2b53f65..7659a3f6 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js @@ -217,6 +217,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { console.log(err); } try { + // TODO (Guy): Logic to not reload dcs. await Promise.all([ this._reloadDCData(this.datacollection), this._reloadDCData(this._contentDC), @@ -333,6 +334,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { console.error(err); } try { + // TODO (Guy): Logic to not reload dcs. await Promise.all([ this._reloadDCData(dc), this._reloadDCData(this._contentDC), @@ -416,6 +418,9 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { 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 @@ -790,6 +795,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { console.error(err); } try { + // TODO (Guy): Logic to not reload dcs. await Promise.all([ this._reloadDCData(teamDC), this._reloadDCData(contentDC), @@ -832,6 +838,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { console.error(err); } try { + // TODO (Guy): Logic to not reload dcs. await Promise.all([ this._reloadDCData(teamDC), this._reloadDCData(contentDC), @@ -855,6 +862,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { console.error(err); } try { + // TODO (Guy): Logic to not reload dcs. await Promise.all([ this._reloadDCData(teamDC), this._reloadDCData(contentDC), @@ -1634,42 +1642,43 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { const contentLinkedFieldColumnName = contentLinkedField.columnName; this.clearAll(); + debugger; this.define( "data", // TODO (Guy): Hardcode Employee DC. - _dataPanelDC - .getData((panelRecord) => - panelRecord.isinactive !== "T" && - tabIndex === "0" - ? contentDC.getData( - (contentRecord) => - contentRecord[ - contentLinkedFieldColumnName - ] == panelRecord.id - )[0] == null - : tabIndex === "1" - ? contentDC.getData( - (contentRecord) => - contentRecord[ - contentLinkedFieldColumnName - ] == panelRecord.id - )[0] != null - : _dataPanelDCs - .find( - (dataPanelDC) => - dataPanelDC.id === dataPanelDCID - ) - .getData() - ) - .sort((a, b) => { - if (a.firstName < b.firstName) { - return -1; - } - if (a.firstName > b.firstName) { - return 1; - } - return 0; - }) + (parseInt(tabIndex) < 2 + ? _dataPanelDC.getData( + (panelRecord) => + panelRecord.isinactive !== "T" && + (tabIndex === "0" + ? contentDC.getData( + (contentRecord) => + contentRecord[ + contentLinkedFieldColumnName + ] == panelRecord.id + )[0] == null + : contentDC.getData( + (contentRecord) => + contentRecord[ + contentLinkedFieldColumnName + ] == panelRecord.id + )[0] != null) + ) + : _dataPanelDCs + .find( + (dataPanelDC) => + dataPanelDC.id === dataPanelDCID + ) + .getData() + ).sort((a, b) => { + if (a.firstName < b.firstName) { + return -1; + } + if (a.firstName > b.firstName) { + return 1; + } + return 0; + }) ); await self._callAfterRender(() => { const $itemElements = From 2c979aa7290862617123d9591e665418f07b9f25 Mon Sep 17 00:00:00 2001 From: nh758 <7259@pm.me> Date: Thu, 9 Jan 2025 15:39:58 +0700 Subject: [PATCH 097/129] sort drop down by label and allow type to filter --- .../views/viewComponent/ABViewDataSelectComponent.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/AppBuilder/platform/views/viewComponent/ABViewDataSelectComponent.js b/AppBuilder/platform/views/viewComponent/ABViewDataSelectComponent.js index 57660190..a68dbaa0 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewDataSelectComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewDataSelectComponent.js @@ -16,7 +16,7 @@ export default class ABViewDataSelectComponent extends ABViewComponent { ui() { const _ui = super.ui([ { - view: "richselect", + view: "combo", id: this.ids.select, on: { onChange: (n, o) => { @@ -41,7 +41,8 @@ export default class ABViewDataSelectComponent extends ABViewComponent { )?.columnName; const options = dc .getData() - .map((o) => ({ id: o.id, value: o[labelField] })); + .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(); From d4d77fd554c79caf2cacda98019e06c53c03a5b2 Mon Sep 17 00:00:00 2001 From: guyyoo Date: Fri, 10 Jan 2025 11:34:57 +0700 Subject: [PATCH 098/129] Fix the chart exist after switch to empty chart entity and the team inactive bugs --- .../ABViewOrgChartTeamsComponent.js | 47 ++++++++++++------- 1 file changed, 31 insertions(+), 16 deletions(-) diff --git a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js index 7659a3f6..ee49fabd 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js @@ -304,7 +304,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { $deleteButton.onclick = () => this.teamDelete(values); $buttons.append($deleteButton); } - if (this.__filters.inactive === 1) { + if (this.__filters.inactive == 1) { const isInactive = data.isInactive; const activeClass = isInactive ? "is-inactive" : "is-active"; const $active = element("div", `team-button ${activeClass}`); @@ -1642,7 +1642,6 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { const contentLinkedFieldColumnName = contentLinkedField.columnName; this.clearAll(); - debugger; this.define( "data", // TODO (Guy): Hardcode Employee DC. @@ -2097,6 +2096,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { * load the data and format it for display */ async pullData() { + this._chartData = null; const dc = this.datacollection; if (dc == null) return; const settings = this.settings; @@ -2123,7 +2123,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { // Don't show inactive teams if ( !childData || - (this.__filters?.inactive !== 1 && + (this.__filters?.inactive == 0 && childData[this.getSettingField("teamInactive").columnName]) ) return; @@ -2381,8 +2381,21 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { values[cName] = entity; } } - const _rawData = - (isServerSideUpdate && (await teamDC.model.create(values))) || values; + let _rawData; + try { + _rawData = + (isServerSideUpdate && (await teamDC.model.create(values))) || + values; + } catch (err) { + // TODO (Guy): The update error. + console.error(err); + } + if ( + this.__filters?.inactive == 0 && + (_rawData == null || + _rawData[this.getSettingField("teamInactive").columnName]) + ) + return; const id = _rawData.id; const linkField = this.AB.definitionByID( this.getSettingField("teamLink").settings.linkColumn @@ -2445,7 +2458,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { return true; } - teamDelete(values) { + teamDelete(values, isServerSideUpdate = true) { if (!this.teamCanDelete(values)) { this.AB.Webix.message({ text: "This team cannot be deleted", @@ -2457,7 +2470,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { return this.AB.Webix.confirm({ text: "This can't be undone, are you sure?", }).then(() => { - this.datacollection.model.delete(values.id); + isServerSideUpdate && this.datacollection.model.delete(values.id); const nodeID = this.teamNodeID(values.id); this.__orgchart.removeNodes(document.querySelector(`#${nodeID}`)); }); @@ -2471,12 +2484,13 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { // TODO (Guy): Fix the values later. // delete values[`${strategyLink}__relation`]; - isServerSideUpdate && - (await this.datacollection.model - .update(values.id, values) - .catch((err) => { - //TODO - })); + try { + isServerSideUpdate && + (await this.datacollection.model.update(values.id, values)); + } catch (err) { + // TODO (Guy): the update error + console.error(err); + } const nodeID = this.teamNodeID(values.id); const node = document.querySelector(`#${nodeID}`); const currentStrategy = node.classList?.value?.match(/strategy-\S+/)[0]; @@ -2487,12 +2501,13 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { node.classList?.add(newStrategy); } - const inactive = this.getSettingField("teamInactive").columnName; // Remove inactive node from display, unless the filter setting to show // inctive nodes is on. - if (this.__filters?.inactive !== 1 && values[inactive] === 1) { + if ( + this.__filters?.inactive == 0 && + values[this.getSettingField("teamInactive").columnName] + ) this.__orgchart.removeNodes(node); - } const nameCol = this.getSettingField("teamName").columnName; node.querySelector(".title").innerHTML = values[nameCol]; } From 4df1b63fc8a20074c8f0e52125ea274bf1a0598b Mon Sep 17 00:00:00 2001 From: nh758 <7259@pm.me> Date: Mon, 13 Jan 2025 16:24:15 +0700 Subject: [PATCH 099/129] fix respond to window resize --- .../ABViewOrgChartTeamsComponent.js | 141 ++++++++++-------- styles/team-widget.css | 16 +- 2 files changed, 88 insertions(+), 69 deletions(-) diff --git a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js index ee49fabd..ce4754c5 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js @@ -10,10 +10,13 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { Object.assign( { chartView: "", - chartDom: "", + // chartDom: "", chartContent: "", + chartHeader: "", dataPanel: "", + dataPanelButton: "", dataPanelPopup: "", + filterButton: "", filterPopup: "", filterForm: "", contentForm: "", @@ -1010,7 +1013,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { }, }); } - $popup.show(event.currentTarget); + $popup.show($$(this.ids.filterButton).$view); }; // Generate strategy css @@ -1530,7 +1533,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { await this._waitDCReady(dc); } - _showDataPanel(ev) { + _showDataPanel() { let $panel = $$(this.ids.dataPanelPopup); if (!$panel) { $panel = this.AB.Webix.ui({ @@ -1542,7 +1545,16 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { modal: true, }); } - $panel.show(ev.currentTarget, { x: -30, y: -35 }); + 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(() => { + $panel.show($dpButtonElem, { x: -30, y: -35 }); + }); + } + this._resizeObserver.observe($dpButtonWebix); + $panel.show($dpButtonElem, { x: -30, y: -35 }); $$(this.ids.dataPanel) .getChildViews()[1] .getChildViews() @@ -1563,7 +1575,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { data: AB.cloneDeep(chartData), direction: settings.direction, // depth: settings.depth, - chartContainer: `#${ids.chartDom}`, + // chartContainer: `#${ids.chartDom}`, pan: true, // settings.pan == 1, zoom: false, // settings.zoom == 1, draggable, @@ -1584,9 +1596,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { orgchart.setAttribute("style", oldOrgchart.getAttribute("style")); oldOrgchart.remove(); } - $$(ids.chartContent).$view.children[0].appendChild( - (this.__orgchart = orgchart) - ); + $$(ids.chartContent).$view.appendChild((this.__orgchart = orgchart)); } _uiDataPanel() { @@ -1731,6 +1741,9 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { onClick: { "data-panel-close": () => { $$(this.ids.dataPanelPopup).hide(); + this._resizeObserver?.unobserve( + $$(this.ids.dataPanelButton).$view + ); return false; }, }, @@ -1790,63 +1803,59 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { const _ui = super.ui([ { id: ids.chartView, - view: "template", + // view: "template", responsive: true, - template: `
`, - css: { - position: "relative", - }, - on: { - onAfterRender() { - Webix.extend(this, Webix.ProgressBar); - const chartDom = document.querySelector(`#${ids.chartDom}`); - chartDom.textContent = ""; - chartDom.innerHTML = ""; - const $chartDomComponents = AB.Webix.ui({ - cols: [ - { - rows: [ - { - view: "template", - height: 50, - }, - { - id: ids.chartContent, - view: "template", - scroll: "auto", - }, - ], + type: "clean", + rows: [ + { + responsive: true, + id: ids.chartHeader, + view: "toolbar", + height: 50, + type: "clean", + cols: [ + { + view: "template", + id: this.ids.filterButton, + template: ``, + align: "left", + onClick: { + "filter-button": (ev) => self._fnShowFilterPopup(ev), }, - ], - }).$view; - chartDom.appendChild($chartDomComponents); - - // Add the filter button to the UI - const $filterButton = document.createElement("button"); - $filterButton.innerHTML = ` Filter`; - $filterButton.classList.add("filter-button"); - $filterButton.onclick = self._fnShowFilterPopup; - const $dataPanelButton = document.createElement("div"); - $dataPanelButton.innerHTML = ` - -
- ${self.label("Staff Assignment")} - -
`; - $dataPanelButton.classList.add("data-panel-button"); - $dataPanelButton.querySelector(".data-panel-open").onclick = ( - ev - ) => self._showDataPanel(ev); - $chartDomComponents.children[0].children[0].children[0].append( - $filterButton, - $dataPanelButton - ); + }, + { + view: "template", + id: this.ids.dataPanelButton, + template: `
+ +
+ ${self.label("Staff Assignment")} + +
+
`, + align: "right", + onClick: { + "data-panel-open": (ev) => self._showDataPanel(ev), + }, + }, + ], }, - }, + { + responsive: true, + id: ids.chartContent, + view: "template", + scroll: "auto", + + on: { + onAfterRender() { + Webix.extend(this, Webix.ProgressBar); + }, + }, + }, + ], }, ]); delete _ui.type; @@ -2698,27 +2707,27 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { closest(el, fn) { return ( el && - (fn(el) && el !== document.querySelector(`#${this.ids.chartDom}`) + (fn(el) && el !== document.querySelector(`#${this.ids.chartContent}`) ? el : this.closest(el.parentNode, fn)) ); } busy() { - const $chartView = $$(this.ids.chartView); + const $chartView = $$(this.ids.chartContent); $chartView.disable(); $chartView.showProgress({ type: "icon" }); } ready() { - const $chartView = $$(this.ids.chartView); + const $chartView = $$(this.ids.chartContent); $chartView.enable(); $chartView.hideProgress(); } }; /** - * Creates a new HTML element with the given type and classes. + * Creates a new HTML element with the given type and classes * @param {string} type - The type of the HTML element to create. * @param {string} classes - A space-separated list of classes to add to the element. * @returns {Element} The newly created HTML element. diff --git a/styles/team-widget.css b/styles/team-widget.css index 41396e81..efa6b740 100644 --- a/styles/team-widget.css +++ b/styles/team-widget.css @@ -275,6 +275,11 @@ org-chart tr.lines .downLine { margin-right: 25px; } +.data-panel-button .fa-users { + display: block; + color: #2F27CE; +} + .data-panel-close { background: #2F27CE; color: #FFF; @@ -310,9 +315,14 @@ org-chart tr.lines .downLine { .data-panel-open { background: #FFF; color: #2F27CE; - border-radius: 5px; - padding: 4px; - float: right; + font-family: "Roboto"; + font-size: 17px; + font-weight: 900; + border-radius: 10px; + float: right !important; + width: 90%; + padding: 5px; + margin-left: 10px; } .data-panel-open:hover { From f66f778311d510a9f772150365fca8b9f003334c Mon Sep 17 00:00:00 2001 From: nh758 <7259@pm.me> Date: Tue, 14 Jan 2025 14:50:15 +0700 Subject: [PATCH 100/129] fix: hide popup when swicthing to another page --- .../views/viewComponent/ABViewOrgChartTeamsComponent.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js index ce4754c5..3968d181 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js @@ -1549,7 +1549,11 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { const $dpButtonElem = $dpButtonWebix.querySelector(".data-panel-button"); // Ensure the popup will stay to the right when resizing if (!this._resizeObserver) { - this._resizeObserver = new 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 }); }); } From 6e43546cc5ad6a2fb68cf444af3a578440fd3a7e Mon Sep 17 00:00:00 2001 From: guyyoo Date: Thu, 16 Jan 2025 13:45:34 +0700 Subject: [PATCH 101/129] UI fixes, optimize things and fix bugs --- .../ABViewOrgChartTeamsComponent.js | 493 ++++++++++++------ styles/team-widget.css | 16 +- 2 files changed, 353 insertions(+), 156 deletions(-) diff --git a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js index 3968d181..6109680e 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js @@ -263,7 +263,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { const $group = element("div", "team-group-section"); $content.appendChild($group); const groupStyle = $group.style; - groupStyle["minHeight"] = `${225 / contentGroupOptionsLength}px`; + groupStyle["minHeight"] = `${325 / contentGroupOptionsLength}px`; // TODO: should this be a config option const groupColor = group.name === "Leader" ? "#003366" : "#DDDDDD"; @@ -301,6 +301,8 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { 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")); @@ -380,7 +382,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { resolve ); }; - (this._fnPageContentGroupCallback = async ( + this._fnPageContentGroupCallback = async ( contentGroupRecords, isContentGroupDone, contentGroupDC, @@ -395,28 +397,28 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { this._fnPageContentGroupCallback, resolve ); - }), - (this._fnPageContentDisplayCallback = ( - contentDisplayRecords, - isContentDisplayDone, - contentDisplayDC, - resolve - ) => { - const contentDC = this._contentDC; - this._fnPageContentCallback( - contentDC.getData(), - true, - contentDC, - () => {} + }; + 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 ); - if (isContentDisplayDone) resolve(); - else - this._callPagingEvent( - contentDisplayDC, - this._fnPageContentDisplayCallback, - resolve - ); - }); + }; this._fnPageData = async (dc, callback, resolve) => { await this._waitDCReady(dc); let records = dc.getData(); @@ -460,7 +462,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { await this.teamAddChild(teamRecord, false); $teamNode = document.getElementById(teamNodeID); if ($teamNode == null) continue; - } else await this.teamEdit(teamRecord, null, false); + } else await this.teamEdit(teamRecord, false); const contentRecords = contentDC.getData( (contentRecord) => teamRecord[contentFieldColumnName].indexOf( @@ -525,7 +527,8 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { "contentFieldDateEnd" )?.columnName; const contentDataRecord = JSON.parse( - event.currentTarget.dataset.source + event.currentTarget.dataset.source || + event.currentTarget.parentElement.dataset.source ); const rules = {}; const labelWidth = 200; @@ -640,7 +643,6 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { abWebix.extend(this, abWebix.ProgressBar); this.showProgress({ type: "icon" }); try { - // TODO (Guy): Add spinner. this.define( "options", (await fieldLinkObj.model().findAll()).data.map( @@ -726,24 +728,17 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { click: async () => { const $contentFormData = $$(ids.contentFormData); if (!$contentFormData.validate()) return; - let isDataChanged = false; const newFormData = this._parseFormValueByType( + contentObj, contentDataRecord, $contentFormData.getValues() ); - for (const key in newFormData) - if ( - JSON.stringify(newFormData[key]) !== - JSON.stringify(contentDataRecord[key]) - ) { - isDataChanged = true; - break; - } const $contentForm = $$(ids.contentForm); $contentForm.blockEvent(); $contentForm.$view.remove(); $contentForm.destructor(); - if (!isDataChanged) return; + if (!this._checkDataIsChanged(contentDataRecord, newFormData)) + return; const teamDC = this.datacollection; const contentDC = this._contentDC; const dataID = newFormData.id; @@ -1099,7 +1094,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { $ui.setAttribute("id", this.contentNodeID(data.id)); $ui.setAttribute("data-source", JSON.stringify(data)); $ui.style.borderColor = color; - $ui.addEventListener("click", this._fnShowContentForm); + $ui.addEventListener("dblclick", this._fnShowContentForm); if (this.settings.draggable === 1) { $ui.setAttribute("draggable", "true"); $ui.addEventListener("dragstart", this._fnContentDragStart); @@ -1322,6 +1317,10 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { } !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; } @@ -1344,6 +1343,14 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { 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; + } + _initDC(dc) { dc.init(); if (dc.dataStatus === dc.dataStatusFlag.notInitial) dc.loadData(); @@ -1420,12 +1427,12 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { ); } - _parseFormValueByType(oldFormData, newFormData) { - const contentAllFields = this._contentDC.datasource.fields(); - for (const field of contentAllFields) { + _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 oldValue = oldFormData?.[columnName]; const newValue = newFormData[columnName]; switch (fieldKey) { case "date": @@ -1440,14 +1447,23 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { } break; case "connectObject": - switch (typeof oldValue) { - case "number": - newFormData[columnName] = parseInt(newValue) || null; - break; - default: - newFormData[columnName] = newValue?.toString() || null; - break; - } + delete newFormData[`${columnName}__relation`]; + if (field.linkType() === "one") + switch (typeof oldValue) { + case "number": + newFormData[columnName] = parseInt(newValue) || null; + break; + default: + newFormData[columnName] = newValue?.toString() || null; + if ( + oldValue === undefined && + newFormData[columnName] == null + ) + delete newFormData[columnName]; + 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 === "") @@ -2093,6 +2109,16 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { this._resolveInit(); } + getChartData(id, chartData = this._chartData) { + if (this.teamNodeID(id) === chartData.id) return chartData; + if (chartData.children?.length > 0) { + for (let child of chartData.children) { + child = this.getChartData(id, child); + if (child != null) return child; + } + } + } + async onShow() { this.busy(); this.AB.performance.mark("TeamChart.onShow"); @@ -2374,9 +2400,10 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { return this.AB.definitionByID(this.settings[setting]); } - async teamAddChild(values, isServerSideUpdate = true) { + async teamAddChild(values, isServerSideUpdate = true, children = []) { const entityDC = this._entityDC; const teamDC = this.datacollection; + const teamObj = teamDC.datasource; const teamObjID = teamDC.datasource.id; // Add the entity value @@ -2394,14 +2421,16 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { values[cName] = entity; } } - let _rawData; - try { - _rawData = - (isServerSideUpdate && (await teamDC.model.create(values))) || - values; - } catch (err) { - // TODO (Guy): The update error. - console.error(err); + let _rawData = values; + if (isServerSideUpdate) { + this.busy(); + try { + _rawData = await teamDC.model.create(values); + } catch (err) { + // TODO (Guy): The update error. + console.error(err); + } + this.ready(); } if ( this.__filters?.inactive == 0 && @@ -2409,37 +2438,62 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { _rawData[this.getSettingField("teamInactive").columnName]) ) return; - const id = _rawData.id; - const linkField = this.AB.definitionByID( - this.getSettingField("teamLink").settings.linkColumn - ).columnName; - const nameField = this.getSettingField("teamName").columnName; const parent = document.querySelector( - `#${this.teamNodeID(values[linkField])}` + `#${this.teamNodeID( + _rawData[ + this.AB.definitionByID( + this.getSettingField("teamLink").settings.linkColumn + ).columnName + ] + )}` ); if (parent == null) return; - const strategyLink = this.getSettingField("teamStrategy").columnName; - const strategyField = this.getSettingField("strategyCode").columnName; - const strategyCode = - _rawData[`${strategyLink}__relation`]?.[strategyField]; const hasChild = parent.parentNode.colSpan > 1; + const teamID = _rawData.id; const newChild = { - name: values[nameField], - id: this.teamNodeID(id), + name: _rawData[this.getSettingField("teamName").columnName], + filteredOut: false, + isInactive: _rawData[this.getSettingField("teamInactive").columnName], + id: this.teamNodeID(teamID), relationship: hasChild ? "110" : "100", - className: `strategy-${strategyCode}`, + className: `strategy-${ + _rawData[ + `${this.getSettingField("teamStrategy").columnName}__relation` + ]?.[this.getSettingField("strategyCode").columnName] + }`, _rawData, }; + newChild.filteredOut = this.filterTeam(newChild); // Need to add differently if the node already has child nodes - if (hasChild) { - const sibling = this.closest(parent, (el) => el.nodeName === "TABLE") - .querySelector(".nodes") - .querySelector(".node"); - this.__orgchart.addSiblings(sibling, { siblings: [newChild] }); - } else { - this.__orgchart.addChildren(parent, { children: [newChild] }); - } + if (hasChild) + this.__orgchart.addSiblings( + // Sibling + this.closest(parent, (el) => el.nodeName === "TABLE") + .querySelector(".nodes") + .querySelector(".node"), + { siblings: [newChild] } + ); + else this.__orgchart.addChildren(parent, { children: [newChild] }); + + // TODO(Guy): Render assignment for specific node later. + // const contentDC = this._contentDC; + // const contentFieldLinkColumnName = teamObj.fieldByID( + // this.settings.contentField + // ).fieldLink.columnName; + // await this._callAfterRender(async () => { + // this._fnPageContentCallback( + // contentDC.getData((e) => e[contentFieldLinkColumnName] == teamID), + // true, + // contentDC, + // () => {} + // ); + // await Promise.all( + // children.map((child) => + // this.teamAddChild(child._rawData, false, child.children) + // ) + // ); + // }); } teamCanInactivate(values) { @@ -2480,95 +2534,137 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { }); return; } - return this.AB.Webix.confirm({ - text: "This can't be undone, are you sure?", - }).then(() => { - isServerSideUpdate && this.datacollection.model.delete(values.id); - const nodeID = this.teamNodeID(values.id); - this.__orgchart.removeNodes(document.querySelector(`#${nodeID}`)); - }); + const nodeID = this.teamNodeID(values.id); + const $teamNode = document.querySelector(`#${nodeID}`); + if ($teamNode.querySelectorAll(".team-group-record").length > 0) + this.AB.Webix.alert({ + text: this.label( + "Since there are assignments or teams associated with this team, this action cannot be done until all of its assignments are made inactive" + ), + }); + else + this.AB.Webix.confirm({ + text: this.label( + "This will permanently remove this team. Click OK to continue or Cancel to not remove the team." + ), + }).then(() => { + isServerSideUpdate && this.datacollection.model.delete(values.id); + this.__orgchart.removeNodes(document.querySelector(`#${nodeID}`)); + }); } - async teamEdit(values, strategy, isServerSideUpdate = true) { - const strategyLink = this.getSettingField("teamStrategy").columnName; - const strategyField = this.getSettingField("strategyCode").columnName; - const strategyCode = strategy?.[strategyField]; - values[strategyLink] = strategy?.id || values[strategyLink]; - - // TODO (Guy): Fix the values later. - // delete values[`${strategyLink}__relation`]; - try { - isServerSideUpdate && - (await this.datacollection.model.update(values.id, values)); - } catch (err) { - // TODO (Guy): the update error - console.error(err); - } - const nodeID = this.teamNodeID(values.id); - const node = document.querySelector(`#${nodeID}`); - const currentStrategy = node.classList?.value?.match(/strategy-\S+/)[0]; - const newStrategy = - (strategyCode && `strategy-${strategyCode}`) || currentStrategy; - if (currentStrategy !== newStrategy) { - node.classList?.remove(currentStrategy); - node.classList?.add(newStrategy); + async teamEdit(values, isServerSideUpdate = true) { + let _rawData = values; + if (isServerSideUpdate) { + this.busy(); + try { + _rawData = await this.datacollection.model.update( + values.id, + values + ); + } catch (err) { + // TODO (Guy): the update error + console.error(err); + } + this.ready(); } + const $node = document.querySelector(`#${this.teamNodeID(_rawData.id)}`); // Remove inactive node from display, unless the filter setting to show // inctive nodes is on. if ( this.__filters?.inactive == 0 && - values[this.getSettingField("teamInactive").columnName] - ) - this.__orgchart.removeNodes(node); - const nameCol = this.getSettingField("teamName").columnName; - node.querySelector(".title").innerHTML = values[nameCol]; + _rawData[this.getSettingField("teamInactive").columnName] + ) { + this.__orgchart.removeNodes($node); + return; + } + const oldChartData = JSON.parse($node.dataset.source); + const linkFieldColumnName = this.datacollection.datasource.fieldByID( + this.getSettingField("teamLink").settings.linkColumn + ).columnName; + if ( + oldChartData._rawData[linkFieldColumnName] != + _rawData[linkFieldColumnName] + ) { + // TODO (Guy): Fix refresh the only updated node and those children and assignments later. + await this.refresh(); + // this.__orgchart.removeNodes($node); + // this.teamAddChild( + // _rawData, + // false, + // this.getChartData(_rawData.id)?.children + // ); + return; + } + const currentStrategy = $node.classList?.value?.match(/strategy-\S+/)[0]; + const strategyCode = + _rawData[ + `${this.getSettingField("teamStrategy").columnName}__relation` + ]?.[this.getSettingField("strategyCode").columnName]; + const newStrategy = + (strategyCode && `strategy-${strategyCode}`) || currentStrategy; + if (currentStrategy !== newStrategy) { + $node.classList?.remove(currentStrategy); + $node.classList?.add(newStrategy); + } + const teamName = _rawData[this.getSettingField("teamName").columnName]; + $node.querySelector(".title").innerHTML = teamName; + const newChartData = { + className: newStrategy, + filteredOut: false, + id: this.teamNodeID(_rawData.id), + isInactive: _rawData[this.getSettingField("teamInactive").columnName], + name: teamName, + relationship: oldChartData.relationship, + _rawData, + }; + newChartData.filteredOut = this.filterTeam(newChartData); + $node.dataset.source = JSON.stringify(newChartData); } async teamForm(mode, values) { let $teamFormPopup = $$(this.ids.teamFormPopup); - const inactive = this.getSettingField("teamInactive").columnName; - const linkField = this.AB.definitionByID( - this.getSettingField("teamLink").settings.linkColumn - ).columnName; const ids = this.ids; if (!$teamFormPopup) { const teamObj = this.datacollection.datasource; const settings = this.settings; const nameField = teamObj.fieldByID(settings.teamName); + const linkField = teamObj.fieldByID( + this.getSettingField("teamLink").settings.linkColumn + ); + const entityDC = this._entityDC; + const entityObjID = entityDC.datasource.id; + const entityDCCursorID = entityDC.getCursor().id; const strategyField = teamObj.fieldByID(settings.teamStrategy); const strategyObj = this.AB.objectByID( strategyField.settings.linkObject ); - const entityDC = this._entityDC; - const entityLink = strategyObj.connectFields( - (f) => f.settings.linkObject === entityDC.datasource.id - )[0]; - const cond = { + const teamCond = { glue: "and", rules: [ { - key: entityLink.columnName, - value: entityDC.getCursor().id, + key: teamObj.connectFields( + (f) => f.settings.linkObject === entityObjID + )[0].columnName, + value: entityDCCursorID, rule: "equals", }, ], }; - const subCol = this.getSettingField("subStrategy").columnName; - this.entitySrategyOptions = await strategyField.getOptions( - cond, - null, - null, - null, - [subCol] - ); - - const strategyOptions = this.entitySrategyOptions.map((o) => { - return { - id: o.id, - value: o[`${subCol}__relation`].name, - }; - }); + const strategyCond = { + glue: "and", + rules: [ + { + key: strategyObj.connectFields( + (f) => f.settings.linkObject === entityObjID + )[0].columnName, + value: entityDCCursorID, + rule: "equals", + }, + ], + }; + const subStrategyCol = this.getSettingField("subStrategy").columnName; $teamFormPopup = webix.ui({ view: "popup", id: ids.teamFormPopup, @@ -2599,6 +2695,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { { view: "form", id: ids.teamForm, + hidden: true, borderless: true, elements: [ { @@ -2611,33 +2708,114 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { view: "richselect", label: strategyField.label, name: strategyField.columnName, - options: strategyOptions, + options: [], required: true, + on: { + async onViewShow() { + webix.extend(this, webix.ProgressBar); + this.showProgress({ type: "icon" }); + try { + this.disable(); + this.define( + "options", + ( + await strategyField.getOptions( + strategyCond, + null, + null, + null, + [subStrategyCol] + ) + ).map((e) => ({ + id: e.id, + value: e[ + `${subStrategyCol}__relation` + ].name, + // value: strategyObj.displayData(e), + })) + ); + this.refresh(); + this.enable(); + this.hideProgress(); + } catch { + // Close popup before response or possily response fail + } + }, + }, + }, + { + view: "combo", + label: linkField.label, + name: linkField.columnName, + options: [], + required: true, + on: { + async onViewShow() { + webix.extend(this, webix.ProgressBar); + this.showProgress({ type: "icon" }); + try { + this.disable(); + this.define( + "options", + ( + await linkField.getOptions(teamCond) + ).map((e) => ({ + id: e.id, + value: teamObj.displayData(e), + })) + ); + this.refresh(); + this.enable(); + this.hideProgress(); + } catch { + // Close popup before response or possily response fail + } + }, + }, }, { view: "switch", id: ids.teamFormInactive, - name: inactive, + name: this.getSettingField("teamInactive") + .columnName, label: "Inactive", }, { view: "text", name: "id", hidden: true }, - { view: "text", name: linkField, hidden: true }, { id: ids.teamFormSubmit, view: "button", value: this.label("Save"), disabled: true, css: "webix_primary", - click: () => { - const values = $$(ids.teamForm).getValues(); - const strategy = strategyOptions.find( - (f) => - f.id === values[strategyField.columnName] - ); - if (values.id) { - this.teamEdit(values, strategy); + click: async () => { + let newValues = $$(ids.teamForm).getValues(); + if (newValues.id) { + const $node = document.getElementById( + this.teamNodeID(newValues.id) + ); + const oldValues = JSON.parse( + $node.dataset.source + )._rawData; + newValues = this._parseFormValueByType( + teamObj, + oldValues, + newValues + ); + if ( + !this._checkDataIsChanged( + oldValues, + newValues + ) + ) + return; + this.teamEdit(newValues); } else { - this.teamAddChild(values, strategy); + newValues = this._parseFormValueByType( + teamObj, + null, + newValues + ); + this.teamAddChild(newValues); } $teamFormPopup.hide(); }, @@ -2648,7 +2826,8 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { const values = $$(ids.teamForm).getValues(); const valid = !!values[strategyField.columnName] && - !!values[nameField.columnName]; + !!values[nameField.columnName] && + !!values[linkField.columnName]; const $teamFormSubmit = $$(ids.teamFormSubmit); if (valid) $teamFormSubmit.enable(); else $teamFormSubmit.disable(); @@ -2657,6 +2836,14 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { }, ], }, + on: { + onShow() { + $$(ids.teamForm).show(); + }, + onHide() { + $$(ids.teamForm).hide(); + }, + }, }); } if (values.__parentID) { diff --git a/styles/team-widget.css b/styles/team-widget.css index efa6b740..4877596d 100644 --- a/styles/team-widget.css +++ b/styles/team-widget.css @@ -5,7 +5,7 @@ org-chart .node { border: 2px dashed transparent; text-align: center; justify-content: center; - width: 325px !important; + width: 450px !important; font-family: "Jomhuria", sans-serif; font-style: normal; font-weight: 200; @@ -38,7 +38,7 @@ org-chart .node .content { position: relative; top: -15px; width: 100%; - min-height: 225px; + min-height: 325px; border-radius: 0 0 15px 15px; display: flex; flex-direction: column; @@ -98,7 +98,7 @@ org-chart tr.lines .downLine { flex-direction: row; align-items: center; justify-content: center; - gap: 8%; + gap: 5%; } .team-group-record:hover { @@ -397,3 +397,13 @@ org-chart tr.lines .downLine { width: 0; height: 0; } + +org-chart .team-group-record > .team-group-record-edit-icon { + cursor: pointer; + display: none; + padding: 4px; +} + +org-chart .team-group-record:hover > .team-group-record-edit-icon { + display: block; +} From d729452305efeb44f34467a7b2883ec8005a9378 Mon Sep 17 00:00:00 2001 From: guyyoo Date: Fri, 17 Jan 2025 09:13:16 +0700 Subject: [PATCH 102/129] Sort the data panel by lastname and convert to lowercase before sort --- .../viewComponent/ABViewOrgChartTeamsComponent.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js index 6109680e..72b4963d 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js @@ -1700,10 +1700,16 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { ) .getData() ).sort((a, b) => { - if (a.firstName < b.firstName) { + if ( + a.lastName.toLowerCase() < + b.lastName.toLowerCase() + ) { return -1; } - if (a.firstName > b.firstName) { + if ( + a.lastName.toLowerCase() > + b.lastName.toLowerCase() + ) { return 1; } return 0; @@ -2201,7 +2207,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { } else { // sort children alphaetically node.children = node.children.sort((a, b) => - a.name > b.name ? 1 : -1 + a.name.toLowerCase() > b.name.toLowerCase() ? 1 : -1 ); } }; From fa3786975e4465c3b16d9c163000d45d31f86cc1 Mon Sep 17 00:00:00 2001 From: guyyoo Date: Fri, 17 Jan 2025 09:26:24 +0700 Subject: [PATCH 103/129] Fix the bug missing the param --- .../viewComponent/ABViewOrgChartTeamsComponent.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js index 72b4963d..e7776cd3 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js @@ -2630,15 +2630,15 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { } async teamForm(mode, values) { - let $teamFormPopup = $$(this.ids.teamFormPopup); + const teamObj = this.datacollection.datasource; + const linkField = teamObj.fieldByID( + this.getSettingField("teamLink").settings.linkColumn + ); const ids = this.ids; + let $teamFormPopup = $$(ids.teamFormPopup); if (!$teamFormPopup) { - const teamObj = this.datacollection.datasource; const settings = this.settings; const nameField = teamObj.fieldByID(settings.teamName); - const linkField = teamObj.fieldByID( - this.getSettingField("teamLink").settings.linkColumn - ); const entityDC = this._entityDC; const entityObjID = entityDC.datasource.id; const entityDCCursorID = entityDC.getCursor().id; From 26d8f36029decb4aceb5a704b754612208491431 Mon Sep 17 00:00:00 2001 From: guyyoo Date: Fri, 17 Jan 2025 10:08:35 +0700 Subject: [PATCH 104/129] Hide a trash can when there is at least one assignment --- .../views/viewComponent/ABViewOrgChartTeamsComponent.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js index e7776cd3..380aacaa 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js @@ -1032,6 +1032,10 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { const contentGroupDataPK = contentRecord[this.getSettingField("contentGroupByField").columnName]; const contentGroupPKField = contentGroupDC.datasource.PK(); + + // Hide a trash can when there is at least one assignment. + const $trashCan = $teamNode.querySelectorAll(".team-button").item(2); + $trashCan && ($trashCan.style.display = "none"); if ( contentGroupDC.getData( (e) => e[contentGroupPKField] == contentGroupDataPK From 5ca61489ed362e165d8527fd853f48567932e90b Mon Sep 17 00:00:00 2001 From: guyyoo Date: Fri, 17 Jan 2025 11:07:20 +0700 Subject: [PATCH 105/129] Can not inactive if there is an assignment on the team --- .../views/viewComponent/ABViewOrgChartTeamsComponent.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js index 380aacaa..30466309 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js @@ -2520,8 +2520,12 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { ) ) return false; - // @TODO check for active assignment - // if (hasActiveAssignment) return false; + if ( + document + .getElementById(this.teamNodeID(values.id)) + .querySelectorAll(".team-group-record").length > 0 + ) + return false; return true; } From b7a7144a4189aa24e794801e974a4fc8a618c4f8 Mon Sep 17 00:00:00 2001 From: nh758 <7259@pm.me> Date: Thu, 16 Jan 2025 16:48:28 +0700 Subject: [PATCH 106/129] when drag from employee list don't end existing assignment --- AppBuilder/core | 2 +- .../ABViewOrgChartTeamsComponent.js | 26 ++++++++++--------- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/AppBuilder/core b/AppBuilder/core index 621864a4..a45f2151 160000 --- a/AppBuilder/core +++ b/AppBuilder/core @@ -1 +1 @@ -Subproject commit 621864a47ee17a6ee68891fda4ee1996502ce57a +Subproject commit a45f21514a1be50256f217d60491847438f52af6 diff --git a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js index 6109680e..50f5d650 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js @@ -131,18 +131,20 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { contentObj.fieldByID(contentLinkedFieldID).columnName; const pendingPromises = []; const newDate = new Date(); - const $contentRecords = - document.getElementsByClassName("team-group-record"); - for (const $contentRecord of $contentRecords) { - const contentData = JSON.parse($contentRecord.dataset.source); - if (contentData[contentLinkedFieldColumnName] == dataPK) { - contentData[contentDateEndFieldColumnName] = newDate; - pendingPromises.push( - contentModel.update(contentData.id, contentData) - ); - draggedNodes.push($contentRecord); - } - } + // Employee can have multiple assignments, so don't close + // existing + // const $contentRecords = + // document.getElementsByClassName("team-group-record"); + // for (const $contentRecord of $contentRecords) { + // const contentData = JSON.parse($contentRecord.dataset.source); + // if (contentData[contentLinkedFieldColumnName] == dataPK) { + // contentData[contentDateEndFieldColumnName] = newDate; + // pendingPromises.push( + // contentModel.update(contentData.id, contentData) + // ); + // draggedNodes.push($contentRecord); + // } + // } updatedData = {}; updatedData[contentDateStartFieldColumnName] = newDate; updatedData[contentLinkedFieldColumnName] = From dc1591bd9ccc26e83ac9a80c94cd86c09e517230 Mon Sep 17 00:00:00 2001 From: guyyoo Date: Fri, 17 Jan 2025 14:37:45 +0700 Subject: [PATCH 107/129] Fix conection field render and hardcode for the Role Type filter --- .../ABViewOrgChartTeamsComponent.js | 196 +++++++++++++----- 1 file changed, 141 insertions(+), 55 deletions(-) diff --git a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js index 30466309..855d62fe 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js @@ -933,7 +933,8 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { this._fnShowFilterPopup = async (event) => { const contentDisplayedFieldFilters = this.settings.contentDisplayedFieldFilters; - let $popup = $$(this.ids.filterPopup); + const ids = this.ids; + let $popup = $$(ids.filterPopup); if (!$popup) { const strategyID = this.getSettingField("teamStrategy").settings.linkObject; @@ -942,73 +943,158 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { const strategyCodeField = strategyObj.fields( (f) => f.id === strategyCodeFieldID )[0]; - const strategyOptions = await strategyCodeField.getOptions(); - $popup = webix.ui({ view: "popup", css: "filter-popup", - id: this.ids.filterPopup, + id: ids.filterPopup, body: { - view: "form", - borderless: true, - id: this.ids.filterForm, - elements: [ - { - view: "text", - label: this.label("Team Name"), - labelWidth: 90, - name: "teamName", - clear: true, - }, - { - view: "combo", - label: this.label("Strategy"), - labelWidth: 90, - options: strategyOptions.map(fieldToOption), - name: "strategy", - clear: "replace", - }, + rows: [ { - view: "checkbox", - name: "inactive", - labelRight: this.label("Show Inactive Teams"), - labelWidth: 0, - }, - ...(() => { - const contentDisplayedFieldFilterViews = []; - for (const contentDisplayedFieldFilterKey in contentDisplayedFieldFilters) { - if ( - contentDisplayedFieldFilterKey.split(".")[3] == 1 - ) { - contentDisplayedFieldFilterViews.push({ - view: "text", - label: contentDisplayedFieldFilters[ - contentDisplayedFieldFilterKey - ], - labelWidth: 90, - name: contentDisplayedFieldFilterKey, - clear: true, - }); - } - } - return contentDisplayedFieldFilterViews; - })(), - { - cols: [ - {}, + view: "form", + borderless: true, + hidden: true, + id: ids.filterForm, + elements: [ { - view: "icon", - icon: "fa fa-check", - css: "filter-apply", - click: () => this.filterApply(), + 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 strategyCodeField.getOptions() + ).map(fieldToOption) + ); + 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($$(this.ids.filterButton).$view); + $popup.show($$(ids.filterButton).$view); }; // Generate strategy css From 95bc31d059cb8cbac410c68cf2f998ec7a55b678 Mon Sep 17 00:00:00 2001 From: guyyoo Date: Fri, 17 Jan 2025 16:40:57 +0700 Subject: [PATCH 108/129] Fix the bug that the add/edit team popup is alway the first time created mode --- .../ABViewOrgChartTeamsComponent.js | 414 +++++++++--------- 1 file changed, 202 insertions(+), 212 deletions(-) diff --git a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js index 855d62fe..a21807e8 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js @@ -25,7 +25,6 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { teamFormPopup: "", teamFormSubmit: "", teamFormTitle: "", - teamFormInactive: "", }, ids ) @@ -2725,241 +2724,232 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { async teamForm(mode, values) { const teamObj = this.datacollection.datasource; + const ids = this.ids; + const settings = this.settings; + const nameField = teamObj.fieldByID(settings.teamName); + const entityDC = this._entityDC; + const entityObjID = entityDC.datasource.id; + const entityDCCursorID = entityDC.getCursor().id; + const strategyField = teamObj.fieldByID(settings.teamStrategy); + const strategyObj = this.AB.objectByID(strategyField.settings.linkObject); const linkField = teamObj.fieldByID( this.getSettingField("teamLink").settings.linkColumn ); - const ids = this.ids; - let $teamFormPopup = $$(ids.teamFormPopup); - if (!$teamFormPopup) { - const settings = this.settings; - const nameField = teamObj.fieldByID(settings.teamName); - const entityDC = this._entityDC; - const entityObjID = entityDC.datasource.id; - const entityDCCursorID = entityDC.getCursor().id; - const strategyField = teamObj.fieldByID(settings.teamStrategy); - const strategyObj = this.AB.objectByID( - strategyField.settings.linkObject - ); - const teamCond = { - glue: "and", - rules: [ + const linkFieldColumnName = linkField.columnName; + const isEditMode = mode === "Edit"; + const teamCond = { + glue: "and", + rules: [ + { + key: teamObj.connectFields( + (f) => f.settings.linkObject === entityObjID + )[0].columnName, + value: entityDCCursorID, + rule: "equals", + }, + ], + }; + const strategyCond = { + glue: "and", + rules: [ + { + key: strategyObj.connectFields( + (f) => f.settings.linkObject === entityObjID + )[0].columnName, + value: entityDCCursorID, + rule: "equals", + }, + ], + }; + const subStrategyCol = this.getSettingField("subStrategy").columnName; + const $teamFormPopup = webix.ui({ + view: "popup", + id: ids.teamFormPopup, + close: true, + position: "center", + css: { "border-radius": "10px" }, + body: { + rows: [ { - key: teamObj.connectFields( - (f) => f.settings.linkObject === entityObjID - )[0].columnName, - value: entityDCCursorID, - rule: "equals", + view: "toolbar", + css: "webix_dark", + cols: [ + { width: 5 }, + { + view: "label", + label: `${this.label(mode)} Team`, + align: "left", + }, + { + view: "icon", + icon: "fa fa-times", + align: "right", + width: 60, + click: () => { + $teamFormPopup.blockEvent(); + $teamFormPopup.$view.remove(); + $teamFormPopup.destructor(); + }, + }, + ], }, - ], - }; - const strategyCond = { - glue: "and", - rules: [ { - key: strategyObj.connectFields( - (f) => f.settings.linkObject === entityObjID - )[0].columnName, - value: entityDCCursorID, - rule: "equals", - }, - ], - }; - const subStrategyCol = this.getSettingField("subStrategy").columnName; - $teamFormPopup = webix.ui({ - view: "popup", - id: ids.teamFormPopup, - close: true, - position: "center", - css: { "border-radius": "10px" }, - body: { - rows: [ - { - view: "toolbar", - css: "webix_dark", - cols: [ - { width: 5 }, - { - id: ids.teamFormTitle, - view: "label", - align: "left", - }, - { - view: "icon", - icon: "fa fa-times", - align: "right", - width: 60, - click: () => $teamFormPopup.hide(), - }, - ], - }, - { - view: "form", - id: ids.teamForm, - hidden: true, - borderless: true, - elements: [ - { - view: "text", - label: nameField.label, - name: nameField.columnName, - required: true, - }, - { - view: "richselect", - label: strategyField.label, - name: strategyField.columnName, - options: [], - required: true, - on: { - async onViewShow() { - webix.extend(this, webix.ProgressBar); - this.showProgress({ type: "icon" }); - try { - this.disable(); - this.define( - "options", - ( - await strategyField.getOptions( - strategyCond, - null, - null, - null, - [subStrategyCol] - ) - ).map((e) => ({ - id: e.id, - value: e[ - `${subStrategyCol}__relation` - ].name, - // value: strategyObj.displayData(e), - })) - ); - this.refresh(); - this.enable(); - this.hideProgress(); - } catch { - // Close popup before response or possily response fail - } - }, + view: "form", + id: ids.teamForm, + hidden: true, + borderless: true, + elements: [ + { + view: "text", + label: nameField.label, + name: nameField.columnName, + required: true, + }, + { + view: "richselect", + label: strategyField.label, + name: strategyField.columnName, + options: [], + required: true, + on: { + async onViewShow() { + webix.extend(this, webix.ProgressBar); + this.showProgress({ type: "icon" }); + try { + this.disable(); + this.define( + "options", + ( + await strategyField.getOptions( + strategyCond, + null, + null, + null, + [subStrategyCol] + ) + ).map((e) => ({ + id: e.id, + value: e[`${subStrategyCol}__relation`] + .name, + // value: strategyObj.displayData(e), + })) + ); + this.refresh(); + this.enable(); + this.hideProgress(); + } catch { + // Close popup before response or possily response fail + } }, }, - { - view: "combo", - label: linkField.label, - name: linkField.columnName, - options: [], - required: true, - on: { - async onViewShow() { - webix.extend(this, webix.ProgressBar); - this.showProgress({ type: "icon" }); - try { - this.disable(); - this.define( - "options", - ( - await linkField.getOptions(teamCond) - ).map((e) => ({ + }, + { + view: "combo", + label: linkField.label, + name: linkFieldColumnName, + options: [], + required: true, + on: { + async onViewShow() { + webix.extend(this, webix.ProgressBar); + this.showProgress({ type: "icon" }); + try { + this.disable(); + this.define( + "options", + (await linkField.getOptions(teamCond)).map( + (e) => ({ id: e.id, value: teamObj.displayData(e), - })) - ); - this.refresh(); - this.enable(); - this.hideProgress(); - } catch { - // Close popup before response or possily response fail - } - }, - }, - }, - { - view: "switch", - id: ids.teamFormInactive, - name: this.getSettingField("teamInactive") - .columnName, - label: "Inactive", - }, - { view: "text", name: "id", hidden: true }, - { - id: ids.teamFormSubmit, - view: "button", - value: this.label("Save"), - disabled: true, - css: "webix_primary", - click: async () => { - let newValues = $$(ids.teamForm).getValues(); - if (newValues.id) { - const $node = document.getElementById( - this.teamNodeID(newValues.id) - ); - const oldValues = JSON.parse( - $node.dataset.source - )._rawData; - newValues = this._parseFormValueByType( - teamObj, - oldValues, - newValues - ); - if ( - !this._checkDataIsChanged( - oldValues, - newValues + }) ) - ) - return; - this.teamEdit(newValues); - } else { - newValues = this._parseFormValueByType( - teamObj, - null, - newValues ); - this.teamAddChild(newValues); + this.refresh(); + isEditMode && this.enable(); + this.hideProgress(); + } catch { + // Close popup before response or possily response fail } - $teamFormPopup.hide(); }, }, - ], - on: { - onChange: () => { - const values = $$(ids.teamForm).getValues(); - const valid = - !!values[strategyField.columnName] && - !!values[nameField.columnName] && - !!values[linkField.columnName]; - const $teamFormSubmit = $$(ids.teamFormSubmit); - if (valid) $teamFormSubmit.enable(); - else $teamFormSubmit.disable(); + }, + { + view: "switch", + disabled: !this.teamCanInactivate(values), + name: this.getSettingField("teamInactive").columnName, + label: "Inactive", + }, + { view: "text", name: "id", hidden: true }, + { + id: ids.teamFormSubmit, + view: "button", + value: this.label("Save"), + disabled: true, + css: "webix_primary", + click: async () => { + let newValues = $$(ids.teamForm).getValues(); + if (newValues.id) { + const $node = document.getElementById( + this.teamNodeID(newValues.id) + ); + const oldValues = JSON.parse( + $node.dataset.source + )._rawData; + newValues = this._parseFormValueByType( + teamObj, + oldValues, + newValues + ); + if ( + !this._checkDataIsChanged(oldValues, newValues) + ) + return; + this.teamEdit(newValues); + } else { + newValues = this._parseFormValueByType( + teamObj, + null, + newValues + ); + this.teamAddChild(newValues); + } + $teamFormPopup.blockEvent(); + $teamFormPopup.$view.remove(); + $teamFormPopup.destructor(); }, }, + ], + on: { + onChange: () => { + const values = $$(ids.teamForm).getValues(); + let valid = + !!values[strategyField.columnName] && + !!values[nameField.columnName]; + if (isEditMode) + valid = valid && !!values[linkFieldColumnName]; + const $teamFormSubmit = $$(ids.teamFormSubmit); + if (valid) $teamFormSubmit.enable(); + else $teamFormSubmit.disable(); + }, }, - ], - }, - on: { - onShow() { - $$(ids.teamForm).show(); - }, - onHide() { - $$(ids.teamForm).hide(); }, + ], + }, + on: { + onShow() { + $$(ids.teamForm).show(); }, - }); - } + onHide() { + this.$view.remove(); + this.destructor(); + }, + }, + }); if (values.__parentID) { - values[linkField] = values.__parentID; + values[linkFieldColumnName] = values.__parentID; delete values.__parentID; } - $$(ids.teamFormTitle).setValue(`${this.label(mode)} Team`); $$(ids.teamForm).setValues(values); - $$(ids.teamFormSubmit).disable(); - - this.teamCanInactivate(values) - ? $$(ids.teamFormInactive).enable() - : $$(ids.teamFormInactive).disable(); - if (mode === "Edit") { - // Check if we can inactivate - } $teamFormPopup.show(); } From 524f64d5f4c60c8d2a1177c1ef496ef3a80e7d3b Mon Sep 17 00:00:00 2001 From: guyyoo Date: Fri, 17 Jan 2025 17:33:52 +0700 Subject: [PATCH 109/129] Validate End Date > Start Date --- .../ABViewOrgChartTeamsComponent.js | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js index a21807e8..975d82bc 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js @@ -519,9 +519,12 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { const settings = this.settings; const editContentFieldsToCreateNew = settings.editContentFieldsToCreateNew; - const contentDateStartFieldColumnName = this.getSettingField( - "contentFieldDateStart" - )?.columnName; + const contentDateStartField = contentObj.fields( + (field) => field.id === settings.contentFieldDateStart + )[0]; + const contentDateStartFieldLabel = contentDateStartField?.label; + const contentDateStartFieldColumnName = + contentDateStartField?.columnName; const contentDateEndFieldColumnName = this.getSettingField( "contentFieldDateEnd" )?.columnName; @@ -550,8 +553,12 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { let invalidMessage = ""; switch (fieldName) { case contentDateEndFieldColumnName: - invalidMessage = `The ${field.label} must be today or earlier.`; - rules[fieldName] = (value) => value <= new Date(); + invalidMessage = `The ${field.label} must be later than the ${contentDateStartFieldLabel}.`; + rules[fieldName] = (value) => + value > + $$(ids.contentFormData).getValues()[ + contentDateStartFieldColumnName + ]; break; default: rules[fieldName] = () => true; From 0d09a91bd5fdc5cb38406cf29540d370df926b5c Mon Sep 17 00:00:00 2001 From: guyyoo Date: Mon, 20 Jan 2025 10:43:59 +0700 Subject: [PATCH 110/129] Fix sort --- .../ABViewOrgChartTeamsComponent.js | 53 ++++++++----------- 1 file changed, 22 insertions(+), 31 deletions(-) diff --git a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js index 975d82bc..09286067 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js @@ -1795,21 +1795,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { dataPanelDC.id === dataPanelDCID ) .getData() - ).sort((a, b) => { - if ( - a.lastName.toLowerCase() < - b.lastName.toLowerCase() - ) { - return -1; - } - if ( - a.lastName.toLowerCase() > - b.lastName.toLowerCase() - ) { - return 1; - } - return 0; - }) + ).sort(sort) ); await self._callAfterRender(() => { const $itemElements = @@ -2302,9 +2288,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { delete node.children; } else { // sort children alphaetically - node.children = node.children.sort((a, b) => - a.name.toLowerCase() > b.name.toLowerCase() ? 1 : -1 - ); + node.children = node.children.sort(sort); } }; const chartData = (this._chartData = { @@ -2323,14 +2307,23 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { pullChildData(chartData); } - async refresh() { + async refresh(force = true) { const ids = this.ids; $$(ids.teamFormPopup)?.destructor(); $$(ids.contentForm)?.destructor(); await this.pullData(); // this._showDataPanel(); this._showOrgChart(); - this._pageData(); + (force && this._pageData()) || + (await this._callAfterRender(() => { + const contentDC = this._contentDC; + this._fnPageContentCallback( + contentDC.getData(), + true, + contentDC, + () => {} + ); + })); } async filterApply() { @@ -2341,17 +2334,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { this.__filters = $$(ids.filterForm).getValues(); this.__orgchart?.remove(); this.__orgchart = null; - await this.pullData(); - this._showOrgChart(); - await this._callAfterRender(() => { - const contentDC = this._contentDC; - this._fnPageContentCallback( - contentDC.getData(), - true, - contentDC, - () => {} - ); - }); + await this.refresh(false); this.ready(); } @@ -2577,6 +2560,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { { siblings: [newChild] } ); else this.__orgchart.addChildren(parent, { children: [newChild] }); + await this.refresh(false); // TODO(Guy): Render assignment for specific node later. // const contentDC = this._contentDC; @@ -3032,3 +3016,10 @@ function fieldToOption(f) { value: f.text, }; } + +function sort(a, b) { + return (a.lastName ?? a.name).toLowerCase() > + (b.lastName ?? b.name).toLowerCase() + ? 1 + : -1; +} From 5b6f17741ad0e20d2a7bf072491101755162101b Mon Sep 17 00:00:00 2001 From: guyyoo Date: Mon, 20 Jan 2025 11:39:11 +0700 Subject: [PATCH 111/129] Fix the refresh param --- .../views/viewComponent/ABViewOrgChartTeamsComponent.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js index 5cc5d87e..03f75fdd 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js @@ -2562,7 +2562,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { { siblings: [newChild] } ); else this.__orgchart.addChildren(parent, { children: [newChild] }); - await this.refresh(false); + await this.refresh(isServerSideUpdate); // TODO(Guy): Render assignment for specific node later. // const contentDC = this._contentDC; @@ -2680,7 +2680,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { _rawData[linkFieldColumnName] ) { // TODO (Guy): Fix refresh the only updated node and those children and assignments later. - await this.refresh(); + await this.refresh(isServerSideUpdate); // this.__orgchart.removeNodes($node); // this.teamAddChild( // _rawData, @@ -2713,6 +2713,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { }; newChartData.filteredOut = this.filterTeam(newChartData); $node.dataset.source = JSON.stringify(newChartData); + isServerSideUpdate && await this.refresh(isServerSideUpdate); } async teamForm(mode, values) { From 4edc0a43cdf2591b0c4ba086857b42722eb1749f Mon Sep 17 00:00:00 2001 From: guyyoo Date: Tue, 21 Jan 2025 12:41:37 +0700 Subject: [PATCH 112/129] Fix the bug the data panal record does not exist while paging --- .../viewComponent/ABViewOrgChartTeamsComponent.js | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js index 03f75fdd..a13d0014 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js @@ -1811,13 +1811,15 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { "data-content-linked-field-id", contentFieldID ); + const dataPanelRecord = _dataPanelDC.getData( + (e) => + e.id == + $itemElement.getAttribute("webix_l_id") + )[0]; + if (dataPanelRecord == null) continue; $itemElement.setAttribute( "data-pk", - _dataPanelDC.getData( - (e) => - e.id == - $itemElement.getAttribute("webix_l_id") - )[0][panelObj.PK()] + dataPanelRecord[panelObj.PK()] ); $itemElement.setAttribute("draggable", "true"); $itemElement.addEventListener( @@ -2713,7 +2715,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { }; newChartData.filteredOut = this.filterTeam(newChartData); $node.dataset.source = JSON.stringify(newChartData); - isServerSideUpdate && await this.refresh(isServerSideUpdate); + isServerSideUpdate && (await this.refresh(isServerSideUpdate)); } async teamForm(mode, values) { From 0664f710dafa9973e93c5ed9f868a00555f81201 Mon Sep 17 00:00:00 2001 From: guyyoo Date: Tue, 21 Jan 2025 14:49:02 +0700 Subject: [PATCH 113/129] Fix the team delete logic if there are any assignments ever --- .../ABViewOrgChartTeamsComponent.js | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js index a13d0014..0d2729ae 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js @@ -1126,10 +1126,6 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { const contentGroupDataPK = contentRecord[this.getSettingField("contentGroupByField").columnName]; const contentGroupPKField = contentGroupDC.datasource.PK(); - - // Hide a trash can when there is at least one assignment. - const $trashCan = $teamNode.querySelectorAll(".team-button").item(2); - $trashCan && ($trashCan.style.display = "none"); if ( contentGroupDC.getData( (e) => e[contentGroupPKField] == contentGroupDataPK @@ -2610,13 +2606,12 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { } teamCanDelete(values) { - const canInactive = this.getSettingField("teamCanInactivate").columnName; - if (!values[canInactive]) return false; - const children = this.getSettingField("teamLink").columnName; - if (values[children].length > 0) return false; - // @TODO check for any assignment - // if (hasAssignment) return false; - return true; + return ( + values[this.getSettingField("teamCanInactivate").columnName] && + // TODO (Guy): Should not save "many" value in the future. Let's fix later. + values[this.getSettingField("teamLink").columnName].length === 0 && + values[this.getSettingField("contentField").columnName].length === 0 + ); } teamDelete(values, isServerSideUpdate = true) { From 53fa0c50e76a5c8da723d91bed0f6e0e275f802b Mon Sep 17 00:00:00 2001 From: guyyoo Date: Wed, 22 Jan 2025 14:45:48 +0700 Subject: [PATCH 114/129] Adust assignment layouts --- .../platform/views/viewComponent/ABViewOrgChartTeamsComponent.js | 1 + styles/team-widget.css | 1 + 2 files changed, 2 insertions(+) diff --git a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js index 0d2729ae..5d9e174d 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js @@ -1201,6 +1201,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { element("div", "display-block"), element("div", "display-block display-block-right"), ]; + hardcodedDisplays[1].style.width = "40%"; const $hardcodedSpecialDisplay = element( "div", "team-group-record-display" diff --git a/styles/team-widget.css b/styles/team-widget.css index 4877596d..0ab767b7 100644 --- a/styles/team-widget.css +++ b/styles/team-widget.css @@ -88,6 +88,7 @@ org-chart tr.lines .downLine { } .team-group-record { + padding: 5px; width: 100%; border-width: 4px; border-style: solid; From f8ffd8866db2e1fba08a65239ecf576f46a6560f Mon Sep 17 00:00:00 2001 From: guyyoo Date: Wed, 22 Jan 2025 15:03:28 +0700 Subject: [PATCH 115/129] Allow blank date validation --- .../views/viewComponent/ABViewOrgChartTeamsComponent.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js index 5d9e174d..6d1c4248 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js @@ -558,9 +558,9 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { invalidMessage = `The ${field.label} must be later than the ${contentDateStartFieldLabel}.`; rules[fieldName] = (value) => value > - $$(ids.contentFormData).getValues()[ - contentDateStartFieldColumnName - ]; + $$(ids.contentFormData).getValues()[ + contentDateStartFieldColumnName + ] || value == null; break; default: rules[fieldName] = () => true; From eabf2baf51cc3c0d26b1a0cd2acf5bd6b17856c2 Mon Sep 17 00:00:00 2001 From: guyyoo Date: Wed, 22 Jan 2025 15:41:51 +0700 Subject: [PATCH 116/129] Only show Job Title + container when there is one --- .../ABViewOrgChartTeamsComponent.js | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js index 6d1c4248..8248f89f 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js @@ -1253,7 +1253,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { 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) { @@ -1261,6 +1261,19 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { 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: @@ -1275,7 +1288,6 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { break; } currentDisplayIndex++; - const displayedFieldColumnName = displayedField.columnName; const contentDisplayedFieldTypePrefix = `${displayedFieldKey}.${displayedFieldID}`; const contentDisplayedFieldMappingDataObj = JSON.parse( From 985871a1fb30a897c3ab8fba6974cdecd4fabcf8 Mon Sep 17 00:00:00 2001 From: guyyoo Date: Wed, 22 Jan 2025 15:57:00 +0700 Subject: [PATCH 117/129] Prevent creating assignment for the same staff on the same team --- .../ABViewOrgChartTeamsComponent.js | 29 ++++++++++--------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js index 8248f89f..305cdea4 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js @@ -113,9 +113,10 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { if (contentFieldLinkColumnName == null) return; this.busy(); const $group = event.currentTarget; + const $content = $group.parentElement; const newGroupDataPK = $group.dataset.pk; const newNodeDataPK = JSON.parse( - $group.parentElement.parentElement.dataset.source + $content.parentElement.dataset.source )._rawData[nodeObjPK]; let { source: updatedData, @@ -130,20 +131,20 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { contentObj.fieldByID(contentLinkedFieldID).columnName; const pendingPromises = []; const newDate = new Date(); - // Employee can have multiple assignments, so don't close + // Employee can have multiple assignments but not the same team, so don't close // existing - // const $contentRecords = - // document.getElementsByClassName("team-group-record"); - // for (const $contentRecord of $contentRecords) { - // const contentData = JSON.parse($contentRecord.dataset.source); - // if (contentData[contentLinkedFieldColumnName] == dataPK) { - // contentData[contentDateEndFieldColumnName] = newDate; - // pendingPromises.push( - // contentModel.update(contentData.id, contentData) - // ); - // draggedNodes.push($contentRecord); - // } - // } + const $contentRecords = + $content.getElementsByClassName("team-group-record"); + for (const $contentRecord of $contentRecords) { + const contentData = JSON.parse($contentRecord.dataset.source); + if (contentData[contentLinkedFieldColumnName] == dataPK) { + contentData[contentDateEndFieldColumnName] = newDate; + pendingPromises.push( + contentModel.update(contentData.id, contentData) + ); + draggedNodes.push($contentRecord); + } + } updatedData = {}; updatedData[contentDateStartFieldColumnName] = newDate; updatedData[contentLinkedFieldColumnName] = From fdb798cee19c391bfff8903e72c9265e269050ff Mon Sep 17 00:00:00 2001 From: guyyoo Date: Wed, 22 Jan 2025 17:42:37 +0700 Subject: [PATCH 118/129] Fix last_updated_by field for teams and assignments --- .../ABViewOrgChartTeamsComponent.js | 39 +++++++++++++------ styles/team-widget.css | 2 +- 2 files changed, 29 insertions(+), 12 deletions(-) diff --git a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js index 305cdea4..38f81356 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js @@ -115,9 +115,8 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { const $group = event.currentTarget; const $content = $group.parentElement; const newGroupDataPK = $group.dataset.pk; - const newNodeDataPK = JSON.parse( - $content.parentElement.dataset.source - )._rawData[nodeObjPK]; + const newNodeDataPK = JSON.parse($content.parentElement.dataset.source) + ._rawData[nodeObjPK]; let { source: updatedData, pk: dataPK, @@ -134,9 +133,10 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { // Employee can have multiple assignments but not the same team, so don't close // existing const $contentRecords = - $content.getElementsByClassName("team-group-record"); + $content.getElementsByClassName("team-group-record"); for (const $contentRecord of $contentRecords) { const contentData = JSON.parse($contentRecord.dataset.source); + this._setUpdatedBy(contentObj, contentData); if (contentData[contentLinkedFieldColumnName] == dataPK) { contentData[contentDateEndFieldColumnName] = newDate; pendingPromises.push( @@ -164,6 +164,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { entityDC.getCursor() ); } + this._setUpdatedBy(contentObj, updatedData); pendingPromises.push( contentModel.create(updatedData), (async () => { @@ -194,6 +195,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { delete updatedData["created_at"]; delete updatedData["updated_at"]; delete updatedData["properties"]; + this._setUpdatedBy(contentObj, updatedData); if (dropContentToCreate) { const pendingPromises = []; @@ -334,6 +336,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { ] = 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) { @@ -757,6 +760,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { delete newFormData["created_at"]; delete newFormData["updated_at"]; delete newFormData["properties"]; + this._setUpdatedBy(contentObj, newFormData); for (const editContentFieldToCreateNew of editContentFieldsToCreateNew) { const editContentFieldToCreateNewColumnName = contentObj.fieldByID( @@ -786,6 +790,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { const oldData = {}; oldData[contentDateEndFieldColumnName] = new Date(); + this._setUpdatedBy(contentObj, oldData); pendingPromises.push( contentModel.update(dataID, oldData) ); @@ -1202,7 +1207,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { element("div", "display-block"), element("div", "display-block display-block-right"), ]; - hardcodedDisplays[1].style.width = "40%"; + hardcodedDisplays[1].style.width = "50%"; const $hardcodedSpecialDisplay = element( "div", "team-group-record-display" @@ -1657,6 +1662,17 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { 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) { @@ -2503,7 +2519,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { const entityDC = this._entityDC; const teamDC = this.datacollection; const teamObj = teamDC.datasource; - const teamObjID = teamDC.datasource.id; + const teamObjID = teamObj.id; // Add the entity value if (entityDC) { @@ -2523,6 +2539,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { let _rawData = values; if (isServerSideUpdate) { this.busy(); + this._setUpdatedBy(teamObj, values); try { _rawData = await teamDC.model.create(values); } catch (err) { @@ -2658,13 +2675,13 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { async teamEdit(values, isServerSideUpdate = true) { let _rawData = values; + const teamDC = this.datacollection; + const teamObj = teamDC.datasource; if (isServerSideUpdate) { this.busy(); + this._setUpdatedBy(teamObj, values); try { - _rawData = await this.datacollection.model.update( - values.id, - values - ); + _rawData = await teamDC.model.update(values.id, values); } catch (err) { // TODO (Guy): the update error console.error(err); @@ -2683,7 +2700,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { return; } const oldChartData = JSON.parse($node.dataset.source); - const linkFieldColumnName = this.datacollection.datasource.fieldByID( + const linkFieldColumnName = teamObj.fieldByID( this.getSettingField("teamLink").settings.linkColumn ).columnName; if ( diff --git a/styles/team-widget.css b/styles/team-widget.css index 0ab767b7..bd4caf75 100644 --- a/styles/team-widget.css +++ b/styles/team-widget.css @@ -99,7 +99,7 @@ org-chart tr.lines .downLine { flex-direction: row; align-items: center; justify-content: center; - gap: 5%; + gap: 2%; } .team-group-record:hover { From cd025237d0e4f40f2b1cf84c0a30549cea822eb5 Mon Sep 17 00:00:00 2001 From: guyyoo Date: Thu, 23 Jan 2025 17:06:33 +0700 Subject: [PATCH 119/129] Prevent drop assignment with same employees from team to team --- .../ABViewOrgChartTeamsComponent.js | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js index 38f81356..d24c4a97 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js @@ -130,6 +130,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { 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 = @@ -181,6 +182,25 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { 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) + if ( + JSON.parse($contentRecord.dataset.source)[ + dataPanelLinkedFieldColumnName + ] == updatedData[dataPanelLinkedFieldColumnName] + ) { + this.ready(); + return; + } + } // This is move form another team node // Move the child node to the target From 6d291fb4f876c442cd388a51ed25cd83542e0a46 Mon Sep 17 00:00:00 2001 From: guyyoo Date: Fri, 24 Jan 2025 11:27:22 +0700 Subject: [PATCH 120/129] Fix bug dropping assignment to leader/member in the same team --- .../ABViewOrgChartTeamsComponent.js | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js index d24c4a97..c0aa51b6 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js @@ -191,15 +191,19 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { (connectField) => connectField.datasourceLink === contentObj )[0].columnName; - for (const $contentRecord of $contentRecords) + for (const $contentRecord of $contentRecords) { + const contentRecord = JSON.parse( + $contentRecord.dataset.source + ); if ( - JSON.parse($contentRecord.dataset.source)[ - dataPanelLinkedFieldColumnName - ] == updatedData[dataPanelLinkedFieldColumnName] + updatedData.id != contentRecord.id && + contentRecord[dataPanelLinkedFieldColumnName] == + updatedData[dataPanelLinkedFieldColumnName] ) { this.ready(); return; } + } } // This is move form another team node @@ -207,6 +211,13 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { const $draggedNode = document.querySelector( `#${this.contentNodeID(updatedData.id)}` ); + if ( + updatedData[contentGroupByFieldColumnName] == + $group.dataset.pk + ) { + this.ready(); + return; + } $draggedNode.parentNode.removeChild($draggedNode); $group .querySelector(".team-group-content") From 3261ab82efbf990f5494d29478b6293a15941eac Mon Sep 17 00:00:00 2001 From: guyyoo Date: Fri, 24 Jan 2025 11:36:28 +0700 Subject: [PATCH 121/129] Add more condition when drop to the same team --- .../ABViewOrgChartTeamsComponent.js | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js index c0aa51b6..73176386 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js @@ -196,9 +196,12 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { $contentRecord.dataset.source ); if ( - updatedData.id != contentRecord.id && - contentRecord[dataPanelLinkedFieldColumnName] == - updatedData[dataPanelLinkedFieldColumnName] + (updatedData.id != contentRecord.id && + contentRecord[dataPanelLinkedFieldColumnName] == + updatedData[dataPanelLinkedFieldColumnName]) || + (updatedData.id == contentRecord.id && + updatedData[contentGroupByFieldColumnName] == + $group.dataset.pk) ) { this.ready(); return; @@ -211,13 +214,6 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { const $draggedNode = document.querySelector( `#${this.contentNodeID(updatedData.id)}` ); - if ( - updatedData[contentGroupByFieldColumnName] == - $group.dataset.pk - ) { - this.ready(); - return; - } $draggedNode.parentNode.removeChild($draggedNode); $group .querySelector(".team-group-content") From 1d301fa7d8c7e786485a6981bc0c0bd2f901142f Mon Sep 17 00:00:00 2001 From: guyyoo Date: Wed, 5 Feb 2025 13:08:32 +0700 Subject: [PATCH 122/129] Fix the connected entity field for strategy --- .../viewComponent/ABViewOrgChartTeamsComponent.js | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js index 73176386..eea902b8 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js @@ -2798,18 +2798,6 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { }, ], }; - const strategyCond = { - glue: "and", - rules: [ - { - key: strategyObj.connectFields( - (f) => f.settings.linkObject === entityObjID - )[0].columnName, - value: entityDCCursorID, - rule: "equals", - }, - ], - }; const subStrategyCol = this.getSettingField("subStrategy").columnName; const $teamFormPopup = webix.ui({ view: "popup", @@ -2870,7 +2858,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { "options", ( await strategyField.getOptions( - strategyCond, + null, null, null, null, From 9707ea2523bfd96270adc5e264c51e16c0d89606 Mon Sep 17 00:00:00 2001 From: guyyoo Date: Wed, 5 Feb 2025 15:17:53 +0700 Subject: [PATCH 123/129] Limit JobTitle text --- .../views/viewComponent/ABViewOrgChartTeamsComponent.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js index eea902b8..cb687975 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js @@ -1417,6 +1417,11 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { ) ); } + + // TODO (Guy): Hardcode limit text. + if (currentDisplayIndex - 1 === 1) + $currentDisplay.textContent = + $currentDisplay.textContent.slice(0, 35); break; } currentField = null; From 4f5186bfceba8b0f0ee99cb61b4e5159993793d0 Mon Sep 17 00:00:00 2001 From: nh758 <7259@pm.me> Date: Thu, 6 Feb 2025 13:50:56 +0700 Subject: [PATCH 124/129] Refactor team strategy input as 2 drop downs --- .../ABViewOrgChartTeamsComponent.js | 137 ++++++++++++++---- 1 file changed, 109 insertions(+), 28 deletions(-) diff --git a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js index eea902b8..5353cf9e 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js @@ -22,7 +22,9 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { contentForm: "", contentFormData: "", teamForm: "", + teamFormCode: "", teamFormPopup: "", + teamFormStrategy: "", teamFormSubmit: "", teamFormTitle: "", }, @@ -977,13 +979,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { const ids = this.ids; let $popup = $$(ids.filterPopup); if (!$popup) { - const strategyID = - this.getSettingField("teamStrategy").settings.linkObject; - const strategyObj = this.AB.objectByID(strategyID); - const strategyCodeFieldID = this.getSettingField("strategyCode").id; - const strategyCodeField = strategyObj.fields( - (f) => f.id === strategyCodeFieldID - )[0]; + const self = this; $popup = webix.ui({ view: "popup", css: "filter-popup", @@ -1017,9 +1013,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { try { this.define( "options", - ( - await strategyCodeField.getOptions() - ).map(fieldToOption) + await self.strategyCodeOptions() ); this.refresh(); this.enable(); @@ -2780,7 +2774,6 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { const entityObjID = entityDC.datasource.id; const entityDCCursorID = entityDC.getCursor().id; const strategyField = teamObj.fieldByID(settings.teamStrategy); - const strategyObj = this.AB.objectByID(strategyField.settings.linkObject); const linkField = teamObj.fieldByID( this.getSettingField("teamLink").settings.linkColumn ); @@ -2798,12 +2791,14 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { }, ], }; - const subStrategyCol = this.getSettingField("subStrategy").columnName; + const self = this; + const labelWidth = 110; const $teamFormPopup = webix.ui({ view: "popup", id: ids.teamFormPopup, close: true, position: "center", + width: 400, css: { "border-radius": "10px" }, body: { rows: [ @@ -2839,13 +2834,17 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { { view: "text", label: nameField.label, + labelWidth, name: nameField.columnName, required: true, }, { view: "richselect", - label: strategyField.label, - name: strategyField.columnName, + label: this.label("Strategy"), + labelWidth, + id: this.ids.teamFormCode, + // no name because we don't actually save this, it's + // used to filter strategyField options options: [], required: true, on: { @@ -2856,20 +2855,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { this.disable(); this.define( "options", - ( - await strategyField.getOptions( - null, - null, - null, - null, - [subStrategyCol] - ) - ).map((e) => ({ - id: e.id, - value: e[`${subStrategyCol}__relation`] - .name, - // value: strategyObj.displayData(e), - })) + await self.strategyCodeOptions() ); this.refresh(); this.enable(); @@ -2878,11 +2864,54 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { // Close popup before response or possily response fail } }, + async onChange(code, previous) { + if (code === previous) return; + const opts = await self.strategyOptions(code); + const $strategyField = $$( + self.ids.teamFormStrategy + ); + $strategyField.define?.("options", opts); + $strategyField.refresh(); + $strategyField.enable(); + }, + }, + }, + { + view: "richselect", + label: this.label("Sub Strategy"), + labelWidth, + name: strategyField.columnName, + id: this.ids.teamFormStrategy, + options: [], + required: true, + on: { + async onViewShow() { + webix.extend(this, webix.ProgressBar); + this.showProgress({ type: "icon" }); + try { + this.disable(); + const value = this.getValue(); + if (value) { + const { code } = ( + await self.strategyOptions() + ).find((o) => o.id === value); + const options = self.strategyOptions(code); + $$(self.ids.teamFormCode).setValue(code); + this.define("options", options); + this.refresh(); + this.enable(); + } + this.hideProgress(); + } catch { + // Close popup before response or possily response fail + } + }, }, }, { view: "combo", label: linkField.label, + labelWidth, name: linkFieldColumnName, options: [], required: true, @@ -3000,6 +3029,58 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { return `contentnode_${id}`; } + /** + * Get valid drop down option for strategyCode + * @returns {array} + */ + async strategyCodeOptions() { + // These shouldn't change often so cache them to prevent extra requests + // to NetSuite + if (!this._strategyCodeOpts) { + const strategyID = + this.getSettingField("teamStrategy").settings.linkObject; + const strategyObj = this.AB.objectByID(strategyID); + const strategyCodeFieldID = this.getSettingField("strategyCode").id; + const strategyCodeField = strategyObj.fields( + (f) => f.id === strategyCodeFieldID + )[0]; + + const opts = await strategyCodeField.getOptions(); + this._strategyCodeOpts = opts.map(fieldToOption).sort(); + } + return this._strategyCodeOpts; + } + + /** + * Get valid drop down option for teamStrategy based on strategyCode + * @param {string} code the id of a strategyCod + * @returns {array} + */ + async strategyOptions(code) { + // These shouldn't change often so cache instead of querying Netsuite each + // time + if (!this._strategyOpts) { + const teamObj = this.datacollection.datasource; + const strategyField = teamObj.fieldByID(this.settings.teamStrategy); + const subStrategyCol = this.getSettingField("subStrategy").columnName; + const strategyCodeCol = + this.getSettingField("strategyCode").columnName; + const opts = await strategyField.getOptions(null, null, null, null, [ + subStrategyCol, + ]); + this._strategyOpts = opts + .map((e) => ({ + id: e.id, + value: e[`${subStrategyCol}__relation`].name, + code: e[strategyCodeCol], + })) + .sort(); + } + return code + ? this._strategyOpts.filter((o) => o.code === code) + : this._strategyOpts; + } + /** * generate a id for the team dom node based on it's record id * @param {string} id record id From 06a06828b0300fcba036b03fe545654d1653316d Mon Sep 17 00:00:00 2001 From: guyyoo Date: Thu, 6 Feb 2025 17:52:20 +0700 Subject: [PATCH 125/129] Prevent refresh if no changes and day condition when drop node --- .../ABViewOrgChartTeamsComponent.js | 119 ++++++++++++------ 1 file changed, 82 insertions(+), 37 deletions(-) diff --git a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js index cb687975..bddb627b 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js @@ -123,6 +123,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { contentLinkedFieldID, } = JSON.parse(dataTransfer.getData("text/plain")); const draggedNodes = []; + let isRefreshed = true; try { if (!updatedData) { // This is a drop from Employee list (new assignment) @@ -135,50 +136,80 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { // existing const $contentRecords = $content.getElementsByClassName("team-group-record"); + let isUpdated = false; for (const $contentRecord of $contentRecords) { const contentData = JSON.parse($contentRecord.dataset.source); - this._setUpdatedBy(contentObj, contentData); 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, contentData) + ); + draggedNodes.push($contentRecord); + isRefreshed = true; + isUpdated = true; + continue; + } + } contentData[contentDateEndFieldColumnName] = newDate; pendingPromises.push( contentModel.update(contentData.id, contentData) ); draggedNodes.push($contentRecord); + isRefreshed = true; } } - 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() + 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(updatedData), + (async () => { + const $draggedNode = await this._createUIContentRecord( + updatedData, + "grey" + ); + $group + .querySelector(".team-group-content") + .appendChild($draggedNode); + draggedNodes.push($draggedNode); + })() ); } - this._setUpdatedBy(contentObj, updatedData); - pendingPromises.push( - contentModel.create(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); @@ -223,7 +254,17 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { delete updatedData["updated_at"]; delete updatedData["properties"]; this._setUpdatedBy(contentObj, updatedData); - if (dropContentToCreate) { + if ( + !dropContentToCreate || + (updatedData[contentFieldLinkColumnName] == newNodeDataPK && + this._isLessThanDay( + new Date(updatedData[contentDateStartFieldColumnName]) + )) + ) { + updatedData[contentFieldLinkColumnName] = newNodeDataPK; + updatedData[contentGroupByFieldColumnName] = newGroupDataPK; + await contentModel.update(updatedData.id, updatedData); + } else { const pendingPromises = []; // TODO (Guy): Force update Date End with a current date. @@ -240,16 +281,16 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { updatedData[contentGroupByFieldColumnName] = newGroupDataPK; pendingPromises.push(contentModel.create(updatedData)); await Promise.all(pendingPromises); - } else { - updatedData[contentFieldLinkColumnName] = newNodeDataPK; - updatedData[contentGroupByFieldColumnName] = newGroupDataPK; - await contentModel.update(updatedData.id, updatedData); } } } 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([ @@ -1496,6 +1537,10 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { return false; } + _isLessThanDay(date) { + return Math.abs(new Date() - date) / 36e5 < 24; + } + _initDC(dc) { dc.init(); if (dc.dataStatus === dc.dataStatusFlag.notInitial) dc.loadData(); From 309c6c6df8d2ede1d23a0dc780cbc5a153cb3aea Mon Sep 17 00:00:00 2001 From: guyyoo Date: Fri, 7 Feb 2025 12:05:59 +0700 Subject: [PATCH 126/129] Update by blank/null or within 24 hours conditions --- .../ABViewOrgChartTeamsComponent.js | 47 ++++++++++++++----- 1 file changed, 34 insertions(+), 13 deletions(-) diff --git a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js index 76d83308..9e5f24a6 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js @@ -831,21 +831,42 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { delete newFormData["updated_at"]; delete newFormData["properties"]; this._setUpdatedBy(contentObj, newFormData); - for (const editContentFieldToCreateNew of editContentFieldsToCreateNew) { - const editContentFieldToCreateNewColumnName = - contentObj.fieldByID( - editContentFieldToCreateNew - )?.columnName; - if ( - JSON.stringify( - newFormData[editContentFieldToCreateNewColumnName] ?? "" - ) !== - JSON.stringify( + 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"), From 01004a3661babb966bb7564a67a7c45fffbad9c5 Mon Sep 17 00:00:00 2001 From: guyyoo Date: Tue, 11 Feb 2025 15:18:45 +0700 Subject: [PATCH 127/129] Prevent date conversion by new string format --- .../ABViewOrgChartTeamsComponent.js | 112 ++++++++++++++---- 1 file changed, 87 insertions(+), 25 deletions(-) diff --git a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js index 9e5f24a6..c945b0cd 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js @@ -161,7 +161,14 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { contentData[contentGroupByFieldColumnName] = this._parseDataPK(newGroupDataPK); pendingPromises.push( - contentModel.update(contentData.id, contentData) + contentModel.update( + contentData.id, + this._parseFormValueByType( + contentObj, + contentData, + contentData + ) + ) ); draggedNodes.push($contentRecord); isRefreshed = true; @@ -171,7 +178,14 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { } contentData[contentDateEndFieldColumnName] = newDate; pendingPromises.push( - contentModel.update(contentData.id, contentData) + contentModel.update( + contentData.id, + this._parseFormValueByType( + contentObj, + contentData, + contentData + ) + ) ); draggedNodes.push($contentRecord); isRefreshed = true; @@ -199,7 +213,13 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { } this._setUpdatedBy(contentObj, updatedData); pendingPromises.push( - contentModel.create(updatedData), + contentModel.create( + this._parseFormValueByType( + contentObj, + updatedData, + updatedData + ) + ), (async () => { const $draggedNode = await this._createUIContentRecord( updatedData, @@ -265,14 +285,28 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { ) { updatedData[contentFieldLinkColumnName] = newNodeDataPK; updatedData[contentGroupByFieldColumnName] = newGroupDataPK; - await contentModel.update(updatedData.id, updatedData); + 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, updatedData) + contentModel.update( + updatedData.id, + this._parseFormValueByType( + contentObj, + updatedData, + updatedData + ) + ) ); updatedData[contentDateStartFieldColumnName] = updatedData[contentDateEndFieldColumnName]; @@ -281,7 +315,15 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { delete updatedData[contentDateEndFieldColumnName]; updatedData[contentFieldLinkColumnName] = newNodeDataPK; updatedData[contentGroupByFieldColumnName] = newGroupDataPK; - pendingPromises.push(contentModel.create(updatedData)); + pendingPromises.push( + contentModel.create( + this._parseFormValueByType( + contentObj, + updatedData, + updatedData + ) + ) + ); await Promise.all(pendingPromises); } } @@ -773,6 +815,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { view: "datepicker", name: fieldName, label: fieldLabel, + stringResult: true, labelWidth, invalidMessage, timepicker: fieldKey === "datetime", @@ -1641,6 +1684,22 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { 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]; @@ -1653,20 +1712,22 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { break; case "connectObject": delete newFormData[`${columnName}__relation`]; - if (field.linkType() === "one") - switch (typeof oldValue) { - case "number": - newFormData[columnName] = parseInt(newValue) || null; - break; - default: - newFormData[columnName] = newValue?.toString() || null; - if ( - oldValue === undefined && - newFormData[columnName] == null - ) - delete newFormData[columnName]; - break; - } + 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; @@ -3043,12 +3104,13 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { return; this.teamEdit(newValues); } else { - newValues = this._parseFormValueByType( - teamObj, - null, - newValues + this.teamAddChild( + this._parseFormValueByType( + teamObj, + null, + newValues + ) ); - this.teamAddChild(newValues); } $teamFormPopup.blockEvent(); $teamFormPopup.$view.remove(); From f4120ad60380f6675a4b516e3d6c90f94004a023 Mon Sep 17 00:00:00 2001 From: guyyoo Date: Wed, 12 Feb 2025 11:19:33 +0700 Subject: [PATCH 128/129] Fix end date blank validation --- .../views/viewComponent/ABViewOrgChartTeamsComponent.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js index c945b0cd..68ea7f0e 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js @@ -676,7 +676,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { value > $$(ids.contentFormData).getValues()[ contentDateStartFieldColumnName - ] || value == null; + ] || value === "" || value == null; break; default: rules[fieldName] = () => true; From a4ab2872dcc7f80bc1a1178f3120940fa5f6c5b2 Mon Sep 17 00:00:00 2001 From: guyyoo Date: Wed, 12 Feb 2025 16:38:13 +0700 Subject: [PATCH 129/129] Fix the connected self parent bug --- .../ABViewOrgChartTeamsComponent.js | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js index 68ea7f0e..3627354c 100644 --- a/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js +++ b/AppBuilder/platform/views/viewComponent/ABViewOrgChartTeamsComponent.js @@ -676,7 +676,9 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { value > $$(ids.contentFormData).getValues()[ contentDateStartFieldColumnName - ] || value === "" || value == null; + ] || + value === "" || + value == null; break; default: rules[fieldName] = () => true; @@ -2424,15 +2426,22 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { * @param {object} node the current node * @param {number} [depth=0] a count of how many times we have recursed */ + const teamLinkDef = this.getSettingField("teamLink"); + const teamLinkDefColumnName = teamLinkDef.columnName; + const teamLinkedColumnDefColumnName = this.AB.definitionByID( + teamLinkDef.settings.linkColumn + ).columnName; const pullChildData = (node, depth = 0) => { if (depth >= TEAM_CHART_MAX_DEPTH) return; node.children = []; node._rawData[this.getSettingField("teamLink").columnName].forEach( (id) => { const childData = dc.getData((e) => e.id == id)[0]; + // Don't show inactive teams if ( !childData || + childData[teamLinkedColumnDefColumnName] == id || (this.__filters?.inactive == 0 && childData[this.getSettingField("teamInactive").columnName]) ) @@ -2454,10 +2463,7 @@ module.exports = class ABViewOrgChartTeamsComponent extends ABViewComponent { child.filteredOut = this.filterTeam(child); if (child.name === "External Support") child.className = `strategy-external`; - if ( - childData[this.getSettingField("teamLink").columnName] - .length > 0 - ) { + if (childData[teamLinkDefColumnName].length > 0) { pullChildData(child, depth + 1); } // If this node is filtered we still need it if it has children