Meilleure gestion arbre immutable
This commit is contained in:
parent
d8a0136a99
commit
519ec99264
@ -294,6 +294,7 @@ html, body {
|
||||
padding: 0 5px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.edit .profile-tree ul {
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -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));
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
};
|
||||
|
@ -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
|
||||
});
|
||||
|
@ -1,3 +1,4 @@
|
||||
exports.desktopApps = require('./desktop-apps');
|
||||
exports.profile = require('./profile');
|
||||
exports.theme = require('./theme');
|
||||
exports.selectedItem = require('./selected-item');
|
||||
|
@ -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();
|
||||
|
||||
}
|
||||
|
17
src/store/reducers/selected-item.js
Normal file
17
src/store/reducers/selected-item.js
Normal 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;
|
||||
|
||||
};
|
@ -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
65
test/tree.js
Normal 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();
|
||||
|
||||
};
|
Loading…
Reference in New Issue
Block a user