diff --git a/web/pgadmin/static/js/tree/tree.js b/web/pgadmin/static/js/tree/tree.js new file mode 100644 index 00000000..ed67aa85 --- /dev/null +++ b/web/pgadmin/static/js/tree/tree.js @@ -0,0 +1,134 @@ +////////////////////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2018, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////////////////// +export class TreeNode { + constructor(id, data, parent) { + this.id = id; + this.data = data; + this.parentNode = parent; + this.path = this.id; + if (parent !== null && parent !== undefined && parent.path !== undefined) { + this.path = parent.path + '.' + id; + } + this.children = []; + } + + hasParent() { + return this.parentNode !== null && this.parentNode !== undefined; + } + + parent() { + return this.parentNode; + } + + getData() { + if (this.data === undefined) { + return undefined; + } else if (this.data === null) { + return null; + } + return Object.assign({}, this.data); + } +} + +export class Tree { + constructor() { + this.rootNode = new TreeNode(undefined, {}); + this.aciTreeApi = undefined; + } + + addNewNode(id, data, path) { + const parent = this.findNode(path); + return this.createOrUpdateNode(id, data, parent); + } + + findNode(path) { + if (path.length === 0) { + return this.rootNode; + } + return findInTree(this.rootNode, path.join('.')); + } + + findNodeByDomElement(domElement) { + const path = this.translateTreeNodeIdFromACITree(domElement); + if(!path || !path[0]) { + return undefined; + } + + return this.findNode(path); + } + + selected() { + return this.aciTreeApi.selected(); + } + + register($treeJQuery) { + $treeJQuery.on('acitree', function (event, api, item, eventName) { + if (api.isItem(item)) { + if (eventName === 'added') { + const id = api.getId(item); + const data = api.itemData(item); + const parentId = this.translateTreeNodeIdFromACITree(api.parent(item)); + this.addNewNode(id, data, parentId); + } + } + }.bind(this)); + this.aciTreeApi = $treeJQuery.aciTree('api'); + } + + createOrUpdateNode(id, data, parent) { + const oldNode = this.findNode([parent.path, id]); + if (oldNode !== null) { + oldNode.data = Object.assign({}, data); + return oldNode; + } + + const node = new TreeNode(id, data, parent); + if (parent === this.rootNode) { + node.parentNode = null; + } + parent.children.push(node); + return node; + } + + translateTreeNodeIdFromACITree(aciTreeNode) { + let currentTreeNode = aciTreeNode; + let path = []; + while (currentTreeNode !== null && currentTreeNode !== undefined && currentTreeNode.length > 0) { + path.unshift(this.aciTreeApi.getId(currentTreeNode)); + if (this.aciTreeApi.hasParent(currentTreeNode)) { + currentTreeNode = this.aciTreeApi.parent(currentTreeNode); + } else { + break; + } + } + return path; + } +} + +export let tree = new Tree(); + +function findInTree(rootNode, path) { + if (path === null) { + return rootNode; + } + return (function findInNode(currentNode) { + for (let i = 0, length = currentNode.children.length; i < length; i++) { + const calculatedNode = findInNode(currentNode.children[i]); + if (calculatedNode !== null) { + return calculatedNode; + } + } + + if (currentNode.path === path) { + return currentNode; + } else { + return null; + } + })(rootNode); +} diff --git a/web/regression/javascript/tree/tree_fake.js b/web/regression/javascript/tree/tree_fake.js new file mode 100644 index 00000000..a0983c67 --- /dev/null +++ b/web/regression/javascript/tree/tree_fake.js @@ -0,0 +1,55 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2018, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +import {Tree} from "../../../pgadmin/static/js/tree/tree"; + +export class TreeFake extends Tree { + constructor() { + super(); + this.aciTreeToOurTreeTranslator = {}; + } + + addNewNode(id, data, path) { + this.aciTreeToOurTreeTranslator[id] = path.concat(id); + return super.addNewNode(id, data, path); + } + + hasParent(aciTreeNode) { + return this.translateTreeNodeIdFromACITree(aciTreeNode).length > 1; + } + + parent(aciTreeNode) { + if (this.hasParent(aciTreeNode)) { + let path = this.translateTreeNodeIdFromACITree(aciTreeNode); + return [{id: this.findNode(path).parent().id}]; + } + + return null; + } + + translateTreeNodeIdFromACITree(aciTreeNode) { + return this.aciTreeToOurTreeTranslator[aciTreeNode[0].id]; + } + + itemData(aciTreeNode) { + let path = this.translateTreeNodeIdFromACITree(aciTreeNode); + if(path === undefined || path === null) { + return undefined; + } + return this.findNode(path).getData(); + } + + selected() { + return this.selectedNode; + } + + setSelectedNode(selectedNode) { + this.selectedNode = selectedNode; + } +} diff --git a/web/regression/javascript/tree/tree_spec.js b/web/regression/javascript/tree/tree_spec.js new file mode 100644 index 00000000..e8930d84 --- /dev/null +++ b/web/regression/javascript/tree/tree_spec.js @@ -0,0 +1,284 @@ +////////////////////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2018, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////////////////// + +import {Tree, TreeNode} from "../../../pgadmin/static/js/tree/tree"; +import {TreeFake} from "./tree_fake"; + +const context = describe; + +const treeTests = (treeClass, setDefaultCallBack) => { + let tree; + beforeEach(() => { + tree = new treeClass(); + }); + + describe('#addNewNode', () => { + describe('when add a new root element', () => { + beforeEach(() => { + tree.addNewNode('some new node', {data: 'interesting'}, []); + }); + + it('can be retrieved', () => { + const node = tree.findNode(['some new node']); + expect(node.data).toEqual({data: 'interesting'}); + }); + + it('return false for #hasParent()', () => { + const node = tree.findNode(['some new node']); + expect(node.hasParent()).toBe(false); + }); + + it('return null for #parent()', () => { + const node = tree.findNode(['some new node']); + expect(node.parent()).toBeNull(); + }); + }); + + describe('when add a new element as a child', () => { + let parentNode; + beforeEach(() => { + parentNode = tree.addNewNode('parent node', {data: 'parent data'}, []); + tree.addNewNode('some new node', {data: 'interesting'}, ['parent' + + ' node']); + }); + + it('can be retrieved', () => { + const node = tree.findNode(['parent node', 'some new node']); + expect(node.data).toEqual({data: 'interesting'}); + }); + + it('return true for #hasParent()', () => { + const node = tree.findNode(['parent node', 'some new node']); + expect(node.hasParent()).toBe(true); + }); + + it('return "parent node" object for #parent()', () => { + const node = tree.findNode(['parent node', 'some new node']); + expect(node.parent()).toEqual(parentNode); + }); + }); + + describe('when add an element that already exists under a parent', () => { + let parentNode; + beforeEach(() => { + parentNode = tree.addNewNode('parent node', {data: 'parent data'}, []); + tree.addNewNode('some new node', {data: 'interesting'}, ['parent' + + ' node']); + }); + + it('does not add a new child', () => { + tree.addNewNode('some new node', {data: 'interesting 1'}, ['parent' + + ' node']); + const parentNode = tree.findNode(['parent node']); + expect(parentNode.children.length).toBe(1); + }); + + it('updates the existing node data', () => { + tree.addNewNode('some new node', {data: 'interesting 1'}, ['parent' + + ' node']); + const node = tree.findNode(['parent node', 'some new node']); + expect(node.data).toEqual({data: 'interesting 1'}); + }); + }); + }); + + describe('#translateTreeNodeIdFromACITree', () => { + let aciTreeApi; + beforeEach(() => { + aciTreeApi = jasmine.createSpyObj('ACITreeApi', [ + 'hasParent', + 'parent', + 'getId', + ]); + + aciTreeApi.getId.and.callFake((node) => { + return node[0].id; + }); + tree.aciTreeApi = aciTreeApi; + }); + + describe('When tree as a single level', () => { + beforeEach(() => { + aciTreeApi.hasParent.and.returnValue(false); + }); + + it('returns an array with the ID of the first level', () => { + let node = [{ + id: 'some id', + }]; + tree.addNewNode('some id', {}, []); + + expect(tree.translateTreeNodeIdFromACITree(node)).toEqual(['some id']); + }); + }); + + describe('When tree as a 2 levels', () => { + describe('When we try to retrieve the node in the second level', () => { + it('returns an array with the ID of the first level and second level', () => { + aciTreeApi.hasParent.and.returnValues(true, false); + aciTreeApi.parent.and.returnValue([{ + id: 'parent id' + }]); + let node = [{ + id: 'some id', + }]; + + tree.addNewNode('parent id', {}, []); + tree.addNewNode('some id', {}, ['parent id']); + + expect(tree.translateTreeNodeIdFromACITree(node)) + .toEqual(['parent id', 'some id']); + }); + }); + }); + }); + + describe('#selected', () => { + context('a node in the tree is selected', () => { + it('returns that node object', () => { + let selectedNode = new TreeNode('bamm', {}, []); + setDefaultCallBack(tree, selectedNode) + expect(tree.selected()).toEqual(selectedNode); + }); + }); + }); + + describe('#findNodeByTreeElement', () => { + context('retrieve data from node not found', () => { + it('return undefined', () => { + let aciTreeApi = jasmine.createSpyObj('ACITreeApi', [ + 'hasParent', + 'parent', + 'getId', + ]); + + aciTreeApi.getId.and.callFake((node) => { + return node[0].id; + }); + tree.aciTreeApi = aciTreeApi; + expect(tree.findNodeByDomElement(['