Merge pull request #350 from tomalrussell/feature/multi-copy
Feature/multi copy
This commit is contained in:
commit
9433fd879b
@ -118,16 +118,22 @@ class App extends React.Component {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Colour building
|
||||||
|
*
|
||||||
|
* Used in multi-edit mode to colour buildings on map click
|
||||||
|
*
|
||||||
|
* Pulls data from URL to form update
|
||||||
|
*
|
||||||
|
* @param {object} building
|
||||||
|
*/
|
||||||
colourBuilding(building) {
|
colourBuilding(building) {
|
||||||
const cat = parseCategoryURL(window.location.pathname);
|
const cat = parseCategoryURL(window.location.pathname);
|
||||||
const q = parse(window.location.search);
|
const q = parse(window.location.search);
|
||||||
let data;
|
const data = (cat === 'like')? {like: true}: JSON.parse(q.data);
|
||||||
if (cat === 'like'){
|
if (cat === 'like'){
|
||||||
data = {like: true}
|
|
||||||
this.likeBuilding(building.building_id)
|
this.likeBuilding(building.building_id)
|
||||||
} else {
|
} else {
|
||||||
data = {}
|
|
||||||
data[q.k] = q.v;
|
|
||||||
this.updateBuilding(building.building_id, data)
|
this.updateBuilding(building.building_id, data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -50,18 +50,52 @@ BuildingEdit.propTypes = {
|
|||||||
class EditForm extends Component {
|
class EditForm extends Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {}
|
this.state = {
|
||||||
|
error: this.props.error || undefined,
|
||||||
|
like: this.props.like || undefined,
|
||||||
|
copying: false,
|
||||||
|
keys_to_copy: {}
|
||||||
|
}
|
||||||
for (const field of props.fields) {
|
for (const field of props.fields) {
|
||||||
this.state[field.slug] = props[field.slug]
|
this.state[field.slug] = props[field.slug]
|
||||||
}
|
}
|
||||||
this.state.error = this.props.error || undefined;
|
|
||||||
this.state.like = this.props.like || undefined;
|
|
||||||
|
|
||||||
this.handleChange = this.handleChange.bind(this);
|
this.handleChange = this.handleChange.bind(this);
|
||||||
this.handleCheck = this.handleCheck.bind(this);
|
this.handleCheck = this.handleCheck.bind(this);
|
||||||
this.handleLike = this.handleLike.bind(this);
|
this.handleLike = this.handleLike.bind(this);
|
||||||
this.handleSubmit = this.handleSubmit.bind(this);
|
this.handleSubmit = this.handleSubmit.bind(this);
|
||||||
this.handleUpdate = this.handleUpdate.bind(this);
|
this.handleUpdate = this.handleUpdate.bind(this);
|
||||||
|
|
||||||
|
this.toggleCopying = this.toggleCopying.bind(this);
|
||||||
|
this.toggleCopyAttribute = this.toggleCopyAttribute.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enter or exit "copying" state - allow user to select attributes to copy
|
||||||
|
*/
|
||||||
|
toggleCopying() {
|
||||||
|
this.setState({
|
||||||
|
copying: !this.state.copying
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keep track of data to copy (accumulate while in "copying" state)
|
||||||
|
*
|
||||||
|
* Note that we track keys only - values are already held in state
|
||||||
|
*
|
||||||
|
* @param {string} key
|
||||||
|
*/
|
||||||
|
toggleCopyAttribute(key) {
|
||||||
|
const keys = this.state.keys_to_copy;
|
||||||
|
if(this.state.keys_to_copy[key]){
|
||||||
|
delete keys[key];
|
||||||
|
} else {
|
||||||
|
keys[key] = true;
|
||||||
|
}
|
||||||
|
this.setState({
|
||||||
|
keys_to_copy: keys
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -175,6 +209,12 @@ class EditForm extends Component {
|
|||||||
const cat = this.props.cat;
|
const cat = this.props.cat;
|
||||||
const buildingLike = this.props.building_like;
|
const buildingLike = this.props.building_like;
|
||||||
|
|
||||||
|
const values_to_copy = {}
|
||||||
|
for (const key of Object.keys(this.state.keys_to_copy)) {
|
||||||
|
values_to_copy[key] = this.state[key]
|
||||||
|
}
|
||||||
|
const data_string = JSON.stringify(values_to_copy);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className={(this.props.inactive)? 'data-section inactive': 'data-section'}>
|
<section className={(this.props.inactive)? 'data-section inactive': 'data-section'}>
|
||||||
<header className={`section-header edit ${this.props.slug} ${(match? 'active' : '')}`}>
|
<header className={`section-header edit ${this.props.slug} ${(match? 'active' : '')}`}>
|
||||||
@ -187,14 +227,38 @@ class EditForm extends Component {
|
|||||||
</NavLink>
|
</NavLink>
|
||||||
<nav className="icon-buttons">
|
<nav className="icon-buttons">
|
||||||
{
|
{
|
||||||
this.props.help?
|
(match && !this.props.inactive && this.props.slug !== 'like')?
|
||||||
|
this.state.copying?
|
||||||
|
<Fragment>
|
||||||
|
<NavLink
|
||||||
|
to={`/multi-edit/${this.props.cat}.html?data=${data_string}`}
|
||||||
|
className="icon-button copy">
|
||||||
|
Copy selected
|
||||||
|
</NavLink>
|
||||||
|
<a className="icon-button copy" onClick={this.toggleCopying}>Cancel</a>
|
||||||
|
</Fragment>
|
||||||
|
:
|
||||||
|
<a className="icon-button copy" onClick={this.toggleCopying}>Copy</a>
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
{
|
||||||
|
(match && this.props.slug === 'like')?
|
||||||
|
<NavLink
|
||||||
|
to={`/multi-edit/${this.props.cat}.html`}
|
||||||
|
className="icon-button copy">
|
||||||
|
Copy
|
||||||
|
</NavLink>
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
{
|
||||||
|
this.props.help && !this.state.copying?
|
||||||
<a className="icon-button help" title="Find out more" href={this.props.help}>
|
<a className="icon-button help" title="Find out more" href={this.props.help}>
|
||||||
Info
|
Info
|
||||||
</a>
|
</a>
|
||||||
: null
|
: null
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
(match && !this.props.inactive && this.props.slug !== 'like')? // special-case for likes
|
(match && !this.state.copying && !this.props.inactive && this.props.slug !== 'like')? // special-case for likes
|
||||||
<NavLink className="icon-button save" title="Save Changes"
|
<NavLink className="icon-button save" title="Save Changes"
|
||||||
onClick={this.handleSubmit}
|
onClick={this.handleSubmit}
|
||||||
to={`/edit/${this.props.slug}/building/${this.props.building_id}.html`}>
|
to={`/edit/${this.props.slug}/building/${this.props.building_id}.html`}>
|
||||||
@ -221,24 +285,45 @@ class EditForm extends Component {
|
|||||||
switch (props.type) {
|
switch (props.type) {
|
||||||
case 'text':
|
case 'text':
|
||||||
return <TextInput {...props} handleChange={this.handleChange}
|
return <TextInput {...props} handleChange={this.handleChange}
|
||||||
|
copying={this.state.copying}
|
||||||
|
toggleCopyAttribute={this.toggleCopyAttribute}
|
||||||
|
copy={this.state.keys_to_copy[props.slug]}
|
||||||
value={this.state[props.slug]} key={props.slug} cat={cat} />
|
value={this.state[props.slug]} key={props.slug} cat={cat} />
|
||||||
case 'text_list':
|
case 'text_list':
|
||||||
return <TextListInput {...props} handleChange={this.handleChange}
|
return <TextListInput {...props} handleChange={this.handleChange}
|
||||||
|
copying={this.state.copying}
|
||||||
|
toggleCopyAttribute={this.toggleCopyAttribute}
|
||||||
|
copy={this.state.keys_to_copy[props.slug]}
|
||||||
value={this.state[props.slug]} key={props.slug} cat={cat} />
|
value={this.state[props.slug]} key={props.slug} cat={cat} />
|
||||||
case 'text_long':
|
case 'text_long':
|
||||||
return <LongTextInput {...props} handleChange={this.handleChange}
|
return <LongTextInput {...props} handleChange={this.handleChange}
|
||||||
|
copying={this.state.copying}
|
||||||
|
toggleCopyAttribute={this.toggleCopyAttribute}
|
||||||
|
copy={this.state.keys_to_copy[props.slug]}
|
||||||
value={this.state[props.slug]} key={props.slug} cat={cat} />
|
value={this.state[props.slug]} key={props.slug} cat={cat} />
|
||||||
case 'number':
|
case 'number':
|
||||||
return <NumberInput {...props} handleChange={this.handleChange}
|
return <NumberInput {...props} handleChange={this.handleChange}
|
||||||
|
copying={this.state.copying}
|
||||||
|
toggleCopyAttribute={this.toggleCopyAttribute}
|
||||||
|
copy={this.state.keys_to_copy[props.slug]}
|
||||||
value={this.state[props.slug]} key={props.slug} cat={cat} />
|
value={this.state[props.slug]} key={props.slug} cat={cat} />
|
||||||
case 'year_estimator':
|
case 'year_estimator':
|
||||||
return <YearEstimator {...props} handleChange={this.handleChange}
|
return <YearEstimator {...props} handleChange={this.handleChange}
|
||||||
|
copying={this.state.copying}
|
||||||
|
toggleCopyAttribute={this.toggleCopyAttribute}
|
||||||
|
copy={this.state.keys_to_copy[props.slug]}
|
||||||
value={this.state[props.slug]} key={props.slug} cat={cat} />
|
value={this.state[props.slug]} key={props.slug} cat={cat} />
|
||||||
case 'text_multi':
|
case 'text_multi':
|
||||||
return <MultiTextInput {...props} handleChange={this.handleUpdate}
|
return <MultiTextInput {...props} handleChange={this.handleUpdate}
|
||||||
|
copying={this.state.copying}
|
||||||
|
toggleCopyAttribute={this.toggleCopyAttribute}
|
||||||
|
copy={this.state.keys_to_copy[props.slug]}
|
||||||
value={this.state[props.slug]} key={props.slug} cat={cat} />
|
value={this.state[props.slug]} key={props.slug} cat={cat} />
|
||||||
case 'checkbox':
|
case 'checkbox':
|
||||||
return <CheckboxInput {...props} handleChange={this.handleCheck}
|
return <CheckboxInput {...props} handleChange={this.handleCheck}
|
||||||
|
copying={this.state.copying}
|
||||||
|
toggleCopyAttribute={this.toggleCopyAttribute}
|
||||||
|
copy={this.state.keys_to_copy[props.slug]}
|
||||||
value={this.state[props.slug]} key={props.slug} cat={cat} />
|
value={this.state[props.slug]} key={props.slug} cat={cat} />
|
||||||
case 'like':
|
case 'like':
|
||||||
return <LikeButton {...props} handleLike={this.handleLike}
|
return <LikeButton {...props} handleLike={this.handleLike}
|
||||||
@ -285,9 +370,11 @@ EditForm.propTypes = {
|
|||||||
const TextInput = (props) => (
|
const TextInput = (props) => (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<Label slug={props.slug} title={props.title} tooltip={props.tooltip}
|
<Label slug={props.slug} title={props.title} tooltip={props.tooltip}
|
||||||
cat={props.cat} disabled={props.disabled}
|
copying={props.copying}
|
||||||
value={props.value || ''}
|
toggleCopyAttribute={props.toggleCopyAttribute}
|
||||||
/>
|
copy={props.copy}
|
||||||
|
cat={props.cat}
|
||||||
|
disabled={props.disabled} />
|
||||||
<input className="form-control" type="text"
|
<input className="form-control" type="text"
|
||||||
id={props.slug} name={props.slug}
|
id={props.slug} name={props.slug}
|
||||||
value={props.value || ''}
|
value={props.value || ''}
|
||||||
@ -314,6 +401,9 @@ TextInput.propTypes = {
|
|||||||
const LongTextInput = (props) => (
|
const LongTextInput = (props) => (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<Label slug={props.slug} title={props.title} cat={props.cat}
|
<Label slug={props.slug} title={props.title} cat={props.cat}
|
||||||
|
copying={props.copying}
|
||||||
|
toggleCopyAttribute={props.toggleCopyAttribute}
|
||||||
|
copy={props.copy}
|
||||||
disabled={props.disabled} tooltip={props.tooltip} />
|
disabled={props.disabled} tooltip={props.tooltip} />
|
||||||
<textarea className="form-control"
|
<textarea className="form-control"
|
||||||
id={props.slug} name={props.slug}
|
id={props.slug} name={props.slug}
|
||||||
@ -375,7 +465,12 @@ class MultiTextInput extends Component {
|
|||||||
const values = this.getValues();
|
const values = this.getValues();
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<Label slug={this.props.slug} title={this.props.title} tooltip={this.props.tooltip} />
|
<Label slug={this.props.slug} title={this.props.title} tooltip={this.props.tooltip}
|
||||||
|
cat={this.props.cat}
|
||||||
|
copying={this.props.copying}
|
||||||
|
disabled={this.props.disabled}
|
||||||
|
toggleCopyAttribute={this.props.toggleCopyAttribute}
|
||||||
|
copy={this.props.copy} />
|
||||||
{
|
{
|
||||||
values.map((item, i) => (
|
values.map((item, i) => (
|
||||||
<div className="input-group" key={i}>
|
<div className="input-group" key={i}>
|
||||||
@ -409,15 +504,19 @@ MultiTextInput.propTypes = {
|
|||||||
value: PropTypes.arrayOf(PropTypes.string),
|
value: PropTypes.arrayOf(PropTypes.string),
|
||||||
placeholder: PropTypes.string,
|
placeholder: PropTypes.string,
|
||||||
disabled: PropTypes.bool,
|
disabled: PropTypes.bool,
|
||||||
handleChange: PropTypes.func
|
handleChange: PropTypes.func,
|
||||||
|
copy: PropTypes.bool,
|
||||||
|
toggleCopyAttribute: PropTypes.func,
|
||||||
|
copying: PropTypes.bool
|
||||||
}
|
}
|
||||||
|
|
||||||
const TextListInput = (props) => (
|
const TextListInput = (props) => (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<Label slug={props.slug} title={props.title} tooltip={props.tooltip}
|
<Label slug={props.slug} title={props.title} tooltip={props.tooltip}
|
||||||
cat={props.cat} disabled={props.disabled}
|
cat={props.cat} disabled={props.disabled}
|
||||||
value={props.value || ''}
|
copying={props.copying}
|
||||||
/>
|
toggleCopyAttribute={props.toggleCopyAttribute}
|
||||||
|
copy={props.copy} />
|
||||||
<select className="form-control"
|
<select className="form-control"
|
||||||
id={props.slug} name={props.slug}
|
id={props.slug} name={props.slug}
|
||||||
value={props.value || ''}
|
value={props.value || ''}
|
||||||
@ -449,8 +548,9 @@ const NumberInput = (props) => (
|
|||||||
<Fragment>
|
<Fragment>
|
||||||
<Label slug={props.slug} title={props.title} tooltip={props.tooltip}
|
<Label slug={props.slug} title={props.title} tooltip={props.tooltip}
|
||||||
cat={props.cat} disabled={props.disabled}
|
cat={props.cat} disabled={props.disabled}
|
||||||
value={props.value || ''}
|
copying={props.copying}
|
||||||
/>
|
toggleCopyAttribute={props.toggleCopyAttribute}
|
||||||
|
copy={props.copy} />
|
||||||
<input className="form-control" type="number" step={props.step}
|
<input className="form-control" type="number" step={props.step}
|
||||||
id={props.slug} name={props.slug}
|
id={props.slug} name={props.slug}
|
||||||
value={props.value || ''}
|
value={props.value || ''}
|
||||||
@ -488,6 +588,7 @@ class YearEstimator extends Component {
|
|||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<NumberInput {...this.props} handleChange={this.props.handleChange}
|
<NumberInput {...this.props} handleChange={this.props.handleChange}
|
||||||
|
|
||||||
value={this.props.value} key={this.props.slug} />
|
value={this.props.value} key={this.props.slug} />
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -502,10 +603,19 @@ YearEstimator.propTypes = {
|
|||||||
date_lower: PropTypes.number,
|
date_lower: PropTypes.number,
|
||||||
value: PropTypes.number,
|
value: PropTypes.number,
|
||||||
disabled: PropTypes.bool,
|
disabled: PropTypes.bool,
|
||||||
handleChange: PropTypes.func
|
handleChange: PropTypes.func,
|
||||||
|
copy: PropTypes.bool,
|
||||||
|
toggleCopyAttribute: PropTypes.func,
|
||||||
|
copying: PropTypes.bool
|
||||||
}
|
}
|
||||||
|
|
||||||
const CheckboxInput = (props) => (
|
const CheckboxInput = (props) => (
|
||||||
|
<Fragment>
|
||||||
|
<Label slug={props.slug} title={props.title} tooltip={props.tooltip}
|
||||||
|
cat={props.cat} disabled={props.disabled}
|
||||||
|
copying={props.copying}
|
||||||
|
toggleCopyAttribute={props.toggleCopyAttribute}
|
||||||
|
copy={props.copy} />
|
||||||
<div className="form-check">
|
<div className="form-check">
|
||||||
<input className="form-check-input" type="checkbox"
|
<input className="form-check-input" type="checkbox"
|
||||||
id={props.slug} name={props.slug}
|
id={props.slug} name={props.slug}
|
||||||
@ -518,6 +628,7 @@ const CheckboxInput = (props) => (
|
|||||||
{ props.tooltip? <Tooltip text={ props.tooltip } /> : null }
|
{ props.tooltip? <Tooltip text={ props.tooltip } /> : null }
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
</Fragment>
|
||||||
)
|
)
|
||||||
|
|
||||||
CheckboxInput.propTypes = {
|
CheckboxInput.propTypes = {
|
||||||
@ -546,7 +657,7 @@ const LikeButton = (props) => (
|
|||||||
</div>
|
</div>
|
||||||
<p>
|
<p>
|
||||||
<NavLink
|
<NavLink
|
||||||
to={`/multi-edit/${props.cat}.html?k=like&v=${true}`}>
|
to={`/multi-edit/${props.cat}.html`}>
|
||||||
Like more buildings
|
Like more buildings
|
||||||
</NavLink>
|
</NavLink>
|
||||||
</p>
|
</p>
|
||||||
@ -564,26 +675,27 @@ LikeButton.propTypes = {
|
|||||||
handleLike: PropTypes.func
|
handleLike: PropTypes.func
|
||||||
}
|
}
|
||||||
|
|
||||||
const Label = (props) => (
|
const Label = (props) => {
|
||||||
|
return (
|
||||||
<label htmlFor={props.slug}>
|
<label htmlFor={props.slug}>
|
||||||
{props.title}
|
{props.title}
|
||||||
{ props.tooltip? <Tooltip text={ props.tooltip } /> : null }
|
{ (props.copying && props.cat && props.slug && !props.disabled)?
|
||||||
{ (props.cat && props.slug && !props.disabled)?
|
|
||||||
<div className="icon-buttons">
|
<div className="icon-buttons">
|
||||||
<NavLink
|
<label className="icon-button copy">
|
||||||
to={`/multi-edit/${props.cat}.html?k=${props.slug}&v=${props.value}`}
|
|
||||||
className="icon-button copy">
|
|
||||||
Copy
|
Copy
|
||||||
</NavLink>
|
<input type="checkbox" checked={props.copy}
|
||||||
|
onChange={() => props.toggleCopyAttribute(props.slug)}/>
|
||||||
|
</label>
|
||||||
</div> : null
|
</div> : null
|
||||||
}
|
}
|
||||||
|
{ props.tooltip? <Tooltip text={ props.tooltip } /> : null }
|
||||||
</label>
|
</label>
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Label.propTypes = {
|
Label.propTypes = {
|
||||||
slug: PropTypes.string,
|
slug: PropTypes.string,
|
||||||
cat: PropTypes.string,
|
cat: PropTypes.string,
|
||||||
value: PropTypes.any,
|
|
||||||
title: PropTypes.string,
|
title: PropTypes.string,
|
||||||
disabled: PropTypes.bool,
|
disabled: PropTypes.bool,
|
||||||
tooltip: PropTypes.string
|
tooltip: PropTypes.string
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import urlapi from 'url';
|
|
||||||
import React, { Fragment } from 'react';
|
import React, { Fragment } from 'react';
|
||||||
import { Link, NavLink } from 'react-router-dom';
|
import { Link, NavLink } from 'react-router-dom';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
@ -7,6 +6,7 @@ import Sidebar from './sidebar';
|
|||||||
import Tooltip from './tooltip';
|
import Tooltip from './tooltip';
|
||||||
import InfoBox from './info-box';
|
import InfoBox from './info-box';
|
||||||
import { EditIcon } from './icons';
|
import { EditIcon } from './icons';
|
||||||
|
import { sanitiseURL } from './helpers';
|
||||||
|
|
||||||
import CONFIG from './fields-config.json';
|
import CONFIG from './fields-config.json';
|
||||||
|
|
||||||
@ -29,43 +29,7 @@ const BuildingView = (props) => {
|
|||||||
<DataSection
|
<DataSection
|
||||||
key={section.slug} cat={cat}
|
key={section.slug} cat={cat}
|
||||||
building_id={props.building_id}
|
building_id={props.building_id}
|
||||||
{...section}>
|
{...section} {...props} />
|
||||||
{
|
|
||||||
section.fields.map(field => {
|
|
||||||
|
|
||||||
switch (field.type) {
|
|
||||||
case 'uprn_list':
|
|
||||||
return <UPRNsDataEntry
|
|
||||||
key={field.slug}
|
|
||||||
title={field.title}
|
|
||||||
value={props.uprns}
|
|
||||||
tooltip={field.tooltip} />
|
|
||||||
case 'text_multi':
|
|
||||||
return <MultiDataEntry
|
|
||||||
key={field.slug}
|
|
||||||
title={field.title}
|
|
||||||
value={props[field.slug]}
|
|
||||||
tooltip={field.tooltip} />
|
|
||||||
case 'like':
|
|
||||||
return <LikeDataEntry
|
|
||||||
key={field.slug}
|
|
||||||
title={field.title}
|
|
||||||
value={props[field.slug]}
|
|
||||||
user_building_like={props.building_like}
|
|
||||||
tooltip={field.tooltip} />
|
|
||||||
default:
|
|
||||||
return <DataEntry
|
|
||||||
key={field.slug}
|
|
||||||
slug={field.slug}
|
|
||||||
disabled={field.disabled}
|
|
||||||
cat={cat}
|
|
||||||
title={field.title}
|
|
||||||
value={props[field.slug]}
|
|
||||||
tooltip={field.tooltip} />
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</DataSection>
|
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
</Sidebar>
|
</Sidebar>
|
||||||
@ -82,8 +46,48 @@ BuildingView.propTypes = {
|
|||||||
building_like: PropTypes.bool
|
building_like: PropTypes.bool
|
||||||
}
|
}
|
||||||
|
|
||||||
const DataSection = (props) => {
|
class DataSection extends React.Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
copying: false,
|
||||||
|
values_to_copy: {}
|
||||||
|
};
|
||||||
|
this.toggleCopying = this.toggleCopying.bind(this);
|
||||||
|
this.toggleCopyAttribute = this.toggleCopyAttribute.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enter or exit "copying" state - allow user to select attributes to copy
|
||||||
|
*/
|
||||||
|
toggleCopying() {
|
||||||
|
this.setState({
|
||||||
|
copying: !this.state.copying
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keep track of data to copy (accumulate while in "copying" state)
|
||||||
|
*
|
||||||
|
* @param {string} key
|
||||||
|
*/
|
||||||
|
toggleCopyAttribute(key) {
|
||||||
|
const value = this.props[key];
|
||||||
|
const values = this.state.values_to_copy;
|
||||||
|
if(Object.keys(this.state.values_to_copy).includes(key)){
|
||||||
|
delete values[key];
|
||||||
|
} else {
|
||||||
|
values[key] = value;
|
||||||
|
}
|
||||||
|
this.setState({
|
||||||
|
values_to_copy: values
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const props = this.props;
|
||||||
const match = props.cat === props.slug;
|
const match = props.cat === props.slug;
|
||||||
|
const data_string = JSON.stringify(this.state.values_to_copy);
|
||||||
return (
|
return (
|
||||||
<section id={props.slug} className={(props.inactive)? 'data-section inactive': 'data-section'}>
|
<section id={props.slug} className={(props.inactive)? 'data-section inactive': 'data-section'}>
|
||||||
<header className={`section-header view ${props.slug} ${(match? 'active' : '')}`}>
|
<header className={`section-header view ${props.slug} ${(match? 'active' : '')}`}>
|
||||||
@ -96,14 +100,29 @@ const DataSection = (props) => {
|
|||||||
</NavLink>
|
</NavLink>
|
||||||
<nav className="icon-buttons">
|
<nav className="icon-buttons">
|
||||||
{
|
{
|
||||||
props.help?
|
(match && !props.inactive)?
|
||||||
|
this.state.copying?
|
||||||
|
<Fragment>
|
||||||
|
<NavLink
|
||||||
|
to={`/multi-edit/${props.cat}.html?data=${data_string}`}
|
||||||
|
className="icon-button copy">
|
||||||
|
Copy selected
|
||||||
|
</NavLink>
|
||||||
|
<a className="icon-button copy" onClick={this.toggleCopying}>Cancel</a>
|
||||||
|
</Fragment>
|
||||||
|
:
|
||||||
|
<a className="icon-button copy" onClick={this.toggleCopying}>Copy</a>
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
{
|
||||||
|
props.help && !this.state.copying?
|
||||||
<a className="icon-button help" title="Find out more" href={props.help}>
|
<a className="icon-button help" title="Find out more" href={props.help}>
|
||||||
Info
|
Info
|
||||||
</a>
|
</a>
|
||||||
: null
|
: null
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
!props.inactive?
|
!props.inactive && !this.state.copying?
|
||||||
<NavLink className="icon-button edit" title="Edit data"
|
<NavLink className="icon-button edit" title="Edit data"
|
||||||
to={`/edit/${props.slug}/building/${props.building_id}.html`}>
|
to={`/edit/${props.slug}/building/${props.building_id}.html`}>
|
||||||
Edit
|
Edit
|
||||||
@ -116,13 +135,64 @@ const DataSection = (props) => {
|
|||||||
{
|
{
|
||||||
match?
|
match?
|
||||||
!props.inactive?
|
!props.inactive?
|
||||||
<dl className="data-list">{props.children}</dl>
|
<dl className="data-list">
|
||||||
|
{
|
||||||
|
props.fields.map(field => {
|
||||||
|
|
||||||
|
switch (field.type) {
|
||||||
|
case 'uprn_list':
|
||||||
|
return <UPRNsDataEntry
|
||||||
|
key={field.slug}
|
||||||
|
title={field.title}
|
||||||
|
value={props.uprns}
|
||||||
|
tooltip={field.tooltip} />
|
||||||
|
case 'text_multi':
|
||||||
|
return <MultiDataEntry
|
||||||
|
key={field.slug}
|
||||||
|
slug={field.slug}
|
||||||
|
disabled={field.disabled}
|
||||||
|
cat={props.cat}
|
||||||
|
title={field.title}
|
||||||
|
value={props[field.slug]}
|
||||||
|
|
||||||
|
copying={this.state.copying}
|
||||||
|
toggleCopyAttribute={this.toggleCopyAttribute}
|
||||||
|
copy={Object.keys(this.state.values_to_copy).includes(field.slug)}
|
||||||
|
|
||||||
|
tooltip={field.tooltip} />
|
||||||
|
case 'like':
|
||||||
|
return <LikeDataEntry
|
||||||
|
key={field.slug}
|
||||||
|
title={field.title}
|
||||||
|
value={props[field.slug]}
|
||||||
|
user_building_like={props.building_like}
|
||||||
|
tooltip={field.tooltip} />
|
||||||
|
default:
|
||||||
|
return <DataEntry
|
||||||
|
key={field.slug}
|
||||||
|
slug={field.slug}
|
||||||
|
disabled={field.disabled}
|
||||||
|
cat={props.cat}
|
||||||
|
title={field.title}
|
||||||
|
value={props[field.slug]}
|
||||||
|
|
||||||
|
copying={this.state.copying}
|
||||||
|
toggleCopyAttribute={this.toggleCopyAttribute}
|
||||||
|
copy={Object.keys(this.state.values_to_copy).includes(field.slug)}
|
||||||
|
|
||||||
|
tooltip={field.tooltip} />
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
</dl>
|
||||||
: <p className="data-intro">{props.intro}</p>
|
: <p className="data-intro">{props.intro}</p>
|
||||||
: null
|
: null
|
||||||
}
|
}
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
DataSection.propTypes = {
|
DataSection.propTypes = {
|
||||||
title: PropTypes.string,
|
title: PropTypes.string,
|
||||||
@ -135,18 +205,19 @@ DataSection.propTypes = {
|
|||||||
children: PropTypes.node
|
children: PropTypes.node
|
||||||
}
|
}
|
||||||
|
|
||||||
const DataEntry = (props) => (
|
const DataEntry = (props) => {
|
||||||
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<dt>
|
<dt>
|
||||||
{ props.title }
|
{ props.title }
|
||||||
{ props.tooltip? <Tooltip text={ props.tooltip } /> : null }
|
{ props.tooltip? <Tooltip text={ props.tooltip } /> : null }
|
||||||
{ (props.cat && props.slug && !props.disabled)?
|
{ (props.copying && props.cat && props.slug && !props.disabled)?
|
||||||
<div className="icon-buttons">
|
<div className="icon-buttons">
|
||||||
<NavLink
|
<label className="icon-button copy">
|
||||||
to={`/multi-edit/${props.cat}.html?k=${props.slug}&v=${props.value}`}
|
|
||||||
className="icon-button copy">
|
|
||||||
Copy
|
Copy
|
||||||
</NavLink>
|
<input type="checkbox" checked={props.copy}
|
||||||
|
onChange={() => props.toggleCopyAttribute(props.slug)}/>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
: null
|
: null
|
||||||
}
|
}
|
||||||
@ -159,6 +230,7 @@ const DataEntry = (props) => (
|
|||||||
: '\u00A0'}</dd>
|
: '\u00A0'}</dd>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
DataEntry.propTypes = {
|
DataEntry.propTypes = {
|
||||||
title: PropTypes.string,
|
title: PropTypes.string,
|
||||||
@ -169,14 +241,16 @@ DataEntry.propTypes = {
|
|||||||
value: PropTypes.any
|
value: PropTypes.any
|
||||||
}
|
}
|
||||||
|
|
||||||
const LikeDataEntry = (props) => (
|
const LikeDataEntry = (props) => {
|
||||||
|
const data_string = JSON.stringify({like: true});
|
||||||
|
(
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<dt>
|
<dt>
|
||||||
{ props.title }
|
{ props.title }
|
||||||
{ props.tooltip? <Tooltip text={ props.tooltip } /> : null }
|
{ props.tooltip? <Tooltip text={ props.tooltip } /> : null }
|
||||||
<div className="icon-buttons">
|
<div className="icon-buttons">
|
||||||
<NavLink
|
<NavLink
|
||||||
to={`/multi-edit/${props.cat}.html?k=like&v=${true}`}
|
to={`/multi-edit/${props.cat}.html?data=${data_string}`}
|
||||||
className="icon-button copy">
|
className="icon-button copy">
|
||||||
Copy
|
Copy
|
||||||
</NavLink>
|
</NavLink>
|
||||||
@ -196,6 +270,7 @@ const LikeDataEntry = (props) => (
|
|||||||
}
|
}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
LikeDataEntry.propTypes = {
|
LikeDataEntry.propTypes = {
|
||||||
title: PropTypes.string,
|
title: PropTypes.string,
|
||||||
@ -223,6 +298,16 @@ const MultiDataEntry = (props) => {
|
|||||||
<dt>
|
<dt>
|
||||||
{ props.title }
|
{ props.title }
|
||||||
{ props.tooltip? <Tooltip text={ props.tooltip } /> : null }
|
{ props.tooltip? <Tooltip text={ props.tooltip } /> : null }
|
||||||
|
{ (props.copying && props.cat && props.slug && !props.disabled)?
|
||||||
|
<div className="icon-buttons">
|
||||||
|
<label className="icon-button copy">
|
||||||
|
Copy
|
||||||
|
<input type="checkbox" checked={props.copy}
|
||||||
|
onChange={() => props.toggleCopyAttribute(props.slug)}/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
: null
|
||||||
|
}
|
||||||
</dt>
|
</dt>
|
||||||
<dd>{ content }</dd>
|
<dd>{ content }</dd>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
@ -235,42 +320,6 @@ MultiDataEntry.propTypes = {
|
|||||||
value: PropTypes.arrayOf(PropTypes.string)
|
value: PropTypes.arrayOf(PropTypes.string)
|
||||||
}
|
}
|
||||||
|
|
||||||
function sanitiseURL(string){
|
|
||||||
let url_
|
|
||||||
|
|
||||||
// http or https
|
|
||||||
if (!(string.substring(0, 7) === 'http://' || string.substring(0, 8) === 'https://')){
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
url_ = document.createElement('a')
|
|
||||||
url_.href = string
|
|
||||||
} catch (error) {
|
|
||||||
try {
|
|
||||||
url_ = urlapi.parse(string)
|
|
||||||
} catch (error) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// required (www.example.com)
|
|
||||||
if (!url_.hostname || url_.hostname === '' || url_.hostname === 'localhost'){
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
// optional (/some/path)
|
|
||||||
// url_.pathname;
|
|
||||||
|
|
||||||
// optional (?name=value)
|
|
||||||
// url_.search;
|
|
||||||
|
|
||||||
// optional (#anchor)
|
|
||||||
// url_.hash;
|
|
||||||
|
|
||||||
return `${url_.protocol}//${url_.hostname}${url_.pathname || ''}${url_.search || ''}${url_.hash || ''}`
|
|
||||||
}
|
|
||||||
|
|
||||||
const UPRNsDataEntry = (props) => {
|
const UPRNsDataEntry = (props) => {
|
||||||
const uprns = props.value || [];
|
const uprns = props.value || [];
|
||||||
const noParent = uprns.filter(uprn => uprn.parent_uprn == null);
|
const noParent = uprns.filter(uprn => uprn.parent_uprn == null);
|
||||||
|
39
app/src/frontend/helpers.js
Normal file
39
app/src/frontend/helpers.js
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import urlapi from 'url';
|
||||||
|
|
||||||
|
function sanitiseURL(string){
|
||||||
|
let url_
|
||||||
|
|
||||||
|
// http or https
|
||||||
|
if (!(string.substring(0, 7) === 'http://' || string.substring(0, 8) === 'https://')){
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
url_ = document.createElement('a')
|
||||||
|
url_.href = string
|
||||||
|
} catch (error) {
|
||||||
|
try {
|
||||||
|
url_ = urlapi.parse(string)
|
||||||
|
} catch (error) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// required (www.example.com)
|
||||||
|
if (!url_.hostname || url_.hostname === '' || url_.hostname === 'localhost'){
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// optional (/some/path)
|
||||||
|
// url_.pathname;
|
||||||
|
|
||||||
|
// optional (?name=value)
|
||||||
|
// url_.search;
|
||||||
|
|
||||||
|
// optional (#anchor)
|
||||||
|
// url_.hash;
|
||||||
|
|
||||||
|
return `${url_.protocol}//${url_.hostname}${url_.pathname || ''}${url_.search || ''}${url_.hash || ''}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export { sanitiseURL }
|
@ -1,4 +1,4 @@
|
|||||||
import React from 'react';
|
import React, { Fragment } from 'react';
|
||||||
import { Link, Redirect } from 'react-router-dom';
|
import { Link, Redirect } from 'react-router-dom';
|
||||||
import { parse } from 'query-string';
|
import { parse } from 'query-string';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
@ -6,6 +6,7 @@ import PropTypes from 'prop-types';
|
|||||||
import Sidebar from './sidebar';
|
import Sidebar from './sidebar';
|
||||||
import CONFIG from './fields-config.json';
|
import CONFIG from './fields-config.json';
|
||||||
import InfoBox from './info-box';
|
import InfoBox from './info-box';
|
||||||
|
import { sanitiseURL } from './helpers';
|
||||||
|
|
||||||
const MultiEdit = (props) => {
|
const MultiEdit = (props) => {
|
||||||
if (!props.user){
|
if (!props.user){
|
||||||
@ -34,7 +35,7 @@ const MultiEdit = (props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const q = parse(props.location.search);
|
const q = parse(props.location.search);
|
||||||
const label = fieldTitleFromSlug(q.k);
|
const data = JSON.parse(q.data)
|
||||||
const title = sectionTitleFromCat(cat);
|
const title = sectionTitleFromCat(cat);
|
||||||
return (
|
return (
|
||||||
<Sidebar
|
<Sidebar
|
||||||
@ -44,9 +45,16 @@ const MultiEdit = (props) => {
|
|||||||
<header className={`section-header view ${cat} active`}>
|
<header className={`section-header view ${cat} active`}>
|
||||||
<a><h3 className="h3">{title}</h3></a>
|
<a><h3 className="h3">{title}</h3></a>
|
||||||
</header>
|
</header>
|
||||||
<p class='data-intro'>Set <strong>{label}</strong> to <strong>{q.v}</strong></p>
|
<dl className='data-list'>
|
||||||
|
{
|
||||||
|
Object.keys(data).map((key => {
|
||||||
|
const label = fieldTitleFromSlug(key);
|
||||||
|
return <DataEntry key={key} label={label} value={data[key]}/>
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
</dl>
|
||||||
<form className='buttons-container'>
|
<form className='buttons-container'>
|
||||||
<InfoBox msg='Click buildings to colour' />
|
<InfoBox msg='Click buildings to colour using the data above' />
|
||||||
|
|
||||||
<Link to={`/view/${cat}.html`} className='btn btn-secondary'>Back to view</Link>
|
<Link to={`/view/${cat}.html`} className='btn btn-secondary'>Back to view</Link>
|
||||||
<Link to={`/edit/${cat}.html`} className='btn btn-secondary'>Back to edit</Link>
|
<Link to={`/edit/${cat}.html`} className='btn btn-secondary'>Back to edit</Link>
|
||||||
@ -62,6 +70,37 @@ MultiEdit.propTypes = {
|
|||||||
location: PropTypes.object
|
location: PropTypes.object
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const DataEntry = (props) => {
|
||||||
|
let content;
|
||||||
|
|
||||||
|
if (props.value != null && props.value !== '') {
|
||||||
|
if (typeof(props.value) === 'boolean') {
|
||||||
|
content = (props.value)? 'Yes' : 'No'
|
||||||
|
} else if (Array.isArray(props.value)) {
|
||||||
|
if (props.value.length) {
|
||||||
|
content = <ul>{
|
||||||
|
props.value.map((item, index) => {
|
||||||
|
return <li key={index}><a href={sanitiseURL(item)}>{item}</a></li>
|
||||||
|
})
|
||||||
|
}</ul>
|
||||||
|
} else {
|
||||||
|
content = '\u00A0'
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
content = props.value
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
content = '\u00A0'
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<dt>{props.label}</dt>
|
||||||
|
<dd>{content}</dd>
|
||||||
|
</Fragment>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function sectionTitleFromCat(cat) {
|
function sectionTitleFromCat(cat) {
|
||||||
for (let index = 0; index < CONFIG.length; index++) {
|
for (let index = 0; index < CONFIG.length; index++) {
|
||||||
const section = CONFIG[index];
|
const section = CONFIG[index];
|
||||||
|
@ -28,7 +28,7 @@
|
|||||||
padding: 0.25rem 0.5rem;
|
padding: 0.25rem 0.5rem;
|
||||||
border-bottom: 1px solid #eee;
|
border-bottom: 1px solid #eee;
|
||||||
}
|
}
|
||||||
.search-box .collapse-btn {
|
.collapse-btn {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0.5rem;
|
top: 0.5rem;
|
||||||
left: 0.5rem;
|
left: 0.5rem;
|
||||||
|
@ -288,9 +288,17 @@
|
|||||||
color: rgb(11, 225, 225);
|
color: rgb(11, 225, 225);
|
||||||
}
|
}
|
||||||
.icon-button.help,
|
.icon-button.help,
|
||||||
.section-header .icon-button.help {
|
.section-header .icon-button.help,
|
||||||
|
.section-header .icon-button.copy {
|
||||||
margin-top: 2px;
|
margin-top: 2px;
|
||||||
}
|
}
|
||||||
|
.section-header .icon-button.copy {
|
||||||
|
cursor: pointer;
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
.data-section label .icon-buttons .icon-button.copy {
|
||||||
|
margin-top: 0px;
|
||||||
|
}
|
||||||
.icon-button.copy:hover,
|
.icon-button.copy:hover,
|
||||||
.icon-button.help:hover {
|
.icon-button.help:hover {
|
||||||
color: rgb(0, 81, 255)
|
color: rgb(0, 81, 255)
|
||||||
@ -380,6 +388,7 @@ label .icon-buttons,
|
|||||||
.data-list dd {
|
.data-list dd {
|
||||||
margin: 0 0 0.5rem;
|
margin: 0 0 0.5rem;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
|
white-space: pre;
|
||||||
}
|
}
|
||||||
.data-list .no-data {
|
.data-list .no-data {
|
||||||
color: #999;
|
color: #999;
|
||||||
|
@ -17,6 +17,7 @@ form .alert {
|
|||||||
}
|
}
|
||||||
.form-check-input {
|
.form-check-input {
|
||||||
margin-top: 0.6rem;
|
margin-top: 0.6rem;
|
||||||
|
margin-left: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
}
|
}
|
||||||
label {
|
label {
|
||||||
|
Loading…
Reference in New Issue
Block a user