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:
parent
c03f716a28
commit
426c7ff9f6
@ -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(
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -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>
|
||||||
{
|
{
|
||||||
|
@ -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;
|
||||||
|
@ -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,
|
||||||
|
Loading…
Reference in New Issue
Block a user