From bdd5f115cd372cd6d6c12f696f2d8674ffc3e3e0 Mon Sep 17 00:00:00 2001 From: jPalmer Date: Thu, 4 Jun 2020 13:53:06 -0700 Subject: [PATCH] added clone functionality #88 and improved add layer UI --- src/App.css | 2 +- src/component/Field/FieldSelect.jsx | 7 + src/component/Infotip/InfotipMessage.jsx | 1 - src/component/LayerAdd/index.jsx | 34 +++-- src/component/LayerEdit/LayerEditActions.jsx | 25 ++-- .../LayerEdit/LayerEditModalClone.jsx | 126 ++++++++++++++++++ .../LayerEdit/LayerEditModalRemove.jsx | 25 +++- src/component/SourceEdit/SourceEditLayers.jsx | 12 +- src/component/Style/StyleSettingsRoot.jsx | 1 + src/model/layer/actions.js | 27 +++- src/model/layer/helpers.js | 18 +++ src/model/source/actions.js | 1 + src/model/source/helpers.js | 31 ++++- src/model/style/actions.js | 19 +++ src/model/style/reducer.js | 18 +++ 15 files changed, 305 insertions(+), 42 deletions(-) create mode 100644 src/component/LayerEdit/LayerEditModalClone.jsx diff --git a/src/App.css b/src/App.css index 9ade02e..c8fcedf 100644 --- a/src/App.css +++ b/src/App.css @@ -245,7 +245,7 @@ label .badge{position:relative;top:2px;} .modal{background-color:rgba(39,50,55,0.75);} .modal-container{position:absolute;top:0;bottom:0;left:0;right:0;} .modal-backdrop{position:absolute;top:0;bottom:0;left:0;right:0;background-color:rgba(39,50,55,0.6);} -.modal-content{position:relative;margin:0.75em;background:#fff;z-index:1041;width:inherit;} +.modal-content{position:relative;margin:0.75em;background:#fff;z-index:1041;} .navbar-pill{display:inline-block;border-radius:0.25rem 0.25rem 0 0;padding:0.25rem; margin:0.5rem 0 0 0.5rem;} diff --git a/src/component/Field/FieldSelect.jsx b/src/component/Field/FieldSelect.jsx index c7cb7ef..4e68823 100644 --- a/src/component/Field/FieldSelect.jsx +++ b/src/component/Field/FieldSelect.jsx @@ -1,6 +1,8 @@ import React from 'react' import PropTypes from 'prop-types' +import modelLayer from '../../model/layer' + class FieldSelect extends React.Component { handleChange = (e)=>{ @@ -21,6 +23,11 @@ class FieldSelect extends React.Component { handle.blur && handle.blur(name) } + handleSubmit = async ()=>{ + const {layer, path, style} = this.props + await modelLayer.actions.clone({layer, path, style}) + } + render (){ const {autoFocus, helper, label, name, options, placeholder, value} = this.props diff --git a/src/component/Infotip/InfotipMessage.jsx b/src/component/Infotip/InfotipMessage.jsx index 926b624..221c485 100644 --- a/src/component/Infotip/InfotipMessage.jsx +++ b/src/component/Infotip/InfotipMessage.jsx @@ -39,7 +39,6 @@ class InfotipMessage extends React.Component { } componentWillUnmount (){ - console.log('cleanup:',this.parentEl) document.body.removeChild(this.parentEl) } diff --git a/src/component/LayerAdd/index.jsx b/src/component/LayerAdd/index.jsx index 948a9c7..2423b6e 100644 --- a/src/component/LayerAdd/index.jsx +++ b/src/component/LayerAdd/index.jsx @@ -1,11 +1,13 @@ import PropTypes from 'prop-types' import React from 'react' +import {withRouter} from 'react-router-dom' import utilUrl from '../../utility/utilUrl' import modelApp from '../../model/app' import modelLayer from '../../model/layer' import modelSource from '../../model/source' +import modelStyle from '../../model/style' import Alert from '../Alert' import Property from '../Property' @@ -15,12 +17,15 @@ class LayerAdd extends React.Component { constructor(props) { super(props) + // check for query params + const query = new URLSearchParams(window.location.search) + this.state = { rec:{ - id:'', - source:'', - 'source-layer':'', - type:'' + id:query.has('source-layer')? query.get('source-layer'): '', + source:query.has('source')? query.get('source'): '', + 'source-layer':query.has('source-layer')? query.get('source-layer'): '', + type:query.has('type')? query.get('type'): '', }, error:null } @@ -28,16 +33,20 @@ class LayerAdd extends React.Component { handleSubmit = async (e)=>{ e.preventDefault() - const {path, style} = this.props, + const {history, path, style} = this.props, {rec} = this.state try{ + + if (style.getIn(['current','layers']).find(layer => layer.get('id') === rec.id)){ + throw new Error(`LayerAdd.submit: layerId already exists`) + } await modelApp.actions.setLoading(true) await modelLayer.actions.add({path, rec, style}) await modelApp.actions.setLoading(false) - // route user to layer - // handle.route('layer/'+layer.id) + const route = `layers/${rec.id}` + history.push(modelStyle.helpers.getRouteFromPath({path, route})) } catch(e){ await modelApp.actions.setLoading(false) await modelApp.actions.setError(e) @@ -53,8 +62,6 @@ class LayerAdd extends React.Component { let id = parts.join('.') - // TODO: check for layer id collisions - this.setState({rec:{ ...rec, id @@ -86,9 +93,9 @@ class LayerAdd extends React.Component { {rec} = this.state const typeOptions = modelLayer.helpers.getTypeOptions() - const sourceOptions = modelSource.helpers.getOptions({style}) - const sourceLayerOptions = (this.state.source)? modelSource.helpers.getLayerOptions({style, sourceId:this.state.source}): - null + const sourceOptions = modelSource.helpers.getOptions({style}) || [] + const sourceLayerOptions = rec.source? modelSource.helpers.getLayerOptions({style, sourceId:rec.source}): + [] const handle = { change: this.handleChange @@ -165,10 +172,11 @@ class LayerAdd extends React.Component { } LayerAdd.propTypes = { + history: PropTypes.object, handle: PropTypes.object, match: PropTypes.object, path: PropTypes.array, style: PropTypes.object, } -export default LayerAdd \ No newline at end of file +export default withRouter(LayerAdd) \ No newline at end of file diff --git a/src/component/LayerEdit/LayerEditActions.jsx b/src/component/LayerEdit/LayerEditActions.jsx index b1e45e0..6422794 100644 --- a/src/component/LayerEdit/LayerEditActions.jsx +++ b/src/component/LayerEdit/LayerEditActions.jsx @@ -1,13 +1,11 @@ import React from 'react' import PropTypes from 'prop-types' -import {withRouter} from 'react-router-dom' import utilPath from '../../utility/utilPath' import Icon from '../Icon' import LayerEditModalRemove from './LayerEditModalRemove' - -import modelLayer from '../../model/layer' +import LayerEditModalClone from './LayerEditModalClone' class LayerEditActions extends React.Component { constructor (props){ @@ -19,8 +17,9 @@ class LayerEditActions extends React.Component { } handleClone = async ()=>{ - const {layer} = this.props - await modelLayer.actions.clone({layer}) + this.setState({ + modal: 'cloneDone' + }) } handleModalSet = (modal)=>{ @@ -46,7 +45,7 @@ class LayerEditActions extends React.Component {
- @@ -65,7 +64,7 @@ class LayerEditActions extends React.Component { } renderModal (){ - const {path, style} = this.props, + const {layer, path, style} = this.props, {modal} = this.state switch (modal){ @@ -73,7 +72,15 @@ class LayerEditActions extends React.Component { return ( this.handleModalSet(null)} - handleDone={this.handleRemoveDone} + layer={layer} + path={path} + style={style}/> + ) + case 'cloneDone': + return ( + this.handleModalSet(null)} + layer={layer} path={path} style={style}/> ) @@ -92,4 +99,4 @@ LayerEditActions.propTypes = { style: PropTypes.object, } -export default withRouter(LayerEditActions) \ No newline at end of file +export default LayerEditActions \ No newline at end of file diff --git a/src/component/LayerEdit/LayerEditModalClone.jsx b/src/component/LayerEdit/LayerEditModalClone.jsx new file mode 100644 index 0000000..d53b113 --- /dev/null +++ b/src/component/LayerEdit/LayerEditModalClone.jsx @@ -0,0 +1,126 @@ +import React from 'react' +import PropTypes from 'prop-types' +import Modal from '../Modal' +import {withRouter} from 'react-router-dom' + +import modelApp from '../../model/app' +import modelLayer from '../../model/layer' +import modelStyle from '../../model/style' + +import Property from '../Property' + +class LayerEditModalClone extends React.Component { + + constructor(props) { + super(props) + const {layer, style} = props + + const cloneId = modelLayer.helpers.getLayerCloneId({layer, style}) + + this.state = { + id: cloneId, + placement: '', + } + } + + handleClone = async ()=>{ + const {history, layer, path, style} = this.props, + {id, placement} = this.state + + try{ + await modelApp.actions.setLoading(true) + + const clone = await modelLayer.actions.clone({cloneId: id, layer, path, placement, style}) + await modelApp.actions.setLoading(false) + + // send user to newly created layer + const route = `layers/${clone.get('id')}` + history.push(modelStyle.helpers.getRouteFromPath({path, route})) + + } catch(e){ + await modelApp.actions.setLoading(false) + await modelApp.actions.setError(e) + } + + this.setState({ + modal: 'cloneDone' + }) + } + + handleChange = ({name, value})=>{ + let state = {} + state[name] = value + + this.setState(state) + } + + render (){ + const {handleClose} = this.props, + {id, placement} = this.state + + //const stylePath = modelStyle.helpers.getRouteFromPath({path}) + + const handle = { + change: this.handleChange + } + + const options = [ + {name:'at the bottom', value:'bottom'}, + {name:'after cloned layer', value:'after'}, + ] + + return ( + +
+
CLONE LAYER
+ +
+
+ + + + +
+
+ +
+
+ ) + } + +} + +LayerEditModalClone.propTypes = { + handleClose: PropTypes.func.isRequired, + handleDone: PropTypes.func, + history: PropTypes.object, + layer: PropTypes.object, + path: PropTypes.array, + style: PropTypes.object, +} + +export default withRouter(LayerEditModalClone) + diff --git a/src/component/LayerEdit/LayerEditModalRemove.jsx b/src/component/LayerEdit/LayerEditModalRemove.jsx index 15ee374..fd00760 100644 --- a/src/component/LayerEdit/LayerEditModalRemove.jsx +++ b/src/component/LayerEdit/LayerEditModalRemove.jsx @@ -1,22 +1,36 @@ import React from 'react' import PropTypes from 'prop-types' import Modal from '../Modal' +import {withRouter} from 'react-router-dom' import modelApp from '../../model/app' +import modelLayer from '../../model/layer' import modelStyle from '../../model/style' class LayerEditModalRemove extends React.Component { handleRemove = async ()=>{ - const {handleDone, path} = this.props + const {history, layer, path, style} = this.props try{ await modelApp.actions.setLoading(true) + + const layerIndex = modelLayer.helpers.getIndexById({layerId: layer.get('id'), style}) + if (layerIndex === -1) throw new Error(`LayerEditModalRemove.handleRemove: no layer found to clone`) + await modelStyle.actions.removeIn({ path }) await modelApp.actions.setLoading(false) - handleDone() + + // send user to prev layer + const prevIndex = layerIndex === 0? 1: layerIndex - 1 + // get prev layer + const prevLayer = style.getIn(['current','layers', prevIndex]) + const route = prevLayer? `layers/${prevLayer.get('id')}`: 'layers' + + history.push(modelStyle.helpers.getRouteFromPath({path, route})) + } catch(e){ await modelApp.actions.setLoading(false) await modelApp.actions.setError(e) @@ -35,7 +49,7 @@ class LayerEditModalRemove extends React.Component {
-

Are you sure you want to remove this layer?

+

Are you sure you want to remove this layer?

@@ -50,8 +64,11 @@ class LayerEditModalRemove extends React.Component { LayerEditModalRemove.propTypes = { handleClose: PropTypes.func.isRequired, handleDone: PropTypes.func, + history: PropTypes.object, + layer: PropTypes.object, path: PropTypes.array, + style: PropTypes.object, } -export default LayerEditModalRemove +export default withRouter(LayerEditModalRemove) diff --git a/src/component/SourceEdit/SourceEditLayers.jsx b/src/component/SourceEdit/SourceEditLayers.jsx index 9da8d17..1383155 100644 --- a/src/component/SourceEdit/SourceEditLayers.jsx +++ b/src/component/SourceEdit/SourceEditLayers.jsx @@ -1,6 +1,5 @@ import React from 'react' import PropTypes from 'prop-types' -import {fromJS} from 'immutable' import {connect} from 'react-redux' import {Link, withRouter} from 'react-router-dom' @@ -43,8 +42,11 @@ class SourceEditLayers extends React.Component { foundStyleLayers.push(styleLayer.get('id')) }) + const type = modelSource.helpers.getLayerTypeFromSourceLayer({layer}) + const layerAddPath = modelStyle.helpers.getRouteFromPath({path, route:`layers/add?source=${sourceId}&source-layer=${layer.get('id')}&type=${type}`}) + if (foundStyleLayers.length < 1){ - const layerAddPath = modelStyle.helpers.getRouteFromPath({path, route:`layers/add`}) + return (
{layerId} @@ -58,10 +60,14 @@ class SourceEditLayers extends React.Component { return (
{layerId} - + + + + {foundStyleLayers.length} +
) } diff --git a/src/component/Style/StyleSettingsRoot.jsx b/src/component/Style/StyleSettingsRoot.jsx index 7885bf8..5551719 100644 --- a/src/component/Style/StyleSettingsRoot.jsx +++ b/src/component/Style/StyleSettingsRoot.jsx @@ -63,6 +63,7 @@ class StyleSettingsRoot extends React.Component { name: key, label: key, path: pathProp, + removeEnabled: true, value: value, error: error && error.get && error.get(key) } diff --git a/src/model/layer/actions.js b/src/model/layer/actions.js index fb75602..de8c7ce 100644 --- a/src/model/layer/actions.js +++ b/src/model/layer/actions.js @@ -1,6 +1,6 @@ import {fromJS} from 'immutable' import actions from '../actions' - +import helpers from './helpers' const add = async ({afterLayerInd = 0, rec, path})=>{ @@ -21,12 +21,25 @@ const add = async ({afterLayerInd = 0, rec, path})=>{ }) } -const clone = async ({index, layer, path})=>{ - await actions.act('style.listAdd',{ - item: layer, - path, - pos: index, - }) +const clone = async ({cloneId, layer, path, placement, style})=>{ + + const clone = layer.setIn(['id'], cloneId) + const layersPath = path.slice(0,-1) + + if (placement === 'after'){ + let pos = helpers.getIndexById({layerId: layer.get('id'), style}) + await actions.act('style.listAddAt',{ + at: pos+1, + item: clone, + path: layersPath, + }) + } else { + await actions.act('style.listAdd',{ + item: clone, + path: layersPath, + }) + } + return clone } const reorder = async ({indexOld, indexNew, path})=>{ diff --git a/src/model/layer/helpers.js b/src/model/layer/helpers.js index c34907e..f6781e4 100644 --- a/src/model/layer/helpers.js +++ b/src/model/layer/helpers.js @@ -18,6 +18,23 @@ const getIndexById = ({layerId, style})=>{ }) } +const getLayerCloneId = ({layer, style})=>{ + const layers = style.getIn(['current','layers']) + const id = layer.getIn(['id']) + const idBase = id.replace(/_[0-9]*$/, '') + + let cloneId, inc = 1 + while (!cloneId){ + inc++ + const testId = `${idBase}_${inc}` + if (!layers.find(layer=>layer.get('id') === testId)){ + cloneId = testId + } + } + + return cloneId +} + const getLayerPath = ({layerId, style})=>{ const index = getIndexById({layerId, style}) return [style.getIn(['current','id']), 'current', 'layers', index] @@ -33,6 +50,7 @@ const getTypeOptions = ()=>{ export default { getColor, getIndexById, + getLayerCloneId, getLayerPath, getType, getTypeOptions, diff --git a/src/model/source/actions.js b/src/model/source/actions.js index 51fcfdf..57ca6b3 100644 --- a/src/model/source/actions.js +++ b/src/model/source/actions.js @@ -145,6 +145,7 @@ const makeLayersFromData = async ({sourceId, sourceData})=>{ actions.subscribe('source',{ add, makeLayersFromData, + pullData, }) export default { diff --git a/src/model/source/helpers.js b/src/model/source/helpers.js index 6d5ce66..61def8f 100644 --- a/src/model/source/helpers.js +++ b/src/model/source/helpers.js @@ -1,16 +1,25 @@ import {latest} from 'mapbox-gl/src/style-spec' +import Store from '../../Store' const getLayerOptions = ({style, sourceId})=>{ - if (!style.hasIn(['sources', sourceId, 'layers'])) return [] - const sourceLayers = style.getIn(['sources', sourceId, 'layers']) + const state = Store.getState() + + if (!state.source || !state.source.sources) return [] + + const sourceUrl = style.getIn(['current', 'sources', sourceId, 'url']) + if (!sourceUrl) return [] + + const sourceLayers = state.source.sources.getIn([sourceUrl, 'data', 'vector_layers']) + if (!sourceLayers) return [] + return sourceLayers.map((layer)=>{ return { name:layer.get('id'), value:layer.get('id') } - }) + }).toJS() } const getOptions = ({style})=>{ @@ -23,7 +32,20 @@ const getOptions = ({style})=>{ name:key, value:key } - }) + }).toJS() +} + +const getLayerTypeFromSourceLayer = ({layer})=>{ + switch(layer.get('geometry_type')){ + case 'line': + return 'line' + case 'polygon': + return 'fill' + case 'point': + return 'symbol' + default: + return 'fill' + } } const getTypeOptions = ()=>{ @@ -46,6 +68,7 @@ const getLayersFromData = ({data})=>{ export default { getLayerOptions, getLayersFromData, + getLayerTypeFromSourceLayer, getOptions, getTypeOptions, } \ No newline at end of file diff --git a/src/model/style/actions.js b/src/model/style/actions.js index 0c5e9ca..5df87c2 100644 --- a/src/model/style/actions.js +++ b/src/model/style/actions.js @@ -204,6 +204,23 @@ const listAdd = async ({item, path, pos})=>{ await localBackup() } +const listAddAt = async ({at, item, path, pos})=>{ + + if (!path) throw new Error('style.actions.listAddAt: no path defined') + + Store.dispatch({ + type:'STYLE_LIST_ADD_AT', + payload:{ + at, + pos, + item, + path, + } + }) + + await localBackup() +} + const listConcat = async ({list, path})=>{ if (!path) throw new Error('style.actions.listConcat: no path defined') @@ -373,6 +390,7 @@ const updateUpload = async ({file, style})=>{ actions.subscribe('style',{ changeKeyIn, listAdd, + listAddAt, listConcat, reorderInList, setIn, @@ -388,6 +406,7 @@ export default { errorSet, focusIn, init, + listAddAt, listConcat, remove, removeIn, diff --git a/src/model/style/reducer.js b/src/model/style/reducer.js index 6a2ff30..732ee4e 100644 --- a/src/model/style/reducer.js +++ b/src/model/style/reducer.js @@ -82,6 +82,24 @@ export const reducer = (st = state, action)=>{ styles, }) } + case 'STYLE_LIST_ADD_AT':{ + const {at, item, path} = action.payload + if (!st.styles.hasIn(path)){ + const styles = st.styles.setIn(path, List([item])) + return updateStyle({ + path, + st, + styles, + }) + } + const list = st.styles.getIn(path).insert(at, item) + const styles = st.styles.setIn(path, list) + return updateStyle({ + path, + st, + styles, + }) + } case 'STYLE_LIST_CONCAT':{ const {list, path} = action.payload