Meilleure gestion arbre immutable

This commit is contained in:
wpetit 2015-11-03 17:45:37 +01:00
parent d8a0136a99
commit 519ec99264
11 changed files with 236 additions and 114 deletions

View File

@ -294,6 +294,7 @@ html, body {
padding: 0 5px;
width: 100%;
height: 100%;
overflow-y: auto;
}
.edit .profile-tree ul {

View File

@ -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"

View File

@ -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
};
}

View File

@ -44,7 +44,7 @@ var TreeNode = React.createClass({
renderTreeItem: function(data) {
return (
<TreeItem data={data}
selected={data.selected}
selected={data === this.props.selectedItem}
{...this.props} />
);
}
@ -93,7 +93,8 @@ var ProfileTree = React.createClass({
function select(state) {
return {
profile: state.profile,
theme: state.theme
theme: state.theme,
selectedItem: state.selectedItem
};
}

View File

@ -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));
}
};
};

View File

@ -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
});

View File

@ -1,3 +1,4 @@
exports.desktopApps = require('./desktop-apps');
exports.profile = require('./profile');
exports.theme = require('./theme');
exports.selectedItem = require('./selected-item');

View File

@ -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();
}

View File

@ -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;
};

View File

@ -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;

65
test/tree.js Normal file
View File

@ -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();
};