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) {
|
||||
const cat = parseCategoryURL(window.location.pathname);
|
||||
const q = parse(window.location.search);
|
||||
let data;
|
||||
const data = (cat === 'like')? {like: true}: JSON.parse(q.data);
|
||||
if (cat === 'like'){
|
||||
data = {like: true}
|
||||
this.likeBuilding(building.building_id)
|
||||
} else {
|
||||
data = {}
|
||||
data[q.k] = q.v;
|
||||
this.updateBuilding(building.building_id, data)
|
||||
}
|
||||
}
|
||||
|
@ -50,18 +50,52 @@ BuildingEdit.propTypes = {
|
||||
class EditForm extends Component {
|
||||
constructor(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) {
|
||||
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.handleCheck = this.handleCheck.bind(this);
|
||||
this.handleLike = this.handleLike.bind(this);
|
||||
this.handleSubmit = this.handleSubmit.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 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 (
|
||||
<section className={(this.props.inactive)? 'data-section inactive': 'data-section'}>
|
||||
<header className={`section-header edit ${this.props.slug} ${(match? 'active' : '')}`}>
|
||||
@ -187,14 +227,38 @@ class EditForm extends Component {
|
||||
</NavLink>
|
||||
<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}>
|
||||
Info
|
||||
</a>
|
||||
: 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"
|
||||
onClick={this.handleSubmit}
|
||||
to={`/edit/${this.props.slug}/building/${this.props.building_id}.html`}>
|
||||
@ -221,24 +285,45 @@ class EditForm extends Component {
|
||||
switch (props.type) {
|
||||
case 'text':
|
||||
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} />
|
||||
case 'text_list':
|
||||
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} />
|
||||
case 'text_long':
|
||||
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} />
|
||||
case 'number':
|
||||
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} />
|
||||
case 'year_estimator':
|
||||
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} />
|
||||
case 'text_multi':
|
||||
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} />
|
||||
case 'checkbox':
|
||||
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} />
|
||||
case 'like':
|
||||
return <LikeButton {...props} handleLike={this.handleLike}
|
||||
@ -285,9 +370,11 @@ EditForm.propTypes = {
|
||||
const TextInput = (props) => (
|
||||
<Fragment>
|
||||
<Label slug={props.slug} title={props.title} tooltip={props.tooltip}
|
||||
cat={props.cat} disabled={props.disabled}
|
||||
value={props.value || ''}
|
||||
/>
|
||||
copying={props.copying}
|
||||
toggleCopyAttribute={props.toggleCopyAttribute}
|
||||
copy={props.copy}
|
||||
cat={props.cat}
|
||||
disabled={props.disabled} />
|
||||
<input className="form-control" type="text"
|
||||
id={props.slug} name={props.slug}
|
||||
value={props.value || ''}
|
||||
@ -314,6 +401,9 @@ TextInput.propTypes = {
|
||||
const LongTextInput = (props) => (
|
||||
<Fragment>
|
||||
<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} />
|
||||
<textarea className="form-control"
|
||||
id={props.slug} name={props.slug}
|
||||
@ -375,7 +465,12 @@ class MultiTextInput extends Component {
|
||||
const values = this.getValues();
|
||||
return (
|
||||
<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) => (
|
||||
<div className="input-group" key={i}>
|
||||
@ -409,15 +504,19 @@ MultiTextInput.propTypes = {
|
||||
value: PropTypes.arrayOf(PropTypes.string),
|
||||
placeholder: PropTypes.string,
|
||||
disabled: PropTypes.bool,
|
||||
handleChange: PropTypes.func
|
||||
handleChange: PropTypes.func,
|
||||
copy: PropTypes.bool,
|
||||
toggleCopyAttribute: PropTypes.func,
|
||||
copying: PropTypes.bool
|
||||
}
|
||||
|
||||
const TextListInput = (props) => (
|
||||
<Fragment>
|
||||
<Label slug={props.slug} title={props.title} tooltip={props.tooltip}
|
||||
cat={props.cat} disabled={props.disabled}
|
||||
value={props.value || ''}
|
||||
/>
|
||||
copying={props.copying}
|
||||
toggleCopyAttribute={props.toggleCopyAttribute}
|
||||
copy={props.copy} />
|
||||
<select className="form-control"
|
||||
id={props.slug} name={props.slug}
|
||||
value={props.value || ''}
|
||||
@ -449,8 +548,9 @@ const NumberInput = (props) => (
|
||||
<Fragment>
|
||||
<Label slug={props.slug} title={props.title} tooltip={props.tooltip}
|
||||
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}
|
||||
id={props.slug} name={props.slug}
|
||||
value={props.value || ''}
|
||||
@ -488,6 +588,7 @@ class YearEstimator extends Component {
|
||||
render() {
|
||||
return (
|
||||
<NumberInput {...this.props} handleChange={this.props.handleChange}
|
||||
|
||||
value={this.props.value} key={this.props.slug} />
|
||||
)
|
||||
}
|
||||
@ -502,22 +603,32 @@ YearEstimator.propTypes = {
|
||||
date_lower: PropTypes.number,
|
||||
value: PropTypes.number,
|
||||
disabled: PropTypes.bool,
|
||||
handleChange: PropTypes.func
|
||||
handleChange: PropTypes.func,
|
||||
copy: PropTypes.bool,
|
||||
toggleCopyAttribute: PropTypes.func,
|
||||
copying: PropTypes.bool
|
||||
}
|
||||
|
||||
const CheckboxInput = (props) => (
|
||||
<div className="form-check">
|
||||
<input className="form-check-input" type="checkbox"
|
||||
id={props.slug} name={props.slug}
|
||||
checked={!!props.value}
|
||||
disabled={props.disabled}
|
||||
onChange={props.handleChange}
|
||||
/>
|
||||
<label htmlFor={props.slug} className="form-check-label">
|
||||
{props.title}
|
||||
{ props.tooltip? <Tooltip text={ props.tooltip } /> : null }
|
||||
</label>
|
||||
</div>
|
||||
<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">
|
||||
<input className="form-check-input" type="checkbox"
|
||||
id={props.slug} name={props.slug}
|
||||
checked={!!props.value}
|
||||
disabled={props.disabled}
|
||||
onChange={props.handleChange}
|
||||
/>
|
||||
<label htmlFor={props.slug} className="form-check-label">
|
||||
{props.title}
|
||||
{ props.tooltip? <Tooltip text={ props.tooltip } /> : null }
|
||||
</label>
|
||||
</div>
|
||||
</Fragment>
|
||||
)
|
||||
|
||||
CheckboxInput.propTypes = {
|
||||
@ -546,7 +657,7 @@ const LikeButton = (props) => (
|
||||
</div>
|
||||
<p>
|
||||
<NavLink
|
||||
to={`/multi-edit/${props.cat}.html?k=like&v=${true}`}>
|
||||
to={`/multi-edit/${props.cat}.html`}>
|
||||
Like more buildings
|
||||
</NavLink>
|
||||
</p>
|
||||
@ -564,26 +675,27 @@ LikeButton.propTypes = {
|
||||
handleLike: PropTypes.func
|
||||
}
|
||||
|
||||
const Label = (props) => (
|
||||
<label htmlFor={props.slug}>
|
||||
{props.title}
|
||||
{ props.tooltip? <Tooltip text={ props.tooltip } /> : null }
|
||||
{ (props.cat && props.slug && !props.disabled)?
|
||||
<div className="icon-buttons">
|
||||
<NavLink
|
||||
to={`/multi-edit/${props.cat}.html?k=${props.slug}&v=${props.value}`}
|
||||
className="icon-button copy">
|
||||
Copy
|
||||
</NavLink>
|
||||
</div> : null
|
||||
}
|
||||
</label>
|
||||
);
|
||||
const Label = (props) => {
|
||||
return (
|
||||
<label htmlFor={props.slug}>
|
||||
{props.title}
|
||||
{ (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
|
||||
}
|
||||
{ props.tooltip? <Tooltip text={ props.tooltip } /> : null }
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
Label.propTypes = {
|
||||
slug: PropTypes.string,
|
||||
cat: PropTypes.string,
|
||||
value: PropTypes.any,
|
||||
title: PropTypes.string,
|
||||
disabled: PropTypes.bool,
|
||||
tooltip: PropTypes.string
|
||||
|
@ -1,4 +1,3 @@
|
||||
import urlapi from 'url';
|
||||
import React, { Fragment } from 'react';
|
||||
import { Link, NavLink } from 'react-router-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
@ -7,6 +6,7 @@ import Sidebar from './sidebar';
|
||||
import Tooltip from './tooltip';
|
||||
import InfoBox from './info-box';
|
||||
import { EditIcon } from './icons';
|
||||
import { sanitiseURL } from './helpers';
|
||||
|
||||
import CONFIG from './fields-config.json';
|
||||
|
||||
@ -29,43 +29,7 @@ const BuildingView = (props) => {
|
||||
<DataSection
|
||||
key={section.slug} cat={cat}
|
||||
building_id={props.building_id}
|
||||
{...section}>
|
||||
{
|
||||
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>
|
||||
{...section} {...props} />
|
||||
))
|
||||
}
|
||||
</Sidebar>
|
||||
@ -82,46 +46,152 @@ BuildingView.propTypes = {
|
||||
building_like: PropTypes.bool
|
||||
}
|
||||
|
||||
const DataSection = (props) => {
|
||||
const match = props.cat === props.slug;
|
||||
return (
|
||||
<section id={props.slug} className={(props.inactive)? 'data-section inactive': 'data-section'}>
|
||||
<header className={`section-header view ${props.slug} ${(match? 'active' : '')}`}>
|
||||
<NavLink
|
||||
to={`/view/${props.slug}/building/${props.building_id}.html`}
|
||||
title={(props.inactive)? 'Coming soon… Click the ? for more info.' :
|
||||
(match)? 'Hide details' : 'Show details'}
|
||||
isActive={() => match}>
|
||||
<h3 className="h3">{props.title}</h3>
|
||||
</NavLink>
|
||||
<nav className="icon-buttons">
|
||||
{
|
||||
props.help?
|
||||
<a className="icon-button help" title="Find out more" href={props.help}>
|
||||
Info
|
||||
</a>
|
||||
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 data_string = JSON.stringify(this.state.values_to_copy);
|
||||
return (
|
||||
<section id={props.slug} className={(props.inactive)? 'data-section inactive': 'data-section'}>
|
||||
<header className={`section-header view ${props.slug} ${(match? 'active' : '')}`}>
|
||||
<NavLink
|
||||
to={`/view/${props.slug}/building/${props.building_id}.html`}
|
||||
title={(props.inactive)? 'Coming soon… Click the ? for more info.' :
|
||||
(match)? 'Hide details' : 'Show details'}
|
||||
isActive={() => match}>
|
||||
<h3 className="h3">{props.title}</h3>
|
||||
</NavLink>
|
||||
<nav className="icon-buttons">
|
||||
{
|
||||
(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}>
|
||||
Info
|
||||
</a>
|
||||
: null
|
||||
}
|
||||
{
|
||||
!props.inactive && !this.state.copying?
|
||||
<NavLink className="icon-button edit" title="Edit data"
|
||||
to={`/edit/${props.slug}/building/${props.building_id}.html`}>
|
||||
Edit
|
||||
<EditIcon />
|
||||
</NavLink>
|
||||
: null
|
||||
}
|
||||
</nav>
|
||||
</header>
|
||||
{
|
||||
match?
|
||||
!props.inactive?
|
||||
<NavLink className="icon-button edit" title="Edit data"
|
||||
to={`/edit/${props.slug}/building/${props.building_id}.html`}>
|
||||
Edit
|
||||
<EditIcon />
|
||||
</NavLink>
|
||||
: null
|
||||
}
|
||||
</nav>
|
||||
</header>
|
||||
{
|
||||
match?
|
||||
!props.inactive?
|
||||
<dl className="data-list">{props.children}</dl>
|
||||
: <p className="data-intro">{props.intro}</p>
|
||||
: null
|
||||
}
|
||||
</section>
|
||||
);
|
||||
<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>
|
||||
: null
|
||||
}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
DataSection.propTypes = {
|
||||
@ -135,30 +205,32 @@ DataSection.propTypes = {
|
||||
children: PropTypes.node
|
||||
}
|
||||
|
||||
const DataEntry = (props) => (
|
||||
<Fragment>
|
||||
<dt>
|
||||
{ props.title }
|
||||
{ props.tooltip? <Tooltip text={ props.tooltip } /> : null }
|
||||
{ (props.cat && props.slug && !props.disabled)?
|
||||
<div className="icon-buttons">
|
||||
<NavLink
|
||||
to={`/multi-edit/${props.cat}.html?k=${props.slug}&v=${props.value}`}
|
||||
className="icon-button copy">
|
||||
Copy
|
||||
</NavLink>
|
||||
</div>
|
||||
: null
|
||||
}
|
||||
</dt>
|
||||
<dd>{
|
||||
(props.value != null && props.value !== '')?
|
||||
(typeof(props.value) === 'boolean')?
|
||||
(props.value)? 'Yes' : 'No'
|
||||
: props.value
|
||||
: '\u00A0'}</dd>
|
||||
</Fragment>
|
||||
);
|
||||
const DataEntry = (props) => {
|
||||
return (
|
||||
<Fragment>
|
||||
<dt>
|
||||
{ props.title }
|
||||
{ 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>
|
||||
<dd>{
|
||||
(props.value != null && props.value !== '')?
|
||||
(typeof(props.value) === 'boolean')?
|
||||
(props.value)? 'Yes' : 'No'
|
||||
: props.value
|
||||
: '\u00A0'}</dd>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
DataEntry.propTypes = {
|
||||
title: PropTypes.string,
|
||||
@ -169,33 +241,36 @@ DataEntry.propTypes = {
|
||||
value: PropTypes.any
|
||||
}
|
||||
|
||||
const LikeDataEntry = (props) => (
|
||||
<Fragment>
|
||||
<dt>
|
||||
{ props.title }
|
||||
{ props.tooltip? <Tooltip text={ props.tooltip } /> : null }
|
||||
<div className="icon-buttons">
|
||||
<NavLink
|
||||
to={`/multi-edit/${props.cat}.html?k=like&v=${true}`}
|
||||
className="icon-button copy">
|
||||
Copy
|
||||
</NavLink>
|
||||
</div>
|
||||
</dt>
|
||||
<dd>
|
||||
const LikeDataEntry = (props) => {
|
||||
const data_string = JSON.stringify({like: true});
|
||||
(
|
||||
<Fragment>
|
||||
<dt>
|
||||
{ props.title }
|
||||
{ props.tooltip? <Tooltip text={ props.tooltip } /> : null }
|
||||
<div className="icon-buttons">
|
||||
<NavLink
|
||||
to={`/multi-edit/${props.cat}.html?data=${data_string}`}
|
||||
className="icon-button copy">
|
||||
Copy
|
||||
</NavLink>
|
||||
</div>
|
||||
</dt>
|
||||
<dd>
|
||||
{
|
||||
(props.value != null)?
|
||||
(props.value === 1)?
|
||||
`${props.value} person likes this building`
|
||||
: `${props.value} people like this building`
|
||||
: '\u00A0'
|
||||
}
|
||||
</dd>
|
||||
{
|
||||
(props.value != null)?
|
||||
(props.value === 1)?
|
||||
`${props.value} person likes this building`
|
||||
: `${props.value} people like this building`
|
||||
: '\u00A0'
|
||||
(props.user_building_like)? <dd>…including you!</dd> : null
|
||||
}
|
||||
</dd>
|
||||
{
|
||||
(props.user_building_like)? <dd>…including you!</dd> : null
|
||||
}
|
||||
</Fragment>
|
||||
);
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
LikeDataEntry.propTypes = {
|
||||
title: PropTypes.string,
|
||||
@ -223,6 +298,16 @@ const MultiDataEntry = (props) => {
|
||||
<dt>
|
||||
{ props.title }
|
||||
{ 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>
|
||||
<dd>{ content }</dd>
|
||||
</Fragment>
|
||||
@ -235,42 +320,6 @@ MultiDataEntry.propTypes = {
|
||||
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 uprns = props.value || [];
|
||||
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 { parse } from 'query-string';
|
||||
import PropTypes from 'prop-types';
|
||||
@ -6,6 +6,7 @@ import PropTypes from 'prop-types';
|
||||
import Sidebar from './sidebar';
|
||||
import CONFIG from './fields-config.json';
|
||||
import InfoBox from './info-box';
|
||||
import { sanitiseURL } from './helpers';
|
||||
|
||||
const MultiEdit = (props) => {
|
||||
if (!props.user){
|
||||
@ -34,7 +35,7 @@ const MultiEdit = (props) => {
|
||||
}
|
||||
|
||||
const q = parse(props.location.search);
|
||||
const label = fieldTitleFromSlug(q.k);
|
||||
const data = JSON.parse(q.data)
|
||||
const title = sectionTitleFromCat(cat);
|
||||
return (
|
||||
<Sidebar
|
||||
@ -44,9 +45,16 @@ const MultiEdit = (props) => {
|
||||
<header className={`section-header view ${cat} active`}>
|
||||
<a><h3 className="h3">{title}</h3></a>
|
||||
</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'>
|
||||
<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={`/edit/${cat}.html`} className='btn btn-secondary'>Back to edit</Link>
|
||||
@ -62,6 +70,37 @@ MultiEdit.propTypes = {
|
||||
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) {
|
||||
for (let index = 0; index < CONFIG.length; index++) {
|
||||
const section = CONFIG[index];
|
||||
|
@ -28,7 +28,7 @@
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
.search-box .collapse-btn {
|
||||
.collapse-btn {
|
||||
position: absolute;
|
||||
top: 0.5rem;
|
||||
left: 0.5rem;
|
||||
|
@ -288,9 +288,17 @@
|
||||
color: rgb(11, 225, 225);
|
||||
}
|
||||
.icon-button.help,
|
||||
.section-header .icon-button.help {
|
||||
.section-header .icon-button.help,
|
||||
.section-header .icon-button.copy {
|
||||
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.help:hover {
|
||||
color: rgb(0, 81, 255)
|
||||
@ -380,6 +388,7 @@ label .icon-buttons,
|
||||
.data-list dd {
|
||||
margin: 0 0 0.5rem;
|
||||
line-height: 1.5;
|
||||
white-space: pre;
|
||||
}
|
||||
.data-list .no-data {
|
||||
color: #999;
|
||||
|
@ -17,6 +17,7 @@ form .alert {
|
||||
}
|
||||
.form-check-input {
|
||||
margin-top: 0.6rem;
|
||||
margin-left: 0;
|
||||
left: 0;
|
||||
}
|
||||
label {
|
||||
|
Loading…
Reference in New Issue
Block a user