From 519ec99264f33f110be51726fd92e9761dde0959 Mon Sep 17 00:00:00 2001 From: William Petit Date: Tue, 3 Nov 2015 17:45:37 +0100 Subject: [PATCH] Meilleure gestion arbre immutable --- css/style.css | 1 + package.json | 2 + src/components/edit/edit-view.js | 3 +- src/components/edit/profile-tree.js | 5 +- src/store/actions/edit.js | 34 ++++++-- src/store/index.js | 2 +- src/store/reducers/index.js | 1 + src/store/reducers/profile.js | 122 +++++++++++++--------------- src/store/reducers/selected-item.js | 17 ++++ src/util/tree.js | 98 +++++++++++++--------- test/tree.js | 65 +++++++++++++++ 11 files changed, 236 insertions(+), 114 deletions(-) create mode 100644 src/store/reducers/selected-item.js create mode 100644 test/tree.js diff --git a/css/style.css b/css/style.css index ce66762..bf189d3 100644 --- a/css/style.css +++ b/css/style.css @@ -294,6 +294,7 @@ html, body { padding: 0 5px; width: 100%; height: 100%; + overflow-y: auto; } .edit .profile-tree ul { diff --git a/package.json b/package.json index 7c02a5f..0294413 100644 --- a/package.json +++ b/package.json @@ -27,9 +27,11 @@ "lodash": "^3.10.1", "react": "^0.14.0", "react-addons-css-transition-group": "^0.14.0", + "react-addons-update": "^0.14.2", "react-dnd": "^1.1.5", "react-dom": "^0.14.0", "react-redux": "^2.0.0", + "recursive-iterator": "^2.0.0", "redux": "^2.0.0", "redux-thunk": "^0.1.0", "winston": "^1.1.2" diff --git a/src/components/edit/edit-view.js b/src/components/edit/edit-view.js index 551d300..674aac4 100644 --- a/src/components/edit/edit-view.js +++ b/src/components/edit/edit-view.js @@ -5,7 +5,6 @@ var DesktopAppList = require('./desktop-app-list.js'); var ItemForm = require('./item-form.js'); var IconThemeSelector = require('./icon-theme-selector.js'); var ProfileMenu = require('./profile-menu.js'); -var tree = require('../../util/tree'); var actions = require('../../store/actions'); var DragDropContext = require('react-dnd').DragDropContext; @@ -96,7 +95,7 @@ function select(state) { desktopApps: state.desktopApps, profile: state.profile, theme: state.theme, - selectedItem: tree.matches(state.profile, {selected: true})[0] + selectedItem: state.selectedItem }; } diff --git a/src/components/edit/profile-tree.js b/src/components/edit/profile-tree.js index 17f7c47..3d00b42 100644 --- a/src/components/edit/profile-tree.js +++ b/src/components/edit/profile-tree.js @@ -44,7 +44,7 @@ var TreeNode = React.createClass({ renderTreeItem: function(data) { return ( ); } @@ -93,7 +93,8 @@ var ProfileTree = React.createClass({ function select(state) { return { profile: state.profile, - theme: state.theme + theme: state.theme, + selectedItem: state.selectedItem }; } diff --git a/src/store/actions/edit.js b/src/store/actions/edit.js index 82a916a..91000e7 100644 --- a/src/store/actions/edit.js +++ b/src/store/actions/edit.js @@ -1,4 +1,5 @@ var Util = require('../../util'); +var Tree = require('../../util/tree'); var logger = Util.Logger; var path = require('path'); var _ = require('lodash'); @@ -111,10 +112,33 @@ exports.selectProfileItem = function(item) { }; exports.updateProfileItem = function(item, key, value) { - return { - type: UPDATE_PROFILE_ITEM, - item: item, - key: key, - value: value + return function(dispatch, getState) { + + var state = getState(); + var selectedPath, tree; + + // If the item is selected, save its path + if(state.selectedItem === item) { + tree = new Tree(state.profile); + var result = tree.find(item); + selectedPath = result.path; + } + + dispatch({ + type: UPDATE_PROFILE_ITEM, + item: item, + key: key, + value: value + }); + + // Re-select item if needed + if(selectedPath) { + state = getState(); + tree = new Tree(state.profile); + var selectedItem = tree.get(selectedPath); + dispatch(exports.selectProfileItem(selectedItem)); + } + }; + }; diff --git a/src/store/index.js b/src/store/index.js index c448a5b..e4044f5 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -10,7 +10,7 @@ var createStore = redux.applyMiddleware( var appReducer = redux.combineReducers({ profile: reducers.profile, - processOpts: reducers.processOpts, + selectedItem: reducers.selectedItem, desktopApps: reducers.desktopApps, theme: reducers.theme }); diff --git a/src/store/reducers/index.js b/src/store/reducers/index.js index 0207160..4b63917 100644 --- a/src/store/reducers/index.js +++ b/src/store/reducers/index.js @@ -1,3 +1,4 @@ exports.desktopApps = require('./desktop-apps'); exports.profile = require('./profile'); exports.theme = require('./theme'); +exports.selectedItem = require('./selected-item'); diff --git a/src/store/reducers/profile.js b/src/store/reducers/profile.js index 454a82d..980889f 100644 --- a/src/store/reducers/profile.js +++ b/src/store/reducers/profile.js @@ -1,6 +1,6 @@ var _ = require('lodash'); var actions = require('../actions'); -var tree = require('../../util/tree'); +var Tree = require('../../util/tree'); module.exports = function(oldProfile, action) { @@ -28,84 +28,76 @@ module.exports = function(oldProfile, action) { newProfile = updateProfileItem(oldProfile, action.item, action.key, action.value); break; - case actions.edit.SELECT_PROFILE_ITEM: - newProfile = selectProfileItem(oldProfile, action.item); - break; - } - if(newProfile) tree.walk(newProfile, ensureItemKey); - return newProfile; }; -function selectProfileItem(oldProfile, item) { - var newProfile = _.cloneDeep(oldProfile); - tree.walk(newProfile, function(currentItem) { - delete currentItem.selected; - if( _.isEqual(currentItem, item) ) { - currentItem.selected = true; - } - }); - return newProfile; +function updateProfileItem(profile, targetItem, key, value) { + var tree = new Tree(profile); + var result = tree.find(targetItem); + var itemPath = result.path; + tree.update(itemPath.concat(key), {$set: value}); + return tree.getState(); } -function updateProfileItem(oldProfile, targetItem, key, value) { - var newProfile = _.cloneDeep(oldProfile); - var item = tree.find(newProfile, targetItem).item; - item[key] = value; - return newProfile; +function removeProfileItem(profile, removedItem) { + var tree = new Tree(profile); + var result = tree.find(removedItem); + tree.del(result.path); + return tree.getState(); } -function removeProfileItem(oldProfile, removedItem) { +function moveProfileItem(profile, movedItem, targetItem) { - var newProfile = _.cloneDeep(oldProfile); - var parent = tree.find(newProfile, removedItem).parent; + var tree = new Tree(profile); - parent.items = _.reject(parent.items, function(item) { - return _.isEqual(item, removedItem); - }); + var movedResult = tree.find(movedItem); + var targetResult = tree.find(targetItem); - return newProfile; + // Remove item from current location + tree.del(movedResult.path); -} + var targetPath = targetResult ? targetResult.path : []; + var targetItemsPath = targetPath.concat('items'); -function moveProfileItem(oldProfile, movedItem, targetItem) { - - var newProfile = _.cloneDeep(oldProfile); - var previousParent = tree.find(newProfile, movedItem).parent; - var newParent = tree.find(newProfile, targetItem).item; - - previousParent.items = _.reject(previousParent.items, function(item) { - return _.isEqual(item, movedItem); - }); - - newParent.items = newParent.items || []; - newParent.items.push(_.cloneDeep(movedItem)); - - return newProfile; - -} - -function addProfileItem(oldProfile, newItem, targetItem) { - - var newProfile = _.cloneDeep(oldProfile); - var newParent = tree.find(newProfile, targetItem).item; - - newParent.items = newParent.items || []; - - newItem = _.cloneDeep(newItem); - ensureItemKey(newItem); - - newParent.items.push(newItem); - - return newProfile; -} - -var _inc = 0; -function ensureItemKey(item) { - if( item && !('_key' in item) ) { - item._key = 'item_'+Date.now()+'_'+_inc++; + // Create "items" collection if not defined on target item + if( !('items' in targetItem) ) { + tree.update(targetItemsPath, { + "$set": [] + }); } + + // Add moved item to target + tree.update(targetItemsPath, { + "$push": [movedItem] + }); + + return tree.getState(); + +} + +function addProfileItem(profile, newItem, targetItem) { + + var tree = new Tree(profile); + + var targetResult = tree.find(targetItem); + var targetPath = targetResult ? targetResult.path : []; + var targetItemsPath = targetPath.concat('items'); + + // Create "items" collection if not defined on target item + if( !('items' in targetItem) ) { + tree.update(targetItemsPath, { + "$set": [] + }); + } + + // Add moved item to target + tree.update(targetItemsPath, { + "$push": [newItem] + }); + + return tree.getState(); + } diff --git a/src/store/reducers/selected-item.js b/src/store/reducers/selected-item.js new file mode 100644 index 0000000..e97e838 --- /dev/null +++ b/src/store/reducers/selected-item.js @@ -0,0 +1,17 @@ +var actions = require('../actions'); + +module.exports = function(selectedItem, action) { + + switch(action.type) { + + case actions.edit.SELECT_PROFILE_ITEM: + selectedItem = action.item; + break; + + default: + return selectedItem || null; + } + + return selectedItem; + +}; diff --git a/src/util/tree.js b/src/util/tree.js index 76d2c97..b685d53 100644 --- a/src/util/tree.js +++ b/src/util/tree.js @@ -1,53 +1,73 @@ var _ = require('lodash'); +var update = require('react-addons-update'); +var RecursiveIterator = require('recursive-iterator'); -// Tree manipulation helpers +function Tree(srcState) { + this._keyCount = 0; + this._state = srcState || {}; +} -exports.walk = function(branch, func, parent) { +var p = Tree.prototype; - if(!branch) return; - - var breakHere = func(branch, parent); - - if(breakHere) return breakHere; - - var items = branch.items; - - if(!items) return; - - for( var i = 0, item = items[i]; (item = items[i]); i++ ) { - breakHere = exports.walk(item, func, branch); - if(breakHere) return breakHere; +p.get = function(pathArr) { + var obj = this._state; + for(var i = 0, len = pathArr.length; i < len; ++i) { + obj = obj[pathArr[i]]; + if(!obj) throw new Error('Unexistant tree path: "'+pathArr.join('.')+'"'); } - + return obj; }; -exports.find = function(tree, obj) { +p.update = function(pathArr, updateCommands) { + var updateQuery = this._createUpdateQueryForPath(pathArr, updateCommands); + this._state = update(this._state, updateQuery); + return this; +}; - var result; - - exports.walk(tree, function(item, parent) { - if( _.isEqual(item, obj) ) { - result = {item: item, parent: parent}; - return true; +p.del = function(pathArr) { + var prop = pathArr[pathArr.length-1]; + var parentPath = pathArr.slice(0, -1); + this.update(parentPath, { + "$apply": function(item) { + delete item[prop]; + return item; } }); - - return result; - + return this; }; -exports.matches = function(tree, obj) { - - var results = []; - - var matches = _.matches(obj); - - exports.walk(tree, function(item) { - if( matches(item) ) { - results.push(item); +p.find = function(obj) { + var iterator = this._getStateIterator(); + for(var item = iterator.next(); !item.done; item = iterator.next()) { + var state = item.value; + if(state.node === obj) { + return state; } - }); - - return results; - + } }; + +p.getState = function() { + return this._state; +}; + +p.toJSON = function() { + return this._state; +}; + + +p._getStateIterator = function() { + var iterator = new RecursiveIterator(this._state); + return iterator; +}; + +p._createUpdateQueryForPath = function(pathArr, updateCommands) { + var cursor, query = {}; + cursor = query; + for(var i = 0, len = pathArr.length; i < len-1; ++i) { + cursor = cursor[pathArr[i]] = {}; + } + cursor[pathArr[pathArr.length-1]] = updateCommands; + return query; +}; + +module.exports = Tree; diff --git a/test/tree.js b/test/tree.js new file mode 100644 index 0000000..df4bdd3 --- /dev/null +++ b/test/tree.js @@ -0,0 +1,65 @@ +var Tree = require('../src/util/tree'); + +var TreeSuite = module.exports = {}; + +TreeSuite.getPropertyByPath = function(test) { + var tree = new Tree(this.treeState); + var label = tree.get(['items', 1, 'label']); + test.ok(label === 'root.child2', 'The property value should be "root.child2" !'); + test.done(); +}; + +TreeSuite.updatePropertyByPath = function(test) { + var tree = new Tree(this.treeState); + var path = ['items', 1, 'label']; + tree.update(path, {$set: 'foo'}); + var label = tree.get(path); + test.ok(label === 'foo', 'The property value should be "foo" !'); + test.done(); +}; + +TreeSuite.findItem = function(test) { + var tree = new Tree(this.treeState); + var result = tree.find(this.treeItem); + test.ok(result.path.join('.') === 'items.1.items.0.items.0', 'The result should have a path equal to "items.1.items.0.items.0" !'); + test.done(); +}; + +TreeSuite.setUp = function(done) { + + this.treeItem = { + label: "root.child2.child1.child1", + items: [] + }; + + this.treeState = { + label: "root", + items: [ + { + label: "root.child1", + items: [ + { + label: "root.child1.child1", + items: [ + + ] + } + ] + }, + { + label: "root.child2", + items: [ + { + label: "root.child2.child1", + items: [ + this.treeItem + ] + } + ] + } + ] + }; + + done(); + +};