Meilleure gestion arbre immutable
This commit is contained in:
parent
d8a0136a99
commit
519ec99264
|
@ -294,6 +294,7 @@ html, body {
|
||||||
padding: 0 5px;
|
padding: 0 5px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.edit .profile-tree ul {
|
.edit .profile-tree ul {
|
||||||
|
|
|
@ -27,9 +27,11 @@
|
||||||
"lodash": "^3.10.1",
|
"lodash": "^3.10.1",
|
||||||
"react": "^0.14.0",
|
"react": "^0.14.0",
|
||||||
"react-addons-css-transition-group": "^0.14.0",
|
"react-addons-css-transition-group": "^0.14.0",
|
||||||
|
"react-addons-update": "^0.14.2",
|
||||||
"react-dnd": "^1.1.5",
|
"react-dnd": "^1.1.5",
|
||||||
"react-dom": "^0.14.0",
|
"react-dom": "^0.14.0",
|
||||||
"react-redux": "^2.0.0",
|
"react-redux": "^2.0.0",
|
||||||
|
"recursive-iterator": "^2.0.0",
|
||||||
"redux": "^2.0.0",
|
"redux": "^2.0.0",
|
||||||
"redux-thunk": "^0.1.0",
|
"redux-thunk": "^0.1.0",
|
||||||
"winston": "^1.1.2"
|
"winston": "^1.1.2"
|
||||||
|
|
|
@ -5,7 +5,6 @@ var DesktopAppList = require('./desktop-app-list.js');
|
||||||
var ItemForm = require('./item-form.js');
|
var ItemForm = require('./item-form.js');
|
||||||
var IconThemeSelector = require('./icon-theme-selector.js');
|
var IconThemeSelector = require('./icon-theme-selector.js');
|
||||||
var ProfileMenu = require('./profile-menu.js');
|
var ProfileMenu = require('./profile-menu.js');
|
||||||
var tree = require('../../util/tree');
|
|
||||||
|
|
||||||
var actions = require('../../store/actions');
|
var actions = require('../../store/actions');
|
||||||
var DragDropContext = require('react-dnd').DragDropContext;
|
var DragDropContext = require('react-dnd').DragDropContext;
|
||||||
|
@ -96,7 +95,7 @@ function select(state) {
|
||||||
desktopApps: state.desktopApps,
|
desktopApps: state.desktopApps,
|
||||||
profile: state.profile,
|
profile: state.profile,
|
||||||
theme: state.theme,
|
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) {
|
renderTreeItem: function(data) {
|
||||||
return (
|
return (
|
||||||
<TreeItem data={data}
|
<TreeItem data={data}
|
||||||
selected={data.selected}
|
selected={data === this.props.selectedItem}
|
||||||
{...this.props} />
|
{...this.props} />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -93,7 +93,8 @@ var ProfileTree = React.createClass({
|
||||||
function select(state) {
|
function select(state) {
|
||||||
return {
|
return {
|
||||||
profile: state.profile,
|
profile: state.profile,
|
||||||
theme: state.theme
|
theme: state.theme,
|
||||||
|
selectedItem: state.selectedItem
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
var Util = require('../../util');
|
var Util = require('../../util');
|
||||||
|
var Tree = require('../../util/tree');
|
||||||
var logger = Util.Logger;
|
var logger = Util.Logger;
|
||||||
var path = require('path');
|
var path = require('path');
|
||||||
var _ = require('lodash');
|
var _ = require('lodash');
|
||||||
|
@ -111,10 +112,33 @@ exports.selectProfileItem = function(item) {
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.updateProfileItem = function(item, key, value) {
|
exports.updateProfileItem = function(item, key, value) {
|
||||||
return {
|
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,
|
type: UPDATE_PROFILE_ITEM,
|
||||||
item: item,
|
item: item,
|
||||||
key: key,
|
key: key,
|
||||||
value: value
|
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({
|
var appReducer = redux.combineReducers({
|
||||||
profile: reducers.profile,
|
profile: reducers.profile,
|
||||||
processOpts: reducers.processOpts,
|
selectedItem: reducers.selectedItem,
|
||||||
desktopApps: reducers.desktopApps,
|
desktopApps: reducers.desktopApps,
|
||||||
theme: reducers.theme
|
theme: reducers.theme
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
exports.desktopApps = require('./desktop-apps');
|
exports.desktopApps = require('./desktop-apps');
|
||||||
exports.profile = require('./profile');
|
exports.profile = require('./profile');
|
||||||
exports.theme = require('./theme');
|
exports.theme = require('./theme');
|
||||||
|
exports.selectedItem = require('./selected-item');
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
var _ = require('lodash');
|
var _ = require('lodash');
|
||||||
var actions = require('../actions');
|
var actions = require('../actions');
|
||||||
var tree = require('../../util/tree');
|
var Tree = require('../../util/tree');
|
||||||
|
|
||||||
module.exports = function(oldProfile, action) {
|
module.exports = function(oldProfile, action) {
|
||||||
|
|
||||||
|
@ -28,84 +28,76 @@ module.exports = function(oldProfile, action) {
|
||||||
newProfile = updateProfileItem(oldProfile, action.item, action.key, action.value);
|
newProfile = updateProfileItem(oldProfile, action.item, action.key, action.value);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case actions.edit.SELECT_PROFILE_ITEM:
|
|
||||||
newProfile = selectProfileItem(oldProfile, action.item);
|
|
||||||
break;
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if(newProfile) tree.walk(newProfile, ensureItemKey);
|
|
||||||
|
|
||||||
return newProfile;
|
return newProfile;
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function selectProfileItem(oldProfile, item) {
|
function updateProfileItem(profile, targetItem, key, value) {
|
||||||
var newProfile = _.cloneDeep(oldProfile);
|
var tree = new Tree(profile);
|
||||||
tree.walk(newProfile, function(currentItem) {
|
var result = tree.find(targetItem);
|
||||||
delete currentItem.selected;
|
var itemPath = result.path;
|
||||||
if( _.isEqual(currentItem, item) ) {
|
tree.update(itemPath.concat(key), {$set: value});
|
||||||
currentItem.selected = true;
|
return tree.getState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function removeProfileItem(profile, removedItem) {
|
||||||
|
var tree = new Tree(profile);
|
||||||
|
var result = tree.find(removedItem);
|
||||||
|
tree.del(result.path);
|
||||||
|
return tree.getState();
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveProfileItem(profile, movedItem, targetItem) {
|
||||||
|
|
||||||
|
var tree = new Tree(profile);
|
||||||
|
|
||||||
|
var movedResult = tree.find(movedItem);
|
||||||
|
var targetResult = tree.find(targetItem);
|
||||||
|
|
||||||
|
// Remove item from current location
|
||||||
|
tree.del(movedResult.path);
|
||||||
|
|
||||||
|
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": []
|
||||||
});
|
});
|
||||||
return newProfile;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateProfileItem(oldProfile, targetItem, key, value) {
|
// Add moved item to target
|
||||||
var newProfile = _.cloneDeep(oldProfile);
|
tree.update(targetItemsPath, {
|
||||||
var item = tree.find(newProfile, targetItem).item;
|
"$push": [movedItem]
|
||||||
item[key] = value;
|
|
||||||
return newProfile;
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeProfileItem(oldProfile, removedItem) {
|
|
||||||
|
|
||||||
var newProfile = _.cloneDeep(oldProfile);
|
|
||||||
var parent = tree.find(newProfile, removedItem).parent;
|
|
||||||
|
|
||||||
parent.items = _.reject(parent.items, function(item) {
|
|
||||||
return _.isEqual(item, removedItem);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return newProfile;
|
return tree.getState();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function moveProfileItem(oldProfile, movedItem, targetItem) {
|
function addProfileItem(profile, newItem, targetItem) {
|
||||||
|
|
||||||
var newProfile = _.cloneDeep(oldProfile);
|
var tree = new Tree(profile);
|
||||||
var previousParent = tree.find(newProfile, movedItem).parent;
|
|
||||||
var newParent = tree.find(newProfile, targetItem).item;
|
|
||||||
|
|
||||||
previousParent.items = _.reject(previousParent.items, function(item) {
|
var targetResult = tree.find(targetItem);
|
||||||
return _.isEqual(item, movedItem);
|
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]
|
||||||
});
|
});
|
||||||
|
|
||||||
newParent.items = newParent.items || [];
|
return tree.getState();
|
||||||
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++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
};
|
100
src/util/tree.js
100
src/util/tree.js
|
@ -1,53 +1,73 @@
|
||||||
var _ = require('lodash');
|
var _ = require('lodash');
|
||||||
|
var update = require('react-addons-update');
|
||||||
|
var RecursiveIterator = require('recursive-iterator');
|
||||||
|
|
||||||
// Tree manipulation helpers
|
function Tree(srcState) {
|
||||||
|
this._keyCount = 0;
|
||||||
exports.walk = function(branch, func, parent) {
|
this._state = srcState || {};
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var p = Tree.prototype;
|
||||||
|
|
||||||
|
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;
|
p.del = function(pathArr) {
|
||||||
|
var prop = pathArr[pathArr.length-1];
|
||||||
exports.walk(tree, function(item, parent) {
|
var parentPath = pathArr.slice(0, -1);
|
||||||
if( _.isEqual(item, obj) ) {
|
this.update(parentPath, {
|
||||||
result = {item: item, parent: parent};
|
"$apply": function(item) {
|
||||||
return true;
|
delete item[prop];
|
||||||
|
return item;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
return this;
|
||||||
return result;
|
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.matches = function(tree, obj) {
|
p.find = function(obj) {
|
||||||
|
var iterator = this._getStateIterator();
|
||||||
var results = [];
|
for(var item = iterator.next(); !item.done; item = iterator.next()) {
|
||||||
|
var state = item.value;
|
||||||
var matches = _.matches(obj);
|
if(state.node === obj) {
|
||||||
|
return state;
|
||||||
exports.walk(tree, function(item) {
|
}
|
||||||
if( matches(item) ) {
|
|
||||||
results.push(item);
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
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;
|
||||||
|
|
|
@ -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