diff --git a/web/client/components/TOC/DefaultLayerOrGroup.jsx b/web/client/components/TOC/DefaultLayerOrGroup.jsx new file mode 100644 index 0000000000..0dce2b67a8 --- /dev/null +++ b/web/client/components/TOC/DefaultLayerOrGroup.jsx @@ -0,0 +1,25 @@ +/** + * Copyright 2017, GeoSolutions Sas. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ +var React = require('react'); + +const DefaultLayerOrGroup = React.createClass({ + propTypes: { + node: React.PropTypes.object, + groupElement: React.PropTypes.element.isRequired, + layerElement: React.PropTypes.element.isRequired + }, + render() { + const {groupElement: DefaultGroup, layerElement: DefaultLayer, node, ...other} = this.props; + if (node.nodes) { + return React.cloneElement(DefaultGroup, {node, ...other}, ()); + } + return (React.cloneElement(DefaultLayer, {node, ...other})); + } +}); + +module.exports = DefaultLayerOrGroup; diff --git a/web/client/components/TOC/fragments/GroupTitle.jsx b/web/client/components/TOC/fragments/GroupTitle.jsx index 4d926dd8ef..7b395ef707 100644 --- a/web/client/components/TOC/fragments/GroupTitle.jsx +++ b/web/client/components/TOC/fragments/GroupTitle.jsx @@ -31,7 +31,7 @@ const GroupTitle = React.createClass({ let expanded = (this.props.node.expanded !== undefined) ? this.props.node.expanded : true; let groupTitle = this.props.node && this.props.node.title || 'Default'; return ( -
this.props.onClick(this.props.node.name, expanded)} style={this.props.style}> +
this.props.onClick(this.props.node.id, expanded)} style={this.props.style}> {groupTitle}
); diff --git a/web/client/components/TOC/fragments/settings/General.jsx b/web/client/components/TOC/fragments/settings/General.jsx index 8f7b476229..288ecf2273 100644 --- a/web/client/components/TOC/fragments/settings/General.jsx +++ b/web/client/components/TOC/fragments/settings/General.jsx @@ -10,6 +10,8 @@ var React = require('react'); const {Input} = require('react-bootstrap'); const Message = require('../../../I18N/Message'); const {SimpleSelect} = require('react-selectize'); +const {isObject} = require('lodash'); + require('react-selectize/themes/index.css'); /** @@ -27,6 +29,18 @@ const General = React.createClass({ updateSettings: () => {} }; }, + getGroups(groups, idx = 0) { + return groups.filter((group) => group.nodes).reduce((acc, g) => { + acc.push({label: g.id.replace(/\./g, '/'), value: g.id}); + if (g.nodes.length > 0) { + return acc.concat(this.getGroups(g.nodes, idx + 1)); + } + return acc; + }, []); + }, + getLabelName(groupLable = "") { + return groupLable.replace(/\./g, '/'); + }, render() { return (
} @@ -46,12 +60,15 @@ const General = React.createClass({ g.name)) || (this.props.element && this.props.element.group) || []).map(function(item) { - return {label: item, value: item}; + ((this.props.groups && this.getGroups(this.props.groups)) || (this.props.element && this.props.element.group) || []).map(function(item) { + if (isObject(item)) { + return item; + } + return {label: this.getLabelName(item), value: item}; }) } - defaultValue={{label: this.props.element && this.props.element.group || "Default", value: this.props.element && this.props.element.group || "Default" }} - placeholder={this.props.element && this.props.element.group || "Default"} + defaultValue={{label: this.getLabelName((this.props.element && this.props.element.group || "Default")), value: this.props.element && this.props.element.group || "Default" }} + placeholder={this.getLabelName((this.props.element && this.props.element.group || "Default"))} onChange={(value) => { this.updateEntry("group", {target: {value: value || "Default"}}); }} @@ -64,7 +81,8 @@ const General = React.createClass({ })).indexOf(search) > -1) { return null; } - return {label: search, value: search}; + const val = search.replace(/\//g, '.'); + return {label: val, value: val}; }} onValueChange={function(item) { diff --git a/web/client/plugins/TOC.jsx b/web/client/plugins/TOC.jsx index d847c7e184..0ee8c4acb7 100644 --- a/web/client/plugins/TOC.jsx +++ b/web/client/plugins/TOC.jsx @@ -138,6 +138,7 @@ const tocSelector = createSelector( const TOC = require('../components/TOC/TOC'); const DefaultGroup = require('../components/TOC/DefaultGroup'); const DefaultLayer = require('../components/TOC/DefaultLayer'); +const DefaultLayerOrGroup = require('../components/TOC/DefaultLayerOrGroup'); const LayerTree = React.createClass({ propTypes: { @@ -194,19 +195,14 @@ const LayerTree = React.createClass({ return group.name !== 'background'; }, renderTOC() { - - return ( -
- - - ); + const Layer = (} saveText={} closeText={} - groups={this.props.groups}/> - + groups={this.props.groups}/>); + return ( +
+ +
); diff --git a/web/client/reducers/__tests__/layers-test.js b/web/client/reducers/__tests__/layers-test.js index 1cc55d1e80..2f606737b1 100644 --- a/web/client/reducers/__tests__/layers-test.js +++ b/web/client/reducers/__tests__/layers-test.js @@ -79,7 +79,7 @@ describe('Test the layers reducer', () => { nodeType: 'groups' }; let initialState = { - groups: [{name: 'sample1'}, {name: 'sample2'}], + groups: [{name: 'sample1', id: 'sample1'}, {name: 'sample2', id: 'sample2'}], flat: [{id: 'layer1', group: 'sample1'}, {id: 'layer2', group: 'sample2'}] }; let state = layers(initialState, testAction); @@ -94,7 +94,7 @@ describe('Test the layers reducer', () => { nodeType: 'layers' }; let initialState = { - groups: [{name: 'sample1'}, {name: 'sample2'}], + groups: [{name: 'sample1', id: 'sample1'}, {name: 'sample2', id: 'sample2'}], flat: [{id: 'layer1', group: 'sample1'}, {id: 'layer2', group: 'sample2'}] }; let state = layers(initialState, testAction); @@ -109,7 +109,7 @@ describe('Test the layers reducer', () => { nodeType: 'groups' }; let initialState = { - groups: [{name: 'sample1', nodes: [{name: 'sample1.nested'}]}, {name: 'sample2'}], + groups: [{name: 'sample1', id: 'sample1', nodes: [{name: 'nested', id: 'sample1.nested'}]}, {name: 'sample2', id: 'sample2'}], flat: [{id: 'layer1', group: 'sample1'}, {id: 'layer2', group: 'sample2'}, {id: 'layer3', group: 'sample1.nested'}] }; let state = layers(initialState, testAction); diff --git a/web/client/reducers/layers.js b/web/client/reducers/layers.js index a3a2c9a0f5..f54f61c4f9 100644 --- a/web/client/reducers/layers.js +++ b/web/client/reducers/layers.js @@ -12,7 +12,7 @@ var {LAYER_LOADING, LAYER_LOAD, LAYER_ERROR, CHANGE_LAYER_PROPERTIES, CHANGE_GRO } = require('../actions/layers'); var assign = require('object-assign'); -var {isObject, isArray, head, findIndex} = require('lodash'); +var {isObject, isArray, head, isString} = require('lodash'); const LayersUtils = require('../utils/LayersUtils'); @@ -20,9 +20,11 @@ const deepChange = (nodes, findValue, propName, propValue) => { if (nodes && isArray(nodes) && nodes.length > 0) { return nodes.map((node) => { if (isObject(node)) { - return node.id === findValue ? - assign({}, node, {[propName]: propValue}) : - assign({}, node, {nodes: deepChange(node.nodes, findValue, propName, propValue)}); + if (node.id === findValue) { + return assign({}, node, {[propName]: propValue}); + }else if (node.nodes) { + return assign({}, node, {nodes: deepChange(node.nodes, findValue, propName, propValue)}); + } } return node; }); @@ -32,83 +34,27 @@ const deepChange = (nodes, findValue, propName, propValue) => { /** Removes a group even if it is nested -Cannot be used to remove layers as well +It works for layers too **/ const deepRemove = (nodes, findValue) => { if (nodes && isArray(nodes) && nodes.length > 0) { - return nodes.filter((node) => node.name !== findValue).map((node) => isObject(node) ? assign({}, node, node.nodes ? { - nodes: deepRemove(node.nodes, findValue, node.name + '.') + return nodes.filter((node) => (node.id && node.id !== findValue) || (isString(node) && node !== findValue )).map((node) => isObject(node) ? assign({}, node, node.nodes ? { + nodes: deepRemove(node.nodes, findValue) } : {}) : node); } return nodes; }; - -/* -Removes a Layer from the TOC -Currently supports only one level of depth -*/ -const removeNode = (groups, nodeId) => { - if (groups && isArray(groups) && groups.length > 0) { - return groups.map((group) => group.nodes && group.nodes.length ? - assign({}, group, {nodes: group.nodes.filter((layerId) => layerId !== nodeId)} - ) - : group); - } - return groups; -}; - -// add the newGroup to the list if it does not already exists -const addGroup = (groups, newGroup) => { - return head(groups.filter( - (group) => group.id === newGroup - )) ? - groups : - groups.concat({ - id: newGroup, - name: newGroup, - title: newGroup, - nodes: [], - expanded: true - }); -}; - -/* -Moves a Layer to a new group -*/ -const moveNode = (groups, nodeId, newGroup) => { - if (groups && isArray(groups) && groups.length > 0) { - return removeNode(addGroup(groups, newGroup), nodeId) - // Add the layer to the new group - .map((group) => { - if (isObject(group)) { - return group.id === newGroup ? - assign({}, group, {nodes: (group.nodes || []).concat(nodeId)}) : - group; - } - return group; - }) - // Remove empty groups - .filter((group) => group && - ( - group.name === 'Default' || - group.nodes && isArray(group.nodes) && group.nodes.length > 0 - ) - ); - } - return groups; -}; - -const getNode = (nodes, name) => { +const getNode = (nodes, id) => { if (nodes && isArray(nodes)) { return nodes.reduce((previous, node) => { if (previous) { return previous; } - if (node && node.name === name) { + if (node && (node.name === id || node.id === id)) { return node; } if (node && node.nodes && node.nodes.length > 0) { - return getNode(node.nodes, name); + return getNode(node.nodes, id); } return previous; }, null); @@ -116,6 +62,32 @@ const getNode = (nodes, name) => { return null; }; +const moveNode = (groups, node, groupId, newLayers, foreground = true) => { + // Remove node from old group + let newGroups = deepRemove(groups, node); + // Check if group to move to exists + let group = getNode(newGroups, groupId); + if (!group) { + // Create missing group + group = head(LayersUtils.getLayersByGroup([getNode(newLayers, node)])); + // check for parent group if exist + const parentGroup = groupId.split('.').reduce((tree, gName, idx) => { + const gId = groupId.split(".", idx + 1).join('.'); + const parent = getNode(newGroups, gId); + return parent ? tree.concat(parent) : tree; + }, []).pop(); + if (parentGroup) { + group = getNode([group], parentGroup.id).nodes[0]; + newGroups = deepChange(newGroups, parentGroup.id, 'nodes', foreground ? [group].concat(parentGroup.nodes) : parentGroup.nodes.concat(group)); + }else { + newGroups = [group].concat(newGroups); + } + }else { + newGroups = deepChange(newGroups, group.id, 'nodes', foreground ? [node].concat(group.nodes.slice(0)) : group.nodes.concat(node)); + } + return LayersUtils.removeEmptyGroups(newGroups); +}; + function layers(state = [], action) { switch (action.type) { case LAYER_LOADING: { @@ -155,8 +127,7 @@ function layers(state = [], action) { case CHANGE_GROUP_PROPERTIES: { let newLayers = state.flat.map((layer) => { const layerGroup = layer.group || 'Default'; - if (layerGroup === action.group || layerGroup.indexOf(action.group + ".") === 0) { - + if (layerGroup === action.group || layerGroup.indexOf(`${action.group}.`) === 0) { return assign({}, layer, action.newProperties); } return assign({}, layer); @@ -192,7 +163,7 @@ function layers(state = [], action) { let sameGroup = false; const newLayers = flatLayers.map((layer) => { - if (layer[selector] === action.node || layer[selector].indexOf(action.node + '.') === 0) { + if (layer[selector] === action.node || layer[selector].indexOf(`{action.node}.`) === 0) { if (!action.options.hasOwnProperty("group") || layer.group === (action.options.group || 'Default')) { // If the layer didn't change group, raise a flag to prevent groups update sameGroup = true; @@ -204,7 +175,10 @@ function layers(state = [], action) { }); let originalNode = head(flatLayers.filter((layer) => { return (layer[selector] === action.node || layer[selector].indexOf(action.node + '.') === 0); })); if (!sameGroup && originalNode ) { - let newGroups = moveNode(state.groups, action.node, (action.options.group || 'Default')); + // Remove layers from old group + const groupId = (action.options.group || 'Default'); + const newGroups = moveNode(state.groups, action.node, groupId, newLayers); + let orderedNewLayers = LayersUtils.sortLayers ? LayersUtils.sortLayers(newGroups, newLayers) : newLayers; return assign({}, state, { flat: orderedNewLayers, @@ -235,7 +209,7 @@ function layers(state = [], action) { }; } if (action.nodeType === 'layers') { - const newGroups = removeNode(state.groups, action.node); + const newGroups = deepRemove(state.groups, action.node); const newLayers = state.flat.filter((layer) => layer.id !== action.node); return assign({}, state, { flat: newLayers, @@ -248,16 +222,9 @@ function layers(state = [], action) { let newGroups = (state.groups || []).concat(); const newLayer = (action.layer.id) ? action.layer : assign({}, action.layer, {id: LayersUtils.getLayerId(action.layer, newLayers)}); newLayers.push(newLayer); - const groupName = newLayer.group || 'Default'; - if (groupName !== "background") { - let node = getNode(newGroups, groupName ); - if (node) { - let newLayerIds = action.foreground ? [newLayer.id].concat(node.nodes) : node.nodes.concat([newLayer.id]); - newGroups = deepChange(state.groups, groupName, 'nodes', newLayerIds); - } else { - const newGroup = LayersUtils.getLayersByGroup([newLayer]); - newGroups = newGroup.concat(newGroups); - } + const groupId = newLayer.group || 'Default'; + if (groupId !== "background") { + newGroups = moveNode(newGroups, newLayer.id, groupId, newLayers, action.foreground); } let orderedNewLayers = LayersUtils.sortLayers ? LayersUtils.sortLayers(newGroups, newLayers) : newLayers; return { @@ -266,21 +233,12 @@ function layers(state = [], action) { }; } case REMOVE_LAYER: { - let layer = head((state.flat || []).filter(obj => obj.id === action.layerId)); - let groupName = layer && layer.group ? layer.group : 'Default'; - let newLayers = (state.flat || []).filter(lyr => lyr.id !== action.layerId); - let newGroups = (state.groups || []).concat(); - let gidx = findIndex(newGroups, entry => entry.id === groupName); - if (gidx >= 0) { - let newGroup = assign({}, newGroups[gidx]); - let nidx = newGroup.nodes.indexOf(action.layerId); - newGroup.nodes = [...newGroup.nodes.slice(0, nidx), ...newGroup.nodes.slice(nidx + 1)]; - newGroups[gidx] = newGroup; - } - return { - flat: newLayers, - groups: newGroups - }; + const newGroups = deepRemove(state.groups, action.layerId); + const newLayers = state.flat.filter((layer) => layer.id !== action.layerId); + return assign({}, state, { + flat: newLayers, + groups: newGroups + }); } case SHOW_SETTINGS: { let settings = assign({}, state.settings, { diff --git a/web/client/utils/LayersUtils.js b/web/client/utils/LayersUtils.js index be655e5ca6..4ad7524fb0 100644 --- a/web/client/utils/LayersUtils.js +++ b/web/client/utils/LayersUtils.js @@ -7,21 +7,40 @@ */ const assign = require('object-assign'); -const {isObject, isArray} = require('lodash'); +const {isObject, isArray, head} = require('lodash'); +const getGroup = (groupId, groups) => { + return head(groups.filter((subGroup) => isObject(subGroup) && subGroup.id === groupId)); +}; +const getLayer = (layerName, allLayers) => { + return head(allLayers.filter((layer) => layer.id === layerName)); +}; +const getLayersId = (groupId, allLayers) => { + return allLayers.filter((layer) => (layer.group || 'Default') === groupId).map((layer) => layer.id).reverse(); +}; const initialReorderLayers = (groups, allLayers) => { return groups.slice(0).reverse().reduce((previous, group) => { - return previous.concat(allLayers.filter((layer) => (layer.group || 'Default') === group.name)) - .concat(initialReorderLayers((group.groups || []).slice(0).reverse(), allLayers, group.name + '.').reverse()); + return previous.concat( + group.nodes.slice(0).reverse().reduce((layers, node) => { + if (isObject(node)) { + return layers.concat(initialReorderLayers([node], allLayers)); + } + return layers.concat(getLayer(node, allLayers)); + }, []) + ); }, []); }; - const reorderLayers = (groups, allLayers) => { - return groups.slice(0).reverse().reduce((previous, group) => { - return previous.concat( - group.nodes.filter((node) => !isObject(node)).reverse().map((layer) => allLayers.filter((fullLayer) => fullLayer.id === layer)[0]) - ).concat(reorderLayers((group.nodes || []).filter((node) => isObject(node)).reverse(), allLayers).reverse()); - }, []); + return initialReorderLayers(groups, allLayers); +}; +const createGroup = (groupId, groupName, layers, addLayers) => { + return assign({}, { + id: groupId, + name: groupName, + title: groupName, + nodes: addLayers ? getLayersId(groupId, layers) : [], + expanded: true + }); }; var LayersUtils = { @@ -33,26 +52,41 @@ var LayersUtils = { let mapLayers = configLayers.map((layer) => assign({}, layer, {storeIndex: i++})); let groupNames = mapLayers.reduce((groups, layer) => { return groups.indexOf(layer.group || 'Default') === -1 ? groups.concat([layer.group || 'Default']) : groups; - }, []).filter((group) => group !== 'background'); - return groupNames.map((group) => { - let groupName = group || 'Default'; + }, []).filter((group) => group !== 'background').reverse(); - return assign({}, { - id: groupName, - name: groupName, - title: groupName, - nodes: mapLayers.filter((layer) => (layer.group || 'Default') === group).map((layer) => layer.id).reverse(), - expanded: true - }); - }).reverse(); + return groupNames.reduce((groups, names)=> { + let name = names || 'Default'; + name.split('.').reduce((subGroups, groupName, idx, array)=> { + const groupId = name.split(".", idx + 1).join('.'); + let group = getGroup(groupId, subGroups); + const addLayers = (idx === array.length - 1); + if (!group) { + group = createGroup(groupId, groupName, mapLayers, addLayers); + subGroups.push(group); + }else if (addLayers) { + group.nodes = group.nodes.concat(getLayersId(groupId, mapLayers)); + } + return group.nodes; + }, groups); + return groups; + }, []); + }, + removeEmptyGroups: (groups) => { + return groups.reduce((acc, group) => { + return acc.concat(LayersUtils.getNotEmptyGroup(group)); + }, []); + }, + getNotEmptyGroup: (group) => { + const nodes = group.nodes.reduce((gNodes, node) => { + return node.nodes ? gNodes.concat(LayersUtils.getNotEmptyGroup(node)) : gNodes.concat(node); + }, []); + return nodes.length > 0 ? assign({}, group, {nodes: nodes}) : []; }, - reorder: (groups, allLayers) => { return allLayers.filter((layer) => layer.group === 'background') - .concat(initialReorderLayers(groups, allLayers)); + .concat(reorderLayers(groups, allLayers)); }, denormalizeGroups: (allLayers, groups) => { - let getGroupVisibility = (nodes) => { let visibility = true; nodes.forEach((node) => { @@ -62,29 +96,19 @@ var LayersUtils = { }); return visibility; }; - + let getNormalizedGroup = (group, layers) => { + const nodes = group.nodes.map((node) => { + if (isObject(node)) { + return getNormalizedGroup(node, layers); + } + return layers.filter((layer) => layer.id === node)[0]; + }); + return assign({}, group, {nodes, visibility: getGroupVisibility(nodes)}); + }; let normalizedLayers = allLayers.map((layer) => assign({}, layer, {expanded: layer.expanded || false})); return { flat: normalizedLayers, - groups: groups.map((group) => assign({}, group, { - nodes: group.nodes.map((node) => { - if (isObject(node)) { - return assign({}, node, { - nodes: node.nodes.map((layerId) => normalizedLayers.filter((layer) => layer.id === layerId)[0]) - }); - } - return normalizedLayers.filter((layer) => layer.id === node)[0]; - }).map((subGroup) => { - if (subGroup && subGroup.nodes && subGroup.nodes.length > 0) { - return assign(subGroup, { - visibility: getGroupVisibility(subGroup.nodes) - }); - } - return subGroup; - }) - })).map((group) => assign(group, { - visibility: getGroupVisibility(group.nodes) - })) + groups: groups.map((group) => getNormalizedGroup(group, normalizedLayers)) }; },