From 9926dbf2e946d13bdd1255cd962881029dff0ca1 Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Mon, 9 Apr 2018 00:02:44 +0530 Subject: [PATCH] Add TreePage which uses the Tree component --- client/desk/index.js | 5 + client/desk/treepage.js | 31 +++++ client/style/style.scss | 121 +--------------- client/style/tree.scss | 111 +++++++++++++++ client/style/variables.scss | 6 + client/ui/modelTable.js | 3 +- client/ui/tree.js | 266 ++++++++++++++++++------------------ client/view/index.js | 4 + client/view/list.js | 8 +- client/view/tree.js | 151 ++++++++++++++++++++ 10 files changed, 450 insertions(+), 256 deletions(-) create mode 100644 client/desk/treepage.js create mode 100644 client/style/tree.scss create mode 100644 client/style/variables.scss create mode 100644 client/view/tree.js diff --git a/client/desk/index.js b/client/desk/index.js index 6efa40be..a4761a3d 100644 --- a/client/desk/index.js +++ b/client/desk/index.js @@ -6,6 +6,7 @@ const Page = require('frappejs/client/view/page'); const views = {}; views.Form = require('./formpage'); views.List = require('./listpage'); +views.Tree = require('./treepage'); views.Print = require('./printpage'); views.FormModal = require('./formmodal'); views.Table = require('./tablepage'); @@ -75,6 +76,10 @@ module.exports = class Desk { await this.showViewPage('List', params.doctype); }); + frappe.router.add('tree/:doctype', async (params) => { + await this.showViewPage('Tree', params.doctype); + }); + frappe.router.add('table/:doctype', async (params) => { await this.showViewPage('Table', params.doctype, params); }) diff --git a/client/desk/treepage.js b/client/desk/treepage.js new file mode 100644 index 00000000..456f593a --- /dev/null +++ b/client/desk/treepage.js @@ -0,0 +1,31 @@ +const frappe = require('frappejs'); +const Page = require('frappejs/client/view/page'); +const view = require('frappejs/client/view'); + +module.exports = class TreePage extends Page { + constructor(name) { + const hasRoute = true; + + super({ + title: frappe._("Tree"), + parent: hasRoute ? frappe.desk.body : frappe.desk.center, + hasRoute: hasRoute + }); + + this.fullPage = true; + + this.name = name; + + this.tree = new (view.getTreeClass(name))({ + doctype: name, + parent: this.body, + page: this + }); + } + + async show(params) { + super.show(); + this.setTitle(this.name===this.tree.meta.name ? (this.tree.meta.label || this.tree.meta.name) : this.name); + await this.tree.refresh(); + } +} \ No newline at end of file diff --git a/client/style/style.scss b/client/style/style.scss index cf9761e0..562fe4d7 100644 --- a/client/style/style.scss +++ b/client/style/style.scss @@ -6,16 +6,9 @@ @import "node_modules/frappe-datatable/dist/frappe-datatable"; // @import "node_modules/octicons/build/build.css"; -$spacer-1: 0.25rem; -$spacer-2: 0.5rem; -$spacer-3: 1rem; -$spacer-4: 2rem; -$spacer-5: 3rem; - +@import "./variables.scss"; @import "./indicators.scss"; -$page-width: 500px; - html { font-size: 12px; } @@ -269,117 +262,7 @@ mark { margin: $spacer-3 0px; } -.tree { - padding: 15px; -} - -.tree li { - list-style: none; - margin: 2px 0px; -} - -ul.tree-children { - padding-left: 20px; -} - -.tree-link { - cursor: pointer; - display: inline-block; - padding: 1px; -} - -.tree-link .node-parent { - color: $gray-600; - font-size: 14px; - width: 10px; - text-align: center; -} - -.tree-link .node-leaf { - color: $gray-400; -} - -.tree-link .node-parent, .tree-link .node-leaf { - margin-right: 5px; - margin-left: 2px; - margin-top: 3px; -} - -.tree-link.active { - svg { - color: $blue; - } - - a { - color: $gray-600; - } -} - -.tree-hover { - background-color: $gray-200; - min-height: 20px; - border: 1px solid $gray-600; -} - -.tree-node-toolbar { - display: inline-block; - padding: 0px 5px; - margin-left: 15px; - margin-bottom: -4px; - margin-top: -8px; -} - -// @media (max-width: @screen-xs) { -// ul.tree-children { -// padding-left: 10px; -// } -// } - -// decoration -// .tree, .tree-node { -.tree.with-skeleton, .tree.with-skeleton .tree-node { - position: relative; - - &.opened::before, &:last-child::after { - content: ''; - position: absolute; - top: 12px; - left: 7px; - height: calc(100% - 23px); - width: 1px; - background: $gray-400; - z-index: -1; - } - - &:last-child::after { - top: 11px; - left: -13px; - height: calc(100% - 15px); - width: 3px; - background: #fff; - } - - &.opened > .tree-children > .tree-node > .tree-link::before { - content: ''; - position: absolute; - width: 18px; - height: 1px; - top: 10px; - left: -12px; - z-index: -1; - background: $gray-400; - } -} - -.tree.with-skeleton.opened::before { - left: 22px; - top: 33px; - height: calc(100% - 67px); -} - -.tree-link.active ~ .balance-area { - color: $gray-600 !important; -} +@import "./tree.scss"; // just for accounting diff --git a/client/style/tree.scss b/client/style/tree.scss new file mode 100644 index 00000000..1df585db --- /dev/null +++ b/client/style/tree.scss @@ -0,0 +1,111 @@ +@import "./variables.scss"; + +.tree { + padding: $spacer-3 $spacer-4; +} + +.tree li { + list-style: none; +} + +ul.tree-children { + padding-left: $spacer-4; +} + +.tree-link { + cursor: pointer; + display: flex; + align-items: center; + width: 100%; +} + +.tree-link:hover { + background-color: $gray-100; +} + +.tree-link .node-parent { + color: $gray-600; + width: 24px; + height: 24px; + text-align: center; +} + +.tree-link .node-leaf { + color: $gray-400; +} + +.tree-link .node-parent, .tree-link .node-leaf { + padding: $spacer-2; +} + +.tree-link.active { + a { + color: $gray-600; + } +} + +.tree-hover { + background-color: $gray-200; + min-height: 20px; + border: 1px solid $gray-600; +} + +.tree-node-toolbar { + display: inline-block; + padding: 0px 5px; + margin-left: 15px; + margin-bottom: -4px; + margin-top: -8px; +} + +// @media (max-width: @screen-xs) { +// ul.tree-children { +// padding-left: 10px; +// } +// } + +// decoration +// .tree, .tree-node { +.tree.with-skeleton, .tree.with-skeleton .tree-node { + position: relative; + + &.opened::before, &:last-child::after { + content: ''; + position: absolute; + top: 12px; + left: 7px; + height: calc(100% - 23px); + width: 1px; + background: $gray-400; + z-index: -1; + } + + &:last-child::after { + top: 11px; + left: -13px; + height: calc(100% - 15px); + width: 3px; + background: #fff; + } + + &.opened > .tree-children > .tree-node > .tree-link::before { + content: ''; + position: absolute; + width: 18px; + height: 1px; + top: 10px; + left: -12px; + z-index: -1; + background: $gray-400; + } +} + +.tree.with-skeleton.opened::before { + left: 22px; + top: 33px; + height: calc(100% - 67px); +} + +.tree-link.active ~ .balance-area { + color: $gray-600 !important; +} diff --git a/client/style/variables.scss b/client/style/variables.scss new file mode 100644 index 00000000..ce8f6664 --- /dev/null +++ b/client/style/variables.scss @@ -0,0 +1,6 @@ +$spacer-1: 0.25rem; +$spacer-2: 0.5rem; +$spacer-3: 1rem; +$spacer-4: 2rem; +$spacer-5: 3rem; +$page-width: 500px; \ No newline at end of file diff --git a/client/ui/modelTable.js b/client/ui/modelTable.js index 83a63c70..cfaa19d5 100644 --- a/client/ui/modelTable.js +++ b/client/ui/modelTable.js @@ -1,6 +1,6 @@ const frappe = require('frappejs'); const DataTable = require('frappe-datatable'); -const controls = require('frappejs/client/view/controls'); + const Modal = require('frappejs/client/ui/modal'); const utils = require('./utils'); @@ -50,6 +50,7 @@ module.exports = class ModelTable { getControl(field, parent) { field.onlyInput = true; + const controls = require('frappejs/client/view/controls'); const control = controls.makeControl({field: field, parent: parent}); // change will be triggered by datatable diff --git a/client/ui/tree.js b/client/ui/tree.js index 263ecb1c..10f731b1 100644 --- a/client/ui/tree.js +++ b/client/ui/tree.js @@ -4,168 +4,168 @@ const utils = require('frappejs/client/ui/utils'); class Tree { constructor({parent, label, iconSet, withSkeleton, method}) { - Object.assign(this, arguments[0]); - this.nodes = {}; - if(!iconSet) { - this.iconSet = { - open: octicons["triangle-down"].toSVG({ "width": 10, "class": "node-parent"}), - closed: octicons["triangle-right"].toSVG({ "width": 5, "class": "node-parent"}), - leaf: octicons["primitive-dot"].toSVG({ "width": 7, "class": "node-leaf"}) - }; - } - this.make(); + Object.assign(this, arguments[0]); + this.nodes = {}; + if(!iconSet) { + this.iconSet = { + open: octicons["triangle-down"].toSVG({ "width": 10, "class": "node-parent"}), + closed: octicons["triangle-right"].toSVG({ "width": 5, "class": "node-parent"}), + leaf: octicons["primitive-dot"].toSVG({ "width": 7, "class": "node-leaf"}) + }; + } + this.make(); } make() { - this.tree = frappe.ui.create('div', { - inside: this.parent, - className: 'tree ' + (this.withSkeleton ? 'with-skeleton' : '') - }); + this.tree = frappe.ui.create('div', { + inside: this.parent, + className: 'tree ' + (this.withSkeleton ? 'with-skeleton' : '') + }); - this.rootNode = this.makeNode(this.label, this.label, true, null, this.tree); - this.expandNode(this.rootNode); - } + this.rootNode = this.makeNode(this.label, this.label, true, null, this.tree); + this.expandNode(this.rootNode); + } - refresh() { - // this.selectedNode.parentNode && - // this.loadChildren(this.selectedNode.parentNode, true); - } + refresh() { + // this.selectedNode.parentNode && + // this.loadChildren(this.selectedNode.parentNode, true); + } - loadChildren(node, deep=false) { - if(!deep) { - this.renderNodeChildren(node, this.method(node.value)); - } else { - this.renderChildrenDeep(node, this.getAllNodes(node.value)); - } - } + async loadChildren(node, deep=false) { + let children = !deep ? await this.method(node) : await this.getAllNodes(node); + this.renderNodeChildren(node, children); + } - renderChildrenDeep(dataList) { - dataList.map(d => { this.renderNodeChildren(this.nodes[d.parent], d.data); }); - } + renderChildrenDeep(dataList) { + dataList.map(d => { this.renderNodeChildren(this.nodes[d.parent], d.data); }); + } - renderNodeChildren(node, dataSet=[]) { - frappe.ui.empty(node.childrenList); + renderNodeChildren(node, dataSet=[]) { + frappe.ui.empty(node.childrenList); - dataSet.forEach(data => { - let parentNode = this.nodes[node.value]; - let childNode = this.makeNode(data.label || data.value, data.value, - data.expandable, parentNode); - childNode.treeLink.dataset.nodeData = data; - }); - node.expanded = false; + dataSet.forEach(data => { + let parentNode = this.nodes[node.value]; + let childNode = this.makeNode(data.label || data.value, data.value, + data.expandable, parentNode); + childNode.treeLink.dataset.nodeData = data; + }); + node.expanded = false; - // As children loaded - node.loaded = true; - this.onNodeClick(node, false); - } + // As children loaded + node.loaded = true; + this.onNodeClick(node, true); + } - getAllNodes() { } + getAllNodes() { } - makeNode(label, value, expandable, parentNode, parentEl) { - let node = { - parent: parent, - label: label, - value: value, - loaded: 0, - expanded: 0, - expandable: expandable, - }; + makeNode(label, value, expandable, parentNode, parentEl) { + let node = { + label: label, + value: value, + loaded: 0, + expanded: 0, + expandable: expandable, + }; - if(parentNode){ - node.parentNode = parentNode; - node.parent = parentNode.childrenList; - node.isRoot = 0; - } else { - node.isRoot = 1; - node.parent = parentEl; - } + if(parentNode){ + node.parentNode = parentNode; + node.parent = parentNode.childrenList; + node.isRoot = 0; + } else { + node.isRoot = 1; + node.parent = parentEl; + } - this.nodes[value] = node; - this.buildNodeElement(node); - this.onRender && this.onRender(node); + this.nodes[value] = node; + this.buildNodeElement(node); + this.onRender && this.onRender(node); - return node; - } + return node; + } - buildNodeElement(node) { - node.parentLi = frappe.ui.create('li', { - inside: node.parent, - className: 'tree-node' - }); + buildNodeElement(node) { + node.parentLi = frappe.ui.create('li', { + inside: node.parent, + className: 'tree-node' + }); - let iconHtml = ''; - if(this.iconSet) { - iconHtml = node.expandable ? this.iconSet.closed : this.iconSet.leaf; - } - let labelEl = ` ${node.label}`; + let iconHtml = ''; + if(this.iconSet) { + iconHtml = node.expandable ? this.iconSet.closed : this.iconSet.leaf; + } + let labelEl = ` ${node.label}`; - node.treeLink = frappe.ui.create('span', { - inside: node.parentLi, - className: 'tree-link', - 'data-label': node.label, - innerHTML: iconHtml + labelEl - }); - node.treeLink.dataset.node = node; - node.treeLink.addEventListener('click', () => { - this.onNodeClick(node); - }); + node.treeLink = frappe.ui.create('span', { + inside: node.parentLi, + className: 'tree-link', + 'data-label': node.label, + innerHTML: iconHtml + labelEl + }); + node.treeLink.dataset.node = node; + node.treeLink.addEventListener('click', () => { + this.onNodeClick(node); + }); - node.childrenList = frappe.ui.create('ul', { - inside: node.parentLi, - className: 'tree-children hide' - }); + node.childrenList = frappe.ui.create('ul', { + inside: node.parentLi, + className: 'tree-children hide' + }); - // if(this.toolbar) { - // node.toolbar = this.getToolbar(node).insertAfter(node.treeLink); - // } - } + // if(this.toolbar) { + // node.toolbar = this.getToolbar(node).insertAfter(node.treeLink); + // } + } - onNodeClick(node, click = true) { - this.setSelectedNode(node); - if(click) { - this.onClick && this.onClick(node); - } - this.expandNode(node); - // select link - utils.activate(this.tree, node.treeLink, 'tree-link', 'active'); - if(node.toolbar) this.showToolbar(node); - } + async onNodeClick(node, click = true) { + this.setSelectedNode(node); + if(click) { + this.onClick && this.onClick(node); + } + await this.expandNode(node); + // select link + utils.activate(this.tree, node.treeLink, 'tree-link', 'active'); + if(node.toolbar) this.showToolbar(node); + } - expandNode(node) { - if(node.expandable) { - this.toggleNode(node); - } + async expandNode(node) { + if(node.expandable) { + await this.toggleNode(node); + } - node.expanded = !node.expanded; - // node.parent.classList.toggle('opened', node.expanded); - node.parent.classList.add('opened'); - node.parentLi.classList.add('opened'); - } + node.expanded = !node.expanded; + // node.parent.classList.toggle('opened', node.expanded); + node.parent.classList.add('opened'); + node.parentLi.classList.add('opened'); + } - toggleNode(node) { - if(!node.loaded) this.loadChildren(node); + async toggleNode(node) { + if(!node.loaded) await this.loadChildren(node); - // expand children - if(node.childrenList) { - if(node.childrenList.innerHTML.length) { - node.childrenList.classList.toggle('hide', !node.expanded); - } + // expand children + if(node.childrenList) { + if(node.childrenList.innerHTML.length) { + if (node.expanded) { + node.childrenList.classList.add('hide'); + } else { + node.childrenList.classList.remove('hide'); + } + } - // open close icon - if(this.iconSet) { - const oldIcon = node.treeLink.querySelector('svg'); - const newIconKey = !node.expanded ? 'closed' : 'open'; - const newIcon = frappe.ui.create(this.iconSet[newIconKey]); - node.treeLink.replaceChild(newIcon, oldIcon); - } - } - } + // open close icon + if(this.iconSet) { + const oldIcon = node.treeLink.querySelector('svg'); + const newIconKey = node.expanded ? 'closed' : 'open'; + const newIcon = frappe.ui.create(this.iconSet[newIconKey]); + node.treeLink.replaceChild(newIcon, oldIcon); + } + } + } - getSelectedNode() { return this.selectedNode; } + getSelectedNode() { return this.selectedNode; } - setSelectedNode(node) { this.selectedNode = node; } + setSelectedNode(node) { this.selectedNode = node; } - showToolbar() { } + showToolbar() { } } module.exports = Tree; diff --git a/client/view/index.js b/client/view/index.js index 98e81861..e2c3ef57 100644 --- a/client/view/index.js +++ b/client/view/index.js @@ -1,4 +1,5 @@ const BaseList = require('frappejs/client/view/list'); +const BaseTree = require('frappejs/client/view/tree'); const BaseForm = require('frappejs/client/view/form'); const frappe = require('frappejs'); @@ -8,5 +9,8 @@ module.exports = { }, getListClass(doctype) { return (frappe.views['List'] && frappe.views['List'][doctype]) || BaseList; + }, + getTreeClass(doctype) { + return (frappe.views['Tree'] && frappe.views['Tree'][doctype] || BaseTree); } } \ No newline at end of file diff --git a/client/view/list.js b/client/view/list.js index c5e93605..abcdbbf6 100644 --- a/client/view/list.js +++ b/client/view/list.js @@ -5,9 +5,11 @@ const Observable = require('frappejs/utils/observable'); module.exports = class BaseList extends Observable { constructor({doctype, parent, fields=[], page}) { super(); - Object.assign(this, arguments[0]); + this.init(); + } + init() { this.meta = frappe.getMeta(this.doctype); this.start = 0; @@ -17,14 +19,14 @@ module.exports = class BaseList extends Observable { this.rows = []; this.data = []; - this.setupListSettings(); + this.setupTreeSettings(); frappe.db.on(`change:${this.doctype}`, (params) => { this.refresh(); }); } - setupListSettings() { + setupTreeSettings() { // list settings that can be overridden by meta this.listSettings = { getFields: list => list.fields, diff --git a/client/view/tree.js b/client/view/tree.js new file mode 100644 index 00000000..58262119 --- /dev/null +++ b/client/view/tree.js @@ -0,0 +1,151 @@ +const frappe = require('frappejs'); +const BaseList = require('./list'); +const Tree = require('frappejs/client/ui/tree'); +// const keyboard = require('frappejs/client/ui/keyboard'); + +module.exports = class BaseTree extends BaseList { + + init() { + this.meta = frappe.getMeta(this.doctype); + + this.body = null; + this.data = []; + + this.setupTreeSettings(); + + frappe.db.on(`change:${this.doctype}`, (params) => { + this.refresh(); + }); + } + + setupTreeSettings() { + // tree settings that can be overridden by meta + this.treeSettings = { + parentField: `parent${this.doctype}` + } + + if (this.meta.treeSettings) { + Object.assign(this.treeSettings, this.meta.treeSettings); + } + } + + async refresh() { + return await this.run(); + } + + async run() { + this.makeBody(); + this.body.innerHTML = ''; + this.dirty = false; + + let accountingSettings = await frappe.db.getSingle('AccountingSettings'); + let rootLabel = accountingSettings.companyName; + + this.renderTree(rootLabel); + this.trigger('state-change'); + } + + makeBody() { + if (!this.body) { + this.makeToolbar(); + this.parent.classList.add('tree-page'); + this.body = frappe.ui.add('div', 'tree-body', this.parent); + this.body.setAttribute('data-doctype', this.doctype); + this.bindKeys(); + } + } + + renderTree(rootLabel) { + this.tree = new Tree({ + label: rootLabel, + parent: this.body, + method: async node => { + const children = await this.getData(node) || []; + return children.map(d => ({ + label: d.name, + value: d.name, + expandable: d.isGroup + })); + } + }); + } + + async getData(node) { + let fields = this.getFields(); + let filters = {}; + + if (node.isRoot) { + filters[this.treeSettings.parentField] = ''; + } else { + filters[this.treeSettings.parentField] = node.value; + } + + return await frappe.db.getAll({ + doctype: this.doctype, + fields, + filters, + order_by: 'name', + order: 'asc' + }); + } + + getFields() { + let fields = [this.treeSettings.parentField, 'isGroup'] + this.updateStandardFields(fields); + return fields; + } + + makeToolbar() { + this.makeSearch(); + + this.btnNew = this.page.addButton(frappe._('New'), 'btn-primary', async () => { + await frappe.router.setRoute('new', this.doctype); + }); + + this.btnDelete = this.page.addButton(frappe._('Delete'), 'btn-secondary hide', async () => { + await frappe.db.deleteMany(this.doctype, this.getCheckedRowNames()); + await this.refresh(); + }); + + this.btnReport = this.page.addButton(frappe._('Report'), 'btn-outline-secondary hide', async () => { + await frappe.router.setRoute('table', this.doctype); + }); + + this.on('state-change', () => { + const checkedCount = this.getCheckedRowNames().length; + this.btnDelete.classList.toggle('hide', checkedCount ? false : true); + this.btnNew.classList.toggle('hide', checkedCount ? true : false); + this.btnReport.classList.toggle('hide', checkedCount ? true : false); + }); + + this.page.body.addEventListener('click', (event) => { + if(event.target.classList.contains('checkbox')) { + this.trigger('state-change'); + } + }) + } + + makeSearch() { + this.toolbar = frappe.ui.add('div', 'list-toolbar', this.parent); + this.toolbar.innerHTML = ` + + `; + + this.searchInput = this.toolbar.querySelector('input'); + this.searchInput.addEventListener('keypress', (event) => { + if (event.keyCode===13) { + this.refresh(); + } + }); + + this.btnSearch = this.toolbar.querySelector('.btn-search'); + this.btnSearch.addEventListener('click', (event) => { + this.refresh(); + }); + } +};