From e0ae000d45d35489952d9e077738f20cd764bb7e Mon Sep 17 00:00:00 2001 From: Tushar Vinod Gosalia Date: Wed, 24 May 2017 22:26:44 +0530 Subject: [PATCH 1/5] fixed node deletion issue --- helpers.js | 66 +++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 58 insertions(+), 8 deletions(-) diff --git a/helpers.js b/helpers.js index 1df581e..d0f6998 100644 --- a/helpers.js +++ b/helpers.js @@ -13,16 +13,66 @@ export const removeNodeWithChildren = R.curry((nodesById, id) => { export const getNodeIdsToDelete = (byId, id) => { if (byId[id].childIds && byId[id].childIds.length) { - return R.reduce((currentNodes, nodeId) => - R.concat(currentNodes, getNodeIdsToDelete(byId, nodeId)), [id], byId[id].childIds); + return R.reduce( + (currentNodes, nodeId) => + R.concat(currentNodes, getNodeIdsToDelete(byId, nodeId)), + [id], + byId[id].childIds, + ); } return [id]; }; -export const deleteNodeWithChildren = (nodes, id) => { - const nodesToDelete = getNodeIdsToDelete(nodes.byId, id); - return R.evolve({ - byId: R.pickBy(node => !R.contains(node.id, nodesToDelete)), - rootIds: R.filter(R.complement(R.equals(id))), - }, nodes); +/** + * Filter out garbage references from node's childIds + */ +const deleteReferencesFromChildIds = R.curry((parentId, nodesToDelete) => + R.pipe( + R.tap(console.log), + R.over( + R.lensProp(parentId), + R.evolve({ + childIds: R.reject(value => R.contains(value, nodesToDelete)), + }), + ), + ), +); + +/** + * Deletes the node from `nodes` object + * @param {string} nodeId + * @param {array} nodesToDelete + * @param {object} nodes + */ +const deleteNodeById = (nodeId, nodesToDelete, nodes) => + R.evolve( + { + byId: R.pickBy(node => !R.contains(node.id, nodesToDelete)), + rootIds: R.reject(R.contains(nodeId)), + }, + nodes, + ); + +/** + * Wrapper around `deleteReferencesFromChildIds` for + * removing the nodes references which got deleted. + * @param {string} parentId + * @param {array} nodesToDelete + * @param {object} nodes + */ +const removeIds = (parentId, nodesToDelete, nodes) => + R.evolve( + { + byId: deleteReferencesFromChildIds(parentId, nodesToDelete), + rootIds: R.identity, + }, + nodes, + ); + +export const deleteNodeWithChildren = (nodes, nodeId, parentId) => { + const nodesToDelete = getNodeIdsToDelete(nodes.byId, nodeId); + return R.pipe( + R.partial(deleteNodeById, [nodeId, nodesToDelete]), + R.partial(removeIds, [parentId, nodesToDelete]), + )(nodes); }; From 10b35142019955935642622c2eedda879cec9c11 Mon Sep 17 00:00:00 2001 From: Tushar Date: Thu, 25 May 2017 08:10:12 +0530 Subject: [PATCH 2/5] added 'shouldSelectNode' api --- App.js | 81 ++++++++++++++++++------------- tree-component.js | 120 +++++++++++++++++++++++++++++----------------- 2 files changed, 125 insertions(+), 76 deletions(-) diff --git a/App.js b/App.js index fbb0478..3f8038e 100644 --- a/App.js +++ b/App.js @@ -1,13 +1,13 @@ -import React from 'react'; -import {autobind} from 'core-decorators'; -import R from 'ramda'; -import classNames from 'classnames'; -import Tree from './tree-component'; -import './styles.less'; -import {deleteNodeWithChildren} from './helpers'; -import TreeRow from './tree-row-component'; -import RowRenderer from './row-renderer'; -import nodes from './mock-data'; +import React from "react"; +import { autobind } from "core-decorators"; +import R from "ramda"; +import classNames from "classnames"; +import Tree from "./tree-component"; +import "./styles.less"; +import { deleteNodeWithChildren } from "./helpers"; +import TreeRow from "./tree-row-component"; +import RowRenderer from "./row-renderer"; +import nodes from "./mock-data"; const MOCK_SERVER_TIME = 0; @@ -22,22 +22,22 @@ export default class App extends React.Component { state = { nodes - } + }; getRandomWord() { - return fetch('http://www.setgetgo.com/randomword/get.php?len=4') - .then((resp) => resp.text()); + return fetch( + "http://www.setgetgo.com/randomword/get.php?len=4" + ).then(resp => resp.text()); } - @autobind - handleExpand(nodeId) { + @autobind handleExpand(nodeId) { const parentNode = this.state.nodes.byId[nodeId]; - if(parentNode.lazyLoad) { + if (parentNode.lazyLoad) { setTimeout(() => { const randomWords = []; this.getRandomWord() - .then((word) => randomWords.push(word)) + .then(word => randomWords.push(word)) .then(this.getRandomWord) - .then((word) => randomWords.push(word)) + .then(word => randomWords.push(word)) .then(() => { const newNode1 = { id: `${nodeId}.${randomWords[0]}`, @@ -51,10 +51,10 @@ export default class App extends React.Component { }; const byId = R.pipe( R.evolve({ - [nodeId]: R.assoc('childIds', [newNode1.id, newNode2.id]) + [nodeId]: R.assoc("childIds", [newNode1.id, newNode2.id]) }), R.assoc(newNode1.id, newNode1), - R.assoc(newNode2.id, newNode2), + R.assoc(newNode2.id, newNode2) )(this.state.nodes.byId); this.setState({ nodes: { @@ -62,32 +62,49 @@ export default class App extends React.Component { byId } }); - }) - }, MOCK_SERVER_TIME) + }); + }, MOCK_SERVER_TIME); } } - @autobind - handleClick(event, nodeId, parentId) { + @autobind handleClick(event, nodeId, parentId) { const classNames = event.target.className; - if (R.contains('delete', classNames)) { + if (R.contains("delete", classNames)) { this.setState({ nodes: deleteNodeWithChildren(this.state.nodes, nodeId, parentId) }); } } + @autobind shouldSelectNode(node) { + console.log( + "shouldSelectNode ", + node.id, + "---", + this.tree.getSelectedNode() + ); + if (!node || node.id === this.tree.getSelectedNode()) { + return false; // Prevent from deselecting the current node + } + return true; + } + render() { - const {nodes} = this.state; + const { nodes } = this.state; return (
+ nodes={nodes} + onClick={this.handleClick} + onExpand={this.handleExpand} + ref={c => { + this.tree = c.state; + console.log("Tree ", c); + }} + shouldSelectNode={this.shouldSelectNode} + rowRenderer={RowRenderer} + onCollapse={this.handleCollapse} + />
); } diff --git a/tree-component.js b/tree-component.js index 217c295..7837ae4 100644 --- a/tree-component.js +++ b/tree-component.js @@ -1,9 +1,9 @@ -import React from 'react'; -import Row from './row'; -import R from 'ramda'; -import {fromJS, List, updateAt} from 'immutable'; -import {autobind} from 'core-decorators'; -import Title from './title'; +import React from "react"; +import Row from "./row"; +import R from "ramda"; +import { fromJS, List, updateAt } from "immutable"; +import { autobind } from "core-decorators"; +import Title from "./title"; export default class TreeComponent extends React.Component { static propTypes = { @@ -21,88 +21,120 @@ export default class TreeComponent extends React.Component { nodes: this.getImmutableData(this.props.nodes), renderedNodeIds: List(), selectedNodeIds: List(), - } + getSelectedNode: this.getSelectedNode + }; static defaultProps = { - shouldSelectNode: R.T, + shouldSelectNode: null, rowRenderer: Title + }; + + /** + * An public api which returns the selected node id + */ + @autobind getSelectedNode() { + return this.state.selectedNodeIds.get(0); } - @autobind - handleToggle(nodeId) { + @autobind handleToggle(nodeId) { const index = this.state.expandedNodeIds.indexOf(nodeId); if (index >= 0) { - return this.setState({ - expandedNodeIds: this.state.expandedNodeIds.remove(index) - }); + return this.setState({ + expandedNodeIds: this.state.expandedNodeIds.remove(index) + }); } this.props.onExpand(nodeId); - if(this.state.nodes.getIn(['byId', nodeId, 'lazyLoad'])) { + if (this.state.nodes.getIn(["byId", nodeId, "lazyLoad"])) { this.setState({ loadingNodeIds: this.state.loadingNodeIds.push(nodeId) - }) + }); } return this.setState({ - expandedNodeIds: this.state.expandedNodeIds.push(nodeId) + expandedNodeIds: this.state.expandedNodeIds.push(nodeId) }); } - @autobind - handleClick(event, nodeId, parentId) { + @autobind handleClick(event, nodeId, parentId) { + //Handles node deletion this.props.onClick(event, nodeId, parentId); - if (!this.props.multiselectMode) { + //Allows user to choose whether node should get selected + // or not + const shouldGetSelected = this.props.shouldSelectNode( + this.props.nodes.byId[nodeId] + ); + + if (!this.props.multiselectMode && shouldGetSelected) { if (this.state.selectedNodeIds.contains(nodeId)) { this.setState({ selectedNodeIds: List() - }) + }); } else { this.setState({ selectedNodeIds: List([nodeId]) }); - this.props.onSelect(nodeId, this.state.selectedNodeIds, parentId); } } } filterCurrentNodes(newNodes, nodesToFilter) { - return nodesToFilter.filter((nodeId) => R.contains(nodeId, R.keys(newNodes.byId))); + return nodesToFilter.filter(nodeId => + R.contains(nodeId, R.keys(newNodes.byId)) + ); } componentWillReceiveProps(newProps) { - const loadingNodeIds = this.state.loadingNodeIds.filter((nodeId) => !newProps.nodes.byId[nodeId].childIds); + const loadingNodeIds = this.state.loadingNodeIds.filter( + nodeId => !newProps.nodes.byId[nodeId].childIds + ); const newNodes = this.getImmutableData(newProps.nodes); this.setState({ nodes: newNodes, - loadingNodeIds, + loadingNodeIds }); this.setState({ - expandedNodeIds: this.filterCurrentNodes(newNodes, this.state.expandedNodeIds), - loadingNodeIds: this.filterCurrentNodes(newNodes, this.state.loadingNodeIds), - selectedNodeIds: this.filterCurrentNodes(newNodes, this.state.selectedNodeIds), + expandedNodeIds: this.filterCurrentNodes( + newNodes, + this.state.expandedNodeIds + ), + loadingNodeIds: this.filterCurrentNodes( + newNodes, + this.state.loadingNodeIds + ), + selectedNodeIds: this.filterCurrentNodes( + newNodes, + this.state.selectedNodeIds + ) }); } render() { - const {selectedNodeIds, expandedNodeIds, loadingNodeIds, nodes} = this.state; - const {rowRenderer} = this.props; - const rootNodeIds = nodes.get('rootIds'); + const { + selectedNodeIds, + expandedNodeIds, + loadingNodeIds, + nodes + } = this.state; + const { rowRenderer } = this.props; + const rootNodeIds = nodes.get("rootIds"); //TODO: can try to optimise to send only children to rows return (
- {rootNodeIds.map((nodeId) => { - const node = nodes.getIn(['byId', nodeId]); - return }) - } + {rootNodeIds.map(nodeId => { + const node = nodes.getIn(["byId", nodeId]); + return ( + + ); + })}
); } From af26fa8d38894273457bacfec4935d9c05e4c55e Mon Sep 17 00:00:00 2001 From: Tushar Date: Mon, 29 May 2017 15:02:38 +0530 Subject: [PATCH 3/5] Added unit test cases --- __tests__/row.test.js | 97 +++++++++++++++++++++++++++++++++ __tests__/rowRenderer.test.js | 42 ++++++++++++++ __tests__/treeComponent.test.js | 21 +++++++ __tests__/treeRow.test.js | 27 +++++++++ jest.config.js | 5 ++ utils/logger.js | 5 ++ 6 files changed, 197 insertions(+) create mode 100644 __tests__/row.test.js create mode 100644 __tests__/rowRenderer.test.js create mode 100644 __tests__/treeComponent.test.js create mode 100644 __tests__/treeRow.test.js create mode 100644 jest.config.js create mode 100644 utils/logger.js diff --git a/__tests__/row.test.js b/__tests__/row.test.js new file mode 100644 index 0000000..0b00822 --- /dev/null +++ b/__tests__/row.test.js @@ -0,0 +1,97 @@ +import React from "react"; +import { shallow, mount } from "enzyme"; +import { fromJS, List } from "immutable"; +import toJSON from "enzyme-to-json"; +import Row from "../row"; +import TitleComponent from "../title"; +import logger from "../utils/logger"; + +const mockData = { + byId: { + 1: { + id: "1", + name: "One", + lazyLoad: true + }, + 2: { + id: "2", + name: "Two", + childIds: ["2.1", "2.2"] + }, + 2.1: { + id: "2.1", + name: "Two.One" + }, + 2.2: { + id: "2.2", + name: "Two.Two" + } + } +}; + +describe("", () => { + const states = { + expander: +, + collapser: -, + loader: ^ + }; + const requiredProps = { + expandedNodeIds: List(), + loadingNodeIds: List(), + nodes: fromJS(mockData), + nodeId: "2", + parentId: "2", + rowRenderer: TitleComponent, + renderedNodeIds: List(), + selectedNodeIds: List() + }; + test("should load without crashing", () => { + const wrapper = shallow(); + expect(wrapper.length).toBe(1); + }); + + test("should have required default props", () => { + const wrapper = shallow(); + "expander,collapser,loader,expanded,depth".split(",").forEach(value => { + expect(wrapper.instance().props[value]).toBeDefined(); + }); + }); + + test("should show expand icon if node have childs or is lazy loaded", () => {}); + + test("should have required methods passed as props", () => { + // onClick={this.handleClick} + // onToggle={this.handleToggle} + }); + + test("should call `toggle` function when clicked on expand icon", () => { + const onToggleFn = jest.fn(); + const wrapper = mount(); + // Row.prototype.mockToggleFn = jest.fn(); + // const spy = jest.spyOn(Row.prototype, "mockToggleFn"); + let ele = wrapper + .find(`#row-${requiredProps.nodeId}`) + .children(".toggle-wrapper") + .find("a") + .simulate("click"); + // expect(spy).toHaveBeenCalled(); + expect(onToggleFn).toHaveBeenCalledWith(requiredProps.nodeId); + // expect(toJSON(wrapper)).toMatchSnapshot(); + }); + + test("should call `onClick` function to `delete node` with proper args when clicked", () => { + const onClick = jest.fn(); + const wrapper = mount(); + const ele = wrapper + .find(`#row-${requiredProps.nodeId}`) + .children(".title-wrapper"); + let event = ele.simulate("click"); + logger.log("ele", ele.length); + //Unable to test this + // expect(onClick).toBeCalledWith(event,requiredProps.nodeId,requiredProps.parentId); + expect(onClick).toBeCalled(); + }); + test("should show loading indicator when node is lazily loaded", () => {}); + test("should select the node when user clicks on any node", () => {}); + test("should be able render the child nodes", () => {}); +}); diff --git a/__tests__/rowRenderer.test.js b/__tests__/rowRenderer.test.js new file mode 100644 index 0000000..4251fda --- /dev/null +++ b/__tests__/rowRenderer.test.js @@ -0,0 +1,42 @@ +import React from "react"; +import RowRenderer from "../row-renderer"; +import logger from "../utils/logger"; +import toJson from "enzyme-to-json"; +import TreeRowComonent from "../tree-row-component"; +import { shallow, mount } from "enzyme"; + +describe("", () => { + test("should render without crashing", () => { + const mockData = { + id: 1, + name: "Foo" + }; + const wrapper = shallow(); + expect(wrapper.length).toBe(1); + }); + + test("should allow user to rename the node", () => { + const mockData = { + id: 1, + name: "Foo", + renameMode: true, + state: { + selected: false + } + }; + const wrapper = mount( + + ); + expect(wrapper.find(".infinite-tree-rename-input").length).toBe(0); + expect(wrapper.find(".infinite-tree-title").length).toBe(1); + + wrapper.setProps({ node: mockData }); + expect(wrapper.find(".infinite-tree-rename-input").length).toBe(1); + expect(wrapper.find(".infinite-tree-title").length).toBe(0); + }); + + it("should have default props defined", () => { + const wrapper = shallow(); + expect(wrapper.instance().props.treeOptions).toEqual({}); + }); +}); diff --git a/__tests__/treeComponent.test.js b/__tests__/treeComponent.test.js new file mode 100644 index 0000000..40907b0 --- /dev/null +++ b/__tests__/treeComponent.test.js @@ -0,0 +1,21 @@ +import React from "react"; + +describe("", () => { + test("should pass", () => { + expect(1 === 1).toBe(true); + }); + + // test("should show collapse icon when node is expanded", () => { + // let $toggleWrapper = wrapper + // .find(`#row-${requiredProps.nodeId}`) + // .children(".toggle-wrapper"); + + // $toggleWrapper.find("a").simulate("click"); + // // expect($toggleWrapper.find("a").html()).toEqual(states.collapser); + // }); + + // test("should call collapse function when clicked on collapse icon", () => {}); + // test("should show loading indicator when node is lazily loaded", () => {}); + // test("should select the node when user clicks on any node", () => {}); + // test("should be able render the child nodes", () => {}); +}); diff --git a/__tests__/treeRow.test.js b/__tests__/treeRow.test.js new file mode 100644 index 0000000..60f952a --- /dev/null +++ b/__tests__/treeRow.test.js @@ -0,0 +1,27 @@ +import React from "react"; +import { shallow } from "enzyme"; +import toJSON from "enzyme-to-json"; +import TreeRowComponent from "../tree-row-component"; +import logger from "../utils/logger"; + +describe("", () => { + test("should load without crashing", () => {}); + + test("should add appropriate class whenever a node is selected", () => { + const mockData = { state: {} }; + const wrapper = shallow(); + let component = wrapper.find(".infinite-tree-item"); + let classNames = component.props().className.split(" "); + + expect(classNames).not.toContain("infinite-tree-selected"); + + wrapper.setProps({ + node: Object.assign({}, mockData, { state: { selected: true } }) + }); + + component = wrapper.find(".infinite-tree-item"); + classNames = component.props().className.split(" "); + + expect(classNames).toContain("infinite-tree-selected"); + }); +}); diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 0000000..b97ca56 --- /dev/null +++ b/jest.config.js @@ -0,0 +1,5 @@ +module.exports = { + moduleFileExtensions: ['js'], + testPathIgnorePatterns: ['/node_modules/'], + verbose: true, +}; diff --git a/utils/logger.js b/utils/logger.js new file mode 100644 index 0000000..d4bd78a --- /dev/null +++ b/utils/logger.js @@ -0,0 +1,5 @@ +export default { + log: (message, value) => { + console.log(`***************${message}***************`, value); + }, +}; From efc384ba5b488fc77e6b57694978fc5c1bd8a0bf Mon Sep 17 00:00:00 2001 From: Tushar Date: Mon, 29 May 2017 15:03:37 +0530 Subject: [PATCH 4/5] minor bug fixes --- App.js | 5 ++- package.json | 7 ++- row-renderer.js | 36 ++++++++------- row.js | 112 +++++++++++++++++++++++++++------------------- tree-component.js | 24 ++++++---- 5 files changed, 110 insertions(+), 74 deletions(-) diff --git a/App.js b/App.js index 3f8038e..6f42f6c 100644 --- a/App.js +++ b/App.js @@ -1,4 +1,6 @@ import React from "react"; +import { Map } from "immutable"; + import { autobind } from "core-decorators"; import R from "ramda"; import classNames from "classnames"; @@ -83,6 +85,7 @@ export default class App extends React.Component { "---", this.tree.getSelectedNode() ); + if (!node || node.id === this.tree.getSelectedNode()) { return false; // Prevent from deselecting the current node } @@ -98,7 +101,7 @@ export default class App extends React.Component { onClick={this.handleClick} onExpand={this.handleExpand} ref={c => { - this.tree = c.state; + this.tree = c && c.state; console.log("Tree ", c); }} shouldSelectNode={this.shouldSelectNode} diff --git a/package.json b/package.json index 081390b..f82ddd2 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "", "main": "main.js", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", + "test": "jest --config jest.config.js --coverage", "start": "concurrently --kill-others \"webpack-dev-server\"" }, "author": "", @@ -39,12 +39,17 @@ "babel-preset-react": "^6.16.0", "babel-preset-stage-0": "^6.16.0", "concurrently": "^3.1.0", + "enzyme": "^2.8.2", + "enzyme-to-json": "^1.5.1", "eslint": "^3.10.2", "eslint-config-airbnb": "^14.0.0", "eslint-plugin-import": "^2.2.0", "eslint-plugin-jsx-a11y": "^4.0.0", "eslint-plugin-react": "^6.7.1", + "jest": "^20.0.4", "less": "^2.7.1", + "react-addons-test-utils": "^15.5.1", + "sinon": "^2.3.2", "webpack": "2.4.1", "webpack-dev-server": "2.4.2" } diff --git a/row-renderer.js b/row-renderer.js index 96f2447..a7aacff 100644 --- a/row-renderer.js +++ b/row-renderer.js @@ -1,41 +1,43 @@ -import React, { PropTypes } from 'react'; -import classNames from 'classnames'; -import TreeRow from './tree-row-component'; +import React, { PropTypes } from "react"; +import classNames from "classnames"; +import TreeRow from "./tree-row-component"; -const RowRendererComponent = (props) => { +const RowRendererComponent = props => { const { node, treeOptions } = props; - const { - id, name, type, renameMode, contextMenu = true, - } = node; + const { id, name, type, renameMode, contextMenu = true } = node; const { deleteNode } = treeOptions; - const label = renameMode ? - () : {name}; + /> + : {name}; - const className = classNames('infinite-tree-closed', { 'context-menu-button': contextMenu }, 'pull-right'); + const className = classNames( + "infinite-tree-closed", + { "context-menu-button": contextMenu }, + "pull-right" + ); return ( - + {label} D - ); + + ); }; RowRendererComponent.propTypes = { node: PropTypes.shape().isRequired, - treeOptions: PropTypes.shape(), + treeOptions: PropTypes.shape() }; RowRendererComponent.defaultProps = { treeOptions: {}, + node: {} }; export default RowRendererComponent; diff --git a/row.js b/row.js index 7ff33a4..9729bde 100644 --- a/row.js +++ b/row.js @@ -1,7 +1,7 @@ -import React from 'react'; -import Title from './title'; -import {autobind} from 'core-decorators'; -import R from 'ramda'; +import React from "react"; +import Title from "./title"; +import { autobind } from "core-decorators"; +import R from "ramda"; export default class Row extends React.Component { static propTypes = { @@ -10,7 +10,7 @@ export default class Row extends React.Component { expanded: React.PropTypes.bool, name: React.PropTypes.string, node: React.PropTypes.object, - rowRenderer: React.PropTypes.func, + rowRenderer: React.PropTypes.func }; static defaultProps = { @@ -18,86 +18,106 @@ export default class Row extends React.Component { collapser: -, loader: ^, expanded: false, - depth: 0, - } + depth: 0 + }; constructor(props) { super(props); } - @autobind - handleExpand() { - if (typeof this.props.children === 'function') { - return this.props.children(); + @autobind handleExpand() { + if (typeof this.props.children === "function") { + return this.props.children(); } return this.setState({ - expanded: true + expanded: true }); } - @autobind - handleToggle(e) { + @autobind handleToggle(e) { this.props.onToggle(this.props.nodeId); e.stopPropagation(); } - @autobind - handleClick(event) { - this.props.onClick(event, this.props.nodeId, this.props.parentId); - event.stopPropagation(); + @autobind handleClick(event) { + this.props.onClick(event, this.props.nodeId, this.props.parentId); + event.stopPropagation(); } render() { - const {expander, collapser, nodeId, depth, nodes, selectedNodeIds, expandedNodeIds, loadingNodeIds, onToggle, onClick, loader, rowRenderer} = this.props; - const node = nodes.get('byId').get(nodeId); - const expanded = expandedNodeIds.indexOf(node.get('id')) >= 0; - const internalLoading = loadingNodeIds.indexOf(node.get('id')) >= 0; - const loading = node.get('isLoading'); - const lazyLoad = node.get('lazyLoad'); + const { + expander, + collapser, + nodeId, + depth, + nodes, + selectedNodeIds, + expandedNodeIds, + loadingNodeIds, + onToggle, + onClick, + loader, + rowRenderer + } = this.props; + const node = nodes.get("byId").get(nodeId); + const expanded = expandedNodeIds.indexOf(node.get("id")) >= 0; + const internalLoading = loadingNodeIds.indexOf(node.get("id")) >= 0; + const loading = node.get("isLoading"); + const lazyLoad = node.get("lazyLoad"); const internalOrExternalLoading = loading || internalLoading; - const expand = !expanded && (lazyLoad || (!expanded && ((node.get('childIds') - && !node.get('childIds').isEmpty())))) && + const expand = + !expanded && + (lazyLoad || (node.get("childIds") && !node.get("childIds").isEmpty())) && {expander}; - const collapse = expanded && node.get('childIds') && node.get('childIds').size && - {collapser} + const collapse = + expanded && + node.get("childIds") && + node.get("childIds").size && + {collapser}; const TitleRenderer = rowRenderer; const nodeState = { - expanded, - selected: selectedNodeIds.contains(nodeId), - depth, - } + expanded, + selected: selectedNodeIds.contains(nodeId), + depth + }; const nodeWithState = { - ...node.toJS(), - state: nodeState - } + ...node.toJS(), + state: nodeState + }; const toggler = expand || collapse; return ( -
+
- {!internalOrExternalLoading ? toggler : loader} + {!internalOrExternalLoading ? toggler : loader}
- +
- {expanded && node.get('childIds') && - node.get('childIds').map((childId) => { - const childNode = nodes.get('byId').get(childId); - return { + const childNode = nodes.get("byId").get(childId); + console.log("childNode ", childNode); + + return ( + })} + /> + ); + })}
); diff --git a/tree-component.js b/tree-component.js index 7837ae4..58b7b53 100644 --- a/tree-component.js +++ b/tree-component.js @@ -77,8 +77,14 @@ export default class TreeComponent extends React.Component { } filterCurrentNodes(newNodes, nodesToFilter) { + console.log( + "filterCurrentNodes ", + newNodes.get("byId"), + " ", + nodesToFilter + ); return nodesToFilter.filter(nodeId => - R.contains(nodeId, R.keys(newNodes.byId)) + R.contains(nodeId, R.keys(newNodes.get("byId").keys)) ); } @@ -92,14 +98,14 @@ export default class TreeComponent extends React.Component { loadingNodeIds }); this.setState({ - expandedNodeIds: this.filterCurrentNodes( - newNodes, - this.state.expandedNodeIds - ), - loadingNodeIds: this.filterCurrentNodes( - newNodes, - this.state.loadingNodeIds - ), + // expandedNodeIds: this.filterCurrentNodes( + // newNodes, + // this.state.expandedNodeIds + // ), + // loadingNodeIds: this.filterCurrentNodes( + // newNodes, + // this.state.loadingNodeIds + // ), selectedNodeIds: this.filterCurrentNodes( newNodes, this.state.selectedNodeIds From 3018b69caaa5a7ef22dbc7fc8d693ab74e3b38f3 Mon Sep 17 00:00:00 2001 From: Tushar Date: Mon, 29 May 2017 22:12:28 +0530 Subject: [PATCH 5/5] Fixed linting and made some UI enhancement --- App.js | 36 +++++----- __tests__/row.test.js | 86 ++++++++++++------------ __tests__/rowRenderer.test.js | 38 +++++------ __tests__/title.test.js | 15 +++++ __tests__/treeComponent.test.js | 6 +- __tests__/treeRow.test.js | 30 ++++----- index.html | 7 +- row-renderer.js | 20 +++--- row.js | 96 ++++++++++++++------------- styles.less | 112 +++++++++++++++++++++++--------- tree-component.js | 32 ++++----- 11 files changed, 280 insertions(+), 198 deletions(-) create mode 100644 __tests__/title.test.js diff --git a/App.js b/App.js index 6f42f6c..9678b90 100644 --- a/App.js +++ b/App.js @@ -1,15 +1,15 @@ -import React from "react"; -import { Map } from "immutable"; +import React from 'react'; +import { Map } from 'immutable'; -import { autobind } from "core-decorators"; -import R from "ramda"; -import classNames from "classnames"; -import Tree from "./tree-component"; -import "./styles.less"; -import { deleteNodeWithChildren } from "./helpers"; -import TreeRow from "./tree-row-component"; -import RowRenderer from "./row-renderer"; -import nodes from "./mock-data"; +import { autobind } from 'core-decorators'; +import R from 'ramda'; +import classNames from 'classnames'; +import Tree from './tree-component'; +import './styles.less'; +import { deleteNodeWithChildren } from './helpers'; +import TreeRow from './tree-row-component'; +import RowRenderer from './row-renderer'; +import nodes from './mock-data'; const MOCK_SERVER_TIME = 0; @@ -27,7 +27,7 @@ export default class App extends React.Component { }; getRandomWord() { return fetch( - "http://www.setgetgo.com/randomword/get.php?len=4" + 'http://www.setgetgo.com/randomword/get.php?len=4' ).then(resp => resp.text()); } @@ -53,7 +53,7 @@ export default class App extends React.Component { }; const byId = R.pipe( R.evolve({ - [nodeId]: R.assoc("childIds", [newNode1.id, newNode2.id]) + [nodeId]: R.assoc('childIds', [newNode1.id, newNode2.id]) }), R.assoc(newNode1.id, newNode1), R.assoc(newNode2.id, newNode2) @@ -71,7 +71,7 @@ export default class App extends React.Component { @autobind handleClick(event, nodeId, parentId) { const classNames = event.target.className; - if (R.contains("delete", classNames)) { + if (R.contains('delete', classNames)) { this.setState({ nodes: deleteNodeWithChildren(this.state.nodes, nodeId, parentId) }); @@ -80,9 +80,9 @@ export default class App extends React.Component { @autobind shouldSelectNode(node) { console.log( - "shouldSelectNode ", + 'shouldSelectNode ', node.id, - "---", + '---', this.tree.getSelectedNode() ); @@ -95,14 +95,14 @@ export default class App extends React.Component { render() { const { nodes } = this.state; return ( -
+
{ this.tree = c && c.state; - console.log("Tree ", c); + console.log('Tree ', c); }} shouldSelectNode={this.shouldSelectNode} rowRenderer={RowRenderer} diff --git a/__tests__/row.test.js b/__tests__/row.test.js index 0b00822..e52b7d6 100644 --- a/__tests__/row.test.js +++ b/__tests__/row.test.js @@ -1,97 +1,99 @@ -import React from "react"; -import { shallow, mount } from "enzyme"; -import { fromJS, List } from "immutable"; -import toJSON from "enzyme-to-json"; -import Row from "../row"; -import TitleComponent from "../title"; -import logger from "../utils/logger"; +import React from 'react'; +import { shallow, mount } from 'enzyme'; +import { fromJS, List } from 'immutable'; +import toJSON from 'enzyme-to-json'; +import Row from '../row'; +import TitleComponent from '../title'; +import logger from '../utils/logger'; const mockData = { byId: { 1: { - id: "1", - name: "One", - lazyLoad: true + id: '1', + name: 'One', + lazyLoad: true, }, 2: { - id: "2", - name: "Two", - childIds: ["2.1", "2.2"] + id: '2', + name: 'Two', + childIds: ['2.1', '2.2'], }, 2.1: { - id: "2.1", - name: "Two.One" + id: '2.1', + name: 'Two.One', }, 2.2: { - id: "2.2", - name: "Two.Two" - } - } + id: '2.2', + name: 'Two.Two', + }, + }, }; -describe("", () => { +describe('', () => { const states = { expander: +, collapser: -, - loader: ^ + loader: ^, }; const requiredProps = { expandedNodeIds: List(), loadingNodeIds: List(), nodes: fromJS(mockData), - nodeId: "2", - parentId: "2", + nodeId: '2', + parentId: '2', rowRenderer: TitleComponent, renderedNodeIds: List(), - selectedNodeIds: List() + selectedNodeIds: List(), }; - test("should load without crashing", () => { + test('should load without crashing', () => { const wrapper = shallow(); expect(wrapper.length).toBe(1); }); - test("should have required default props", () => { + test('should have required default props', () => { const wrapper = shallow(); - "expander,collapser,loader,expanded,depth".split(",").forEach(value => { + 'expander,collapser,loader,expanded,depth'.split(',').forEach((value) => { expect(wrapper.instance().props[value]).toBeDefined(); }); }); - test("should show expand icon if node have childs or is lazy loaded", () => {}); + test('should show expand icon if node have childs or is lazy loaded', () => {}); - test("should have required methods passed as props", () => { + test('should have required methods passed as props', () => { // onClick={this.handleClick} // onToggle={this.handleToggle} }); - test("should call `toggle` function when clicked on expand icon", () => { + test('should call `toggle` function when clicked on expand icon', () => { const onToggleFn = jest.fn(); const wrapper = mount(); // Row.prototype.mockToggleFn = jest.fn(); // const spy = jest.spyOn(Row.prototype, "mockToggleFn"); - let ele = wrapper + const ele = wrapper .find(`#row-${requiredProps.nodeId}`) - .children(".toggle-wrapper") - .find("a") - .simulate("click"); + .children('.node-header') + .children('.toggle-wrapper') + .find('a') + .simulate('click'); // expect(spy).toHaveBeenCalled(); expect(onToggleFn).toHaveBeenCalledWith(requiredProps.nodeId); // expect(toJSON(wrapper)).toMatchSnapshot(); }); - test("should call `onClick` function to `delete node` with proper args when clicked", () => { + test('should call `onClick` function to `delete node` with proper args when clicked', () => { const onClick = jest.fn(); const wrapper = mount(); const ele = wrapper .find(`#row-${requiredProps.nodeId}`) - .children(".title-wrapper"); - let event = ele.simulate("click"); - logger.log("ele", ele.length); - //Unable to test this + .children('.node-header') + .children('.title-wrapper'); + const event = ele.simulate('click'); + logger.log('ele', ele.length); + // Unable to test this // expect(onClick).toBeCalledWith(event,requiredProps.nodeId,requiredProps.parentId); expect(onClick).toBeCalled(); }); - test("should show loading indicator when node is lazily loaded", () => {}); - test("should select the node when user clicks on any node", () => {}); - test("should be able render the child nodes", () => {}); + test('should show loading indicator when node is lazily loaded', () => {}); + test('should select the node when user clicks on any node', () => {}); + test('should be able render the child nodes', () => {}); }); diff --git a/__tests__/rowRenderer.test.js b/__tests__/rowRenderer.test.js index 4251fda..b098741 100644 --- a/__tests__/rowRenderer.test.js +++ b/__tests__/rowRenderer.test.js @@ -1,41 +1,41 @@ -import React from "react"; -import RowRenderer from "../row-renderer"; -import logger from "../utils/logger"; -import toJson from "enzyme-to-json"; -import TreeRowComonent from "../tree-row-component"; -import { shallow, mount } from "enzyme"; +import React from 'react'; +import RowRenderer from '../row-renderer'; +import logger from '../utils/logger'; +import toJson from 'enzyme-to-json'; +import TreeRowComonent from '../tree-row-component'; +import { shallow, mount } from 'enzyme'; -describe("", () => { - test("should render without crashing", () => { +describe('', () => { + test('should render without crashing', () => { const mockData = { id: 1, - name: "Foo" + name: 'Foo', }; const wrapper = shallow(); expect(wrapper.length).toBe(1); }); - test("should allow user to rename the node", () => { + test('should allow user to rename the node', () => { const mockData = { id: 1, - name: "Foo", + name: 'Foo', renameMode: true, state: { - selected: false - } + selected: false, + }, }; const wrapper = mount( - + , ); - expect(wrapper.find(".infinite-tree-rename-input").length).toBe(0); - expect(wrapper.find(".infinite-tree-title").length).toBe(1); + expect(wrapper.find('.infinite-tree-rename-input').length).toBe(0); + expect(wrapper.find('.infinite-tree-title').length).toBe(1); wrapper.setProps({ node: mockData }); - expect(wrapper.find(".infinite-tree-rename-input").length).toBe(1); - expect(wrapper.find(".infinite-tree-title").length).toBe(0); + expect(wrapper.find('.infinite-tree-rename-input').length).toBe(1); + expect(wrapper.find('.infinite-tree-title').length).toBe(0); }); - it("should have default props defined", () => { + test('should have default props defined', () => { const wrapper = shallow(); expect(wrapper.instance().props.treeOptions).toEqual({}); }); diff --git a/__tests__/title.test.js b/__tests__/title.test.js new file mode 100644 index 0000000..b873f85 --- /dev/null +++ b/__tests__/title.test.js @@ -0,0 +1,15 @@ +import React from 'react'; +import { mount } from 'enzyme'; +import Title from '../title'; + +describe('', () => { + test('should render without crashing', () => { + const mockData = { + name: 'Foo', + id: '1', + }; + const wrapper = mount(<Title node={mockData} />); + // wrapper.setProps({ node: mockData }); + expect(wrapper.length).toBe(1); + }); +}); diff --git a/__tests__/treeComponent.test.js b/__tests__/treeComponent.test.js index 40907b0..26e1f02 100644 --- a/__tests__/treeComponent.test.js +++ b/__tests__/treeComponent.test.js @@ -1,7 +1,7 @@ -import React from "react"; +import React from 'react'; -describe("<Tree/>", () => { - test("should pass", () => { +describe('<Tree/>', () => { + test('should pass', () => { expect(1 === 1).toBe(true); }); diff --git a/__tests__/treeRow.test.js b/__tests__/treeRow.test.js index 60f952a..a7b3751 100644 --- a/__tests__/treeRow.test.js +++ b/__tests__/treeRow.test.js @@ -1,27 +1,27 @@ -import React from "react"; -import { shallow } from "enzyme"; -import toJSON from "enzyme-to-json"; -import TreeRowComponent from "../tree-row-component"; -import logger from "../utils/logger"; +import React from 'react'; +import { shallow } from 'enzyme'; +import toJSON from 'enzyme-to-json'; +import TreeRowComponent from '../tree-row-component'; +import logger from '../utils/logger'; -describe("<TreeRowComponent/>", () => { - test("should load without crashing", () => {}); +describe('<TreeRowComponent/>', () => { + test('should load without crashing', () => {}); - test("should add appropriate class whenever a node is selected", () => { + test('should add appropriate class whenever a node is selected', () => { const mockData = { state: {} }; const wrapper = shallow(<TreeRowComponent node={mockData} />); - let component = wrapper.find(".infinite-tree-item"); - let classNames = component.props().className.split(" "); + let component = wrapper.find('.infinite-tree-item'); + let classNames = component.props().className.split(' '); - expect(classNames).not.toContain("infinite-tree-selected"); + expect(classNames).not.toContain('infinite-tree-selected'); wrapper.setProps({ - node: Object.assign({}, mockData, { state: { selected: true } }) + node: Object.assign({}, mockData, { state: { selected: true } }), }); - component = wrapper.find(".infinite-tree-item"); - classNames = component.props().className.split(" "); + component = wrapper.find('.infinite-tree-item'); + classNames = component.props().className.split(' '); - expect(classNames).toContain("infinite-tree-selected"); + expect(classNames).toContain('infinite-tree-selected'); }); }); diff --git a/index.html b/index.html index 02c72f6..b2d9bb6 100644 --- a/index.html +++ b/index.html @@ -1,15 +1,18 @@ <!DOCTYPE html> <html> + <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> - <title> + + Infinite Tree +
-
+ \ No newline at end of file diff --git a/row-renderer.js b/row-renderer.js index a7aacff..b153d71 100644 --- a/row-renderer.js +++ b/row-renderer.js @@ -1,6 +1,6 @@ -import React, { PropTypes } from "react"; -import classNames from "classnames"; -import TreeRow from "./tree-row-component"; +import React, { PropTypes } from 'react'; +import classNames from 'classnames'; +import TreeRow from './tree-row-component'; const RowRendererComponent = props => { const { node, treeOptions } = props; @@ -8,24 +8,24 @@ const RowRendererComponent = props => { const { deleteNode } = treeOptions; const label = renameMode ? - : {name}; + : {name}; const className = classNames( - "infinite-tree-closed", - { "context-menu-button": contextMenu }, - "pull-right" + 'infinite-tree-closed', + { 'context-menu-button': contextMenu }, + 'pull-right' ); return ( {label} - D + D ); }; diff --git a/row.js b/row.js index 9729bde..9a0f158 100644 --- a/row.js +++ b/row.js @@ -1,7 +1,7 @@ -import React from "react"; -import Title from "./title"; -import { autobind } from "core-decorators"; -import R from "ramda"; +import React from 'react'; +import Title from './title'; +import { autobind } from 'core-decorators'; +import R from 'ramda'; export default class Row extends React.Component { static propTypes = { @@ -14,8 +14,8 @@ export default class Row extends React.Component { }; static defaultProps = { - expander: +, - collapser: -, + expander: , + collapser: , loader: ^, expanded: false, depth: 0 @@ -26,7 +26,7 @@ export default class Row extends React.Component { } @autobind handleExpand() { - if (typeof this.props.children === "function") { + if (typeof this.props.children === 'function') { return this.props.children(); } return this.setState({ @@ -59,23 +59,24 @@ export default class Row extends React.Component { loader, rowRenderer } = this.props; - const node = nodes.get("byId").get(nodeId); - const expanded = expandedNodeIds.indexOf(node.get("id")) >= 0; - const internalLoading = loadingNodeIds.indexOf(node.get("id")) >= 0; - const loading = node.get("isLoading"); - const lazyLoad = node.get("lazyLoad"); + const node = nodes.get('byId').get(nodeId); + const expanded = expandedNodeIds.indexOf(node.get('id')) >= 0; + const internalLoading = loadingNodeIds.indexOf(node.get('id')) >= 0; + const loading = node.get('isLoading'); + const lazyLoad = node.get('lazyLoad'); const internalOrExternalLoading = loading || internalLoading; const expand = !expanded && - (lazyLoad || (node.get("childIds") && !node.get("childIds").isEmpty())) && + (lazyLoad || (node.get('childIds') && !node.get('childIds').isEmpty())) && {expander}; const collapse = expanded && - node.get("childIds") && - node.get("childIds").size && + node.get('childIds') && + node.get('childIds').size && {collapser}; + const TitleRenderer = rowRenderer; const nodeState = { expanded, @@ -88,36 +89,43 @@ export default class Row extends React.Component { }; const toggler = expand || collapse; return ( -
-
- {!internalOrExternalLoading ? toggler : loader} -
-
- +
+
+
+ {!internalOrExternalLoading ? toggler : loader} +
+
+ {expand + ?