Click 'Copy' to move to quick/multi edit

- works with single values
- leans on server-side validation
- special case for likes (like +1 only)
- positioning of 'Copy' link not quite right against 'Hint'
- puts like/update fetch call in App component
This commit is contained in:
Tom Russell 2019-05-10 16:10:16 +01:00
parent c03f716a28
commit 426c7ff9f6
5 changed files with 195 additions and 68 deletions

View File

@ -1,6 +1,7 @@
import React, { Fragment } from 'react'; import React, { Fragment } from 'react';
import { Route, Switch, Link } from 'react-router-dom'; import { Route, Switch, Link } from 'react-router-dom';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { parse } from 'query-string';
import '../../node_modules/bootstrap/dist/css/bootstrap.min.css'; import '../../node_modules/bootstrap/dist/css/bootstrap.min.css';
import './app.css'; import './app.css';
@ -16,6 +17,7 @@ import Login from './login';
import MyAccountPage from './my-account'; import MyAccountPage from './my-account';
import SignUp from './signup'; import SignUp from './signup';
import Welcome from './welcome'; import Welcome from './welcome';
import { parseCategoryURL } from '../parse';
/** /**
* App component * App component
@ -116,9 +118,44 @@ class App extends React.Component {
} }
colourBuilding(building) { colourBuilding(building) {
fetch(`/building/${building.building_id}.json`, { const cat = parseCategoryURL(window.location.pathname);
const q = parse(window.location.search);
let 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)
}
}
likeBuilding(building_id) {
fetch(`/building/${building_id}/like.json`, {
method: 'POST', method: 'POST',
body: JSON.stringify({date_year: 1999}), // TODO link to multi/pass in data headers:{
'Content-Type': 'application/json'
},
credentials: 'same-origin',
body: JSON.stringify({like: true})
}).then(
res => res.json()
).then(function(res){
if (res.error) {
console.error({error: res.error})
} else {
this.increaseRevision(res.revision_id);
}
}.bind(this)).catch(
(err) => console.error({error: err})
);
}
updateBuilding(building_id, data){
fetch(`/building/${building_id}.json`, {
method: 'POST',
body: JSON.stringify(data),
headers:{ headers:{
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}, },
@ -129,7 +166,6 @@ class App extends React.Component {
if (res.error) { if (res.error) {
console.error({error: res.error}) console.error({error: res.error})
} else { } else {
console.log(res);
this.increaseRevision(res.revision_id); this.increaseRevision(res.revision_id);
} }
}).catch( }).catch(

View File

@ -19,7 +19,7 @@ const BuildingEdit = (props) => {
return ( return (
<Sidebar title="Building Not Found" back={`/edit/${cat}.html`}> <Sidebar title="Building Not Found" back={`/edit/${cat}.html`}>
<InfoBox msg="We can't find that one anywhere - try the map again?" /> <InfoBox msg="We can't find that one anywhere - try the map again?" />
<div className="buttons-container"> <div className="buttons-container ml-3 mr-3">
<Link to={`/edit/${cat}.html`} className="btn btn-secondary">Back to maps</Link> <Link to={`/edit/${cat}.html`} className="btn btn-secondary">Back to maps</Link>
</div> </div>
</Sidebar> </Sidebar>
@ -172,7 +172,9 @@ class EditForm extends Component {
render() { render() {
const match = this.props.cat === this.props.slug; const match = this.props.cat === this.props.slug;
const cat = this.props.cat;
const buildingLike = this.props.building_like; const buildingLike = this.props.building_like;
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' : '')}`}>
@ -204,61 +206,62 @@ class EditForm extends Component {
</nav> </nav>
</header> </header>
{ {
match? ( match? (
!this.props.inactive? !this.props.inactive?
<form action={`/edit/${this.props.slug}/building/${this.props.building_id}.html`} <form action={`/edit/${this.props.slug}/building/${this.props.building_id}.html`}
method="GET" onSubmit={this.handleSubmit}> method="GET" onSubmit={this.handleSubmit}>
{ {
this.props.slug === 'location'? this.props.slug === 'location'?
<InfoBox msg="Text-based address fields are disabled at the moment. We're looking into how best to collect this data." /> <InfoBox msg="Text-based address fields are disabled at the moment. We're looking into how best to collect this data." />
: null : null
} }
<ErrorBox msg={this.state.error} /> <ErrorBox msg={this.state.error} />
{ {
this.props.fields.map((props) => { this.props.fields.map((props) => {
switch (props.type) { switch (props.type) {
case 'text': case "text":
return <TextInput {...props} handleChange={this.handleChange} return <TextInput {...props} handleChange={this.handleChange}
value={this.state[props.slug]} key={props.slug} /> 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}
value={this.state[props.slug]} key={props.slug} /> 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}
value={this.state[props.slug]} key={props.slug} /> 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}
value={this.state[props.slug]} key={props.slug} /> 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}
value={this.state[props.slug]} key={props.slug} /> 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}
value={this.state[props.slug]} key={props.slug} /> 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}
value={this.state[props.slug]} key={props.slug} /> 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}
building_like={buildingLike} building_like={buildingLike}
value={this.state[props.slug]} key={props.slug} /> value={this.state[props.slug]} key={props.slug} cat={cat} />
default: default:
return null return null
}
})
} }
<InfoBox msg="Colouring may take a few seconds - try zooming the map or hitting refresh after saving (we're working on making this smoother)." /> })
}
{ <InfoBox msg="Colouring may take a few seconds - try zooming the map or hitting refresh after saving (we're working on making this smoother)." />
(this.props.slug === 'like')? // special-case for likes {
null : (this.props.slug === 'like')? // special-case for likes
<div className="buttons-container"> null :
<button type="submit" className="btn btn-primary">Save</button> <div className="buttons-container">
</div> <button type="submit" className="btn btn-primary">Save</button>
} </div>
</form> }
: <form><InfoBox msg={`We're not collection data on ${this.props.title.toLowerCase()} yet - check back soon.`} /></form> </form>
) : null : <form>
<InfoBox msg={`We're not collection data on ${this.props.title.toLowerCase()} yet - check back soon.`} />
</form>
) : null
} }
</section> </section>
) )
@ -281,7 +284,10 @@ 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}
value={props.value || ""}
/>
<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 || ''}
@ -406,7 +412,10 @@ MultiTextInput.propTypes = {
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}
value={props.value || ""}
/>
<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 || ''}
@ -435,7 +444,10 @@ TextListInput.propTypes = {
const NumberInput = (props) => ( 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}
value={props.value || ""}
/>
<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 || ''}
@ -515,6 +527,10 @@ CheckboxInput.propTypes = {
const LikeButton = (props) => ( const LikeButton = (props) => (
<Fragment> <Fragment>
<NavLink
to={`/multi-edit/${props.cat}.html?k=like&v=${true}`}>
Like more buildings
</NavLink>
<p className="likes">{(props.value)? props.value : 0} likes</p> <p className="likes">{(props.value)? props.value : 0} likes</p>
<div className="form-check"> <div className="form-check">
<input className="form-check-input" type="checkbox" <input className="form-check-input" type="checkbox"
@ -545,6 +561,15 @@ const Label = (props) => (
<label htmlFor={props.slug}> <label htmlFor={props.slug}>
{props.title} {props.title}
{ props.tooltip? <Tooltip text={ props.tooltip } /> : null } { props.tooltip? <Tooltip text={ props.tooltip } /> : null }
{ (props.cat && props.slug && props.value)?
<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> </label>
); );

View File

@ -56,6 +56,8 @@ const BuildingView = (props) => {
default: default:
return <DataEntry return <DataEntry
key={field.slug} key={field.slug}
slug={field_props.slug}
cat={cat}
title={field.title} title={field.title}
value={props[field.slug]} value={props[field.slug]}
tooltip={field.tooltip} /> tooltip={field.tooltip} />
@ -137,6 +139,15 @@ const DataEntry = (props) => (
<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.value)?
<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> </dt>
<dd>{ <dd>{
(props.value != null && props.value !== '')? (props.value != null && props.value !== '')?
@ -158,6 +169,13 @@ const LikeDataEntry = (props) => (
<dt> <dt>
{ props.title } { props.title }
{ props.tooltip? <Tooltip text={ props.tooltip } /> : null } { 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> </dt>
<dd> <dd>
{ {

View File

@ -1,25 +1,68 @@
import React from 'react'; import React from 'react';
import { Redirect } from 'react-router-dom'; import { Link, Redirect } from 'react-router-dom';
import { parse } from 'query-string';
import Sidebar from './sidebar'; import Sidebar from './sidebar';
import CONFIG from './fields-config.json';
const MultiEdit = (props) => { const MultiEdit = (props) => {
if (!props.user){ if (!props.user){
return <Redirect to="/sign-up.html" /> return <Redirect to="/sign-up.html" />
} }
const cat = props.match.params.cat; const cat = props.match.params.cat;
if (cat === 'like') {
// special case for likes
return (
<Sidebar
title={`Quick like`}
back={`/edit/${cat}.html`}>
<section className="data-section">
<p className="data-intro">Click all the buildings that you like and think contribute to the city!</p>
<div className="buttons-container ml-3 mr-3">
<Link to={`/view/like.html`} className="btn btn-secondary">Back to view</Link>
<Link to={`/edit/like.html`} className="btn btn-secondary">Back to edit</Link>
</div>
</section>
</Sidebar>
);
}
const q = parse(props.location.search);
const label = field_title_from_slug(q.k);
return ( return (
<Sidebar <Sidebar
title={`Quick edit`} title={`Quick edit`}
back={`/edit/${cat}.html`}> back={`/edit/${cat}.html`}>
<section className="data-section"> <section className="data-section">
<p className="data-intro">Click a building to colour</p> <p className="data-intro">Click a building to colour</p>
<p className="data-intro">Set <strong>Year built</strong> to <strong>1999</strong></p> <p className="data-intro">Set <strong>{label}</strong> to <strong>{q.v}</strong></p>
<div className="buttons-container ml-3">
<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>
</div>
</section> </section>
</Sidebar> </Sidebar>
); );
} }
function field_title_from_slug(slug) {
const fields = CONFIG.reduce(
(prev, section) => {
const el = prev.concat(
section.fields.filter(
field => field.slug === slug
)
)
return el
}, []
)
if (fields.length === 1 && fields[0].title) {
return fields[0].title
} else {
console.error('Expected single match, got', fields)
}
}
export default MultiEdit; export default MultiEdit;

View File

@ -268,10 +268,10 @@
color: #222; color: #222;
margin-top: 2px; margin-top: 2px;
width: 1.8rem; width: 30px;
height: 1.8rem; height: 30px;
padding: 0.4rem; padding: 6px;
border-radius: 0.9rem; border-radius: 15px;
margin: 0 0.05rem; margin: 0 0.05rem;
display: inline-block; display: inline-block;
vertical-align: middle; vertical-align: middle;
@ -279,7 +279,7 @@
.svg-inline--fa.fa-w-11, .svg-inline--fa.fa-w-11,
.svg-inline--fa.fa-w-16, .svg-inline--fa.fa-w-16,
.svg-inline--fa.fa-w-8 { .svg-inline--fa.fa-w-8 {
width: 1.8rem; width: 30px;
} }
.icon-button:hover svg { .icon-button:hover svg {
background-color: #fff; background-color: #fff;
@ -291,6 +291,7 @@
.section-header .icon-button.help { .section-header .icon-button.help {
margin-top: 2px; margin-top: 2px;
} }
.icon-button.copy:hover,
.icon-button.help:hover { .icon-button.help:hover {
color: rgb(0, 81, 255) color: rgb(0, 81, 255)
} }
@ -311,6 +312,10 @@
float: right; float: right;
margin-top: -2px; margin-top: -2px;
} }
label .icon-buttons,
.data-list dt .icon-buttons {
float: right;
}
/* Back button */ /* Back button */
.icon-button.back, .icon-button.back,