Merge pull request #350 from tomalrussell/feature/multi-copy

Feature/multi copy
This commit is contained in:
Tom Russell 2019-08-06 22:28:13 +01:00 committed by GitHub
commit 9433fd879b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 470 additions and 215 deletions

View File

@ -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)
} }
} }

View File

@ -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,22 +603,32 @@ 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) => (
<div className="form-check"> <Fragment>
<input className="form-check-input" type="checkbox" <Label slug={props.slug} title={props.title} tooltip={props.tooltip}
id={props.slug} name={props.slug} cat={props.cat} disabled={props.disabled}
checked={!!props.value} copying={props.copying}
disabled={props.disabled} toggleCopyAttribute={props.toggleCopyAttribute}
onChange={props.handleChange} copy={props.copy} />
/> <div className="form-check">
<label htmlFor={props.slug} className="form-check-label"> <input className="form-check-input" type="checkbox"
{props.title} id={props.slug} name={props.slug}
{ props.tooltip? <Tooltip text={ props.tooltip } /> : null } checked={!!props.value}
</label> disabled={props.disabled}
</div> 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 = { 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) => {
<label htmlFor={props.slug}> return (
{props.title} <label htmlFor={props.slug}>
{ props.tooltip? <Tooltip text={ props.tooltip } /> : null } {props.title}
{ (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}`} Copy
className="icon-button copy"> <input type="checkbox" checked={props.copy}
Copy onChange={() => props.toggleCopyAttribute(props.slug)}/>
</NavLink> </label>
</div> : null </div> : null
} }
</label> { props.tooltip? <Tooltip text={ props.tooltip } /> : null }
); </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

View File

@ -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,46 +46,152 @@ BuildingView.propTypes = {
building_like: PropTypes.bool building_like: PropTypes.bool
} }
const DataSection = (props) => { class DataSection extends React.Component {
const match = props.cat === props.slug; constructor(props) {
return ( super(props);
<section id={props.slug} className={(props.inactive)? 'data-section inactive': 'data-section'}> this.state = {
<header className={`section-header view ${props.slug} ${(match? 'active' : '')}`}> copying: false,
<NavLink values_to_copy: {}
to={`/view/${props.slug}/building/${props.building_id}.html`} };
title={(props.inactive)? 'Coming soon… Click the ? for more info.' : this.toggleCopying = this.toggleCopying.bind(this);
(match)? 'Hide details' : 'Show details'} this.toggleCopyAttribute = this.toggleCopyAttribute.bind(this);
isActive={() => match}> }
<h3 className="h3">{props.title}</h3>
</NavLink> /**
<nav className="icon-buttons"> * Enter or exit "copying" state - allow user to select attributes to copy
{ */
props.help? toggleCopying() {
<a className="icon-button help" title="Find out more" href={props.help}> this.setState({
Info copying: !this.state.copying
</a> })
}
/**
* 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 : 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? !props.inactive?
<NavLink className="icon-button edit" title="Edit data" <dl className="data-list">
to={`/edit/${props.slug}/building/${props.building_id}.html`}> {
Edit props.fields.map(field => {
<EditIcon />
</NavLink> switch (field.type) {
: null case 'uprn_list':
} return <UPRNsDataEntry
</nav> key={field.slug}
</header> title={field.title}
{ value={props.uprns}
match? tooltip={field.tooltip} />
!props.inactive? case 'text_multi':
<dl className="data-list">{props.children}</dl> return <MultiDataEntry
: <p className="data-intro">{props.intro}</p> key={field.slug}
: null slug={field.slug}
} disabled={field.disabled}
</section> 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 = { DataSection.propTypes = {
@ -135,30 +205,32 @@ DataSection.propTypes = {
children: PropTypes.node children: PropTypes.node
} }
const DataEntry = (props) => ( const DataEntry = (props) => {
<Fragment> return (
<dt> <Fragment>
{ props.title } <dt>
{ props.tooltip? <Tooltip text={ props.tooltip } /> : null } { props.title }
{ (props.cat && props.slug && !props.disabled)? { props.tooltip? <Tooltip text={ props.tooltip } /> : null }
<div className="icon-buttons"> { (props.copying && props.cat && props.slug && !props.disabled)?
<NavLink <div className="icon-buttons">
to={`/multi-edit/${props.cat}.html?k=${props.slug}&v=${props.value}`} <label className="icon-button copy">
className="icon-button copy"> Copy
Copy <input type="checkbox" checked={props.copy}
</NavLink> onChange={() => props.toggleCopyAttribute(props.slug)}/>
</div> </label>
: null </div>
} : null
</dt> }
<dd>{ </dt>
(props.value != null && props.value !== '')? <dd>{
(typeof(props.value) === 'boolean')? (props.value != null && props.value !== '')?
(props.value)? 'Yes' : 'No' (typeof(props.value) === 'boolean')?
: props.value (props.value)? 'Yes' : 'No'
: '\u00A0'}</dd> : props.value
</Fragment> : '\u00A0'}</dd>
); </Fragment>
);
}
DataEntry.propTypes = { DataEntry.propTypes = {
title: PropTypes.string, title: PropTypes.string,
@ -169,33 +241,36 @@ DataEntry.propTypes = {
value: PropTypes.any value: PropTypes.any
} }
const LikeDataEntry = (props) => ( const LikeDataEntry = (props) => {
<Fragment> const data_string = JSON.stringify({like: true});
<dt> (
{ props.title } <Fragment>
{ props.tooltip? <Tooltip text={ props.tooltip } /> : null } <dt>
<div className="icon-buttons"> { props.title }
<NavLink { props.tooltip? <Tooltip text={ props.tooltip } /> : null }
to={`/multi-edit/${props.cat}.html?k=like&v=${true}`} <div className="icon-buttons">
className="icon-button copy"> <NavLink
Copy to={`/multi-edit/${props.cat}.html?data=${data_string}`}
</NavLink> className="icon-button copy">
</div> Copy
</dt> </NavLink>
<dd> </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.user_building_like)? <dd>&hellip;including you!</dd> : null
(props.value === 1)?
`${props.value} person likes this building`
: `${props.value} people like this building`
: '\u00A0'
} }
</dd> </Fragment>
{ );
(props.user_building_like)? <dd>&hellip;including you!</dd> : null }
}
</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);

View 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 }

View File

@ -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];

View File

@ -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;

View File

@ -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;

View File

@ -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 {