Merge pull request #281 from tomalrussell/feature/multi-select
Feature/multi edit
This commit is contained in:
commit
39a4fbcbbf
23
app/package-lock.json
generated
23
app/package-lock.json
generated
@ -4145,8 +4145,7 @@
|
|||||||
"decode-uri-component": {
|
"decode-uri-component": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz",
|
||||||
"integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=",
|
"integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"decompress-response": {
|
"decompress-response": {
|
||||||
"version": "3.3.0",
|
"version": "3.3.0",
|
||||||
@ -13005,6 +13004,16 @@
|
|||||||
"integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==",
|
"integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"query-string": {
|
||||||
|
"version": "6.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/query-string/-/query-string-6.5.0.tgz",
|
||||||
|
"integrity": "sha512-TYC4hDjZSvVxLMEucDMySkuAS9UIzSbAiYGyA9GWCjLKB8fQpviFbjd20fD7uejCDxZS+ftSdBKE6DS+xucJFg==",
|
||||||
|
"requires": {
|
||||||
|
"decode-uri-component": "^0.2.0",
|
||||||
|
"split-on-first": "^1.0.0",
|
||||||
|
"strict-uri-encode": "^2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"querystring": {
|
"querystring": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz",
|
||||||
@ -15168,6 +15177,11 @@
|
|||||||
"through": "2"
|
"through": "2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"split-on-first": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw=="
|
||||||
|
},
|
||||||
"split-string": {
|
"split-string": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz",
|
||||||
@ -15308,6 +15322,11 @@
|
|||||||
"integrity": "sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI=",
|
"integrity": "sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"strict-uri-encode": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz",
|
||||||
|
"integrity": "sha1-ucczDHBChi9rFC3CdLvMWGbONUY="
|
||||||
|
},
|
||||||
"string-length": {
|
"string-length": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/string-length/-/string-length-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/string-length/-/string-length-2.0.0.tgz",
|
||||||
|
@ -31,6 +31,7 @@
|
|||||||
"react-leaflet": "^1.0.1",
|
"react-leaflet": "^1.0.1",
|
||||||
"react-leaflet-universal": "^1.2.0",
|
"react-leaflet-universal": "^1.2.0",
|
||||||
"react-router-dom": "^4.3.1",
|
"react-router-dom": "^4.3.1",
|
||||||
|
"query-string": "^6.2.0",
|
||||||
"serialize-javascript": "^1.7.0",
|
"serialize-javascript": "^1.7.0",
|
||||||
"sharp": "^0.21.3"
|
"sharp": "^0.21.3"
|
||||||
},
|
},
|
||||||
|
@ -172,7 +172,7 @@ function likeBuilding(buildingId, userId) {
|
|||||||
// - insert changeset
|
// - insert changeset
|
||||||
// - update building to latest state
|
// - update building to latest state
|
||||||
// commit or rollback (serializable - could be more compact?)
|
// commit or rollback (serializable - could be more compact?)
|
||||||
return db.tx({ serializable }, t => {
|
return db.tx({mode: serializable}, t => {
|
||||||
return t.none(
|
return t.none(
|
||||||
'INSERT INTO building_user_likes ( building_id, user_id ) VALUES ($1, $2);',
|
'INSERT INTO building_user_likes ( building_id, user_id ) VALUES ($1, $2);',
|
||||||
[buildingId, userId]
|
[buildingId, userId]
|
||||||
@ -226,7 +226,7 @@ function unlikeBuilding(buildingId, userId) {
|
|||||||
// - insert changeset
|
// - insert changeset
|
||||||
// - update building to latest state
|
// - update building to latest state
|
||||||
// commit or rollback (serializable - could be more compact?)
|
// commit or rollback (serializable - could be more compact?)
|
||||||
return db.tx({ serializable }, t => {
|
return db.tx({mode: serializable}, t => {
|
||||||
return t.none(
|
return t.none(
|
||||||
'DELETE FROM building_user_likes WHERE building_id = $1 AND user_id = $2;',
|
'DELETE FROM building_user_likes WHERE building_id = $1 AND user_id = $2;',
|
||||||
[buildingId, userId]
|
[buildingId, userId]
|
||||||
|
@ -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';
|
||||||
@ -8,6 +9,7 @@ import './app.css';
|
|||||||
import AboutPage from './about';
|
import AboutPage from './about';
|
||||||
import BuildingEdit from './building-edit';
|
import BuildingEdit from './building-edit';
|
||||||
import BuildingView from './building-view';
|
import BuildingView from './building-view';
|
||||||
|
import MultiEdit from './multi-edit';
|
||||||
import ColouringMap from './map';
|
import ColouringMap from './map';
|
||||||
import Header from './header';
|
import Header from './header';
|
||||||
import Overview from './overview';
|
import Overview from './overview';
|
||||||
@ -15,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
|
||||||
@ -31,15 +34,20 @@ import Welcome from './welcome';
|
|||||||
class App extends React.Component {
|
class App extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
// set building revision id, default 0
|
||||||
|
const rev = (props.building)? +props.building.revision_id : 0;
|
||||||
this.state = {
|
this.state = {
|
||||||
user: props.user,
|
user: props.user,
|
||||||
building: props.building,
|
building: props.building,
|
||||||
building_like: props.building_like
|
building_like: props.building_like,
|
||||||
|
revision_id: rev
|
||||||
};
|
};
|
||||||
this.login = this.login.bind(this);
|
this.login = this.login.bind(this);
|
||||||
this.updateUser = this.updateUser.bind(this);
|
this.updateUser = this.updateUser.bind(this);
|
||||||
this.logout = this.logout.bind(this);
|
this.logout = this.logout.bind(this);
|
||||||
this.selectBuilding = this.selectBuilding.bind(this);
|
this.selectBuilding = this.selectBuilding.bind(this);
|
||||||
|
this.colourBuilding = this.colourBuilding.bind(this);
|
||||||
|
this.increaseRevision = this.increaseRevision.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
login(user) {
|
login(user) {
|
||||||
@ -58,7 +66,16 @@ class App extends React.Component {
|
|||||||
this.setState({user: undefined});
|
this.setState({user: undefined});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
increaseRevision(revisionId) {
|
||||||
|
revisionId = +revisionId;
|
||||||
|
// bump revision id, only ever increasing
|
||||||
|
if (revisionId > this.state.revision_id){
|
||||||
|
this.setState({revision_id: revisionId})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
selectBuilding(building) {
|
selectBuilding(building) {
|
||||||
|
this.increaseRevision(building.revision_id);
|
||||||
// get UPRNs and update
|
// get UPRNs and update
|
||||||
fetch(`/building/${building.building_id}/uprns.json`, {
|
fetch(`/building/${building.building_id}/uprns.json`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
@ -101,6 +118,62 @@ class App extends React.Component {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
colourBuilding(building) {
|
||||||
|
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(buildingId) {
|
||||||
|
fetch(`/building/${buildingId}/like.json`, {
|
||||||
|
method: 'POST',
|
||||||
|
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(buildingId, data){
|
||||||
|
fetch(`/building/${buildingId}.json`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
headers:{
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
credentials: 'same-origin'
|
||||||
|
}).then(
|
||||||
|
res => res.json()
|
||||||
|
).then(res => {
|
||||||
|
if (res.error) {
|
||||||
|
console.error({error: res.error})
|
||||||
|
} else {
|
||||||
|
this.increaseRevision(res.revision_id);
|
||||||
|
}
|
||||||
|
}).catch(
|
||||||
|
(err) => console.error({error: err})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
@ -122,6 +195,12 @@ class App extends React.Component {
|
|||||||
mode='edit' user={this.state.user}
|
mode='edit' user={this.state.user}
|
||||||
/>
|
/>
|
||||||
) } />
|
) } />
|
||||||
|
<Route exact path="/multi-edit/:cat.html" render={(props) => (
|
||||||
|
<MultiEdit
|
||||||
|
{...props}
|
||||||
|
user={this.state.user}
|
||||||
|
/>
|
||||||
|
) } />
|
||||||
<Route exact path="/view/:cat/building/:building.html" render={(props) => (
|
<Route exact path="/view/:cat/building/:building.html" render={(props) => (
|
||||||
<BuildingView
|
<BuildingView
|
||||||
{...props}
|
{...props}
|
||||||
@ -141,11 +220,13 @@ class App extends React.Component {
|
|||||||
) } />
|
) } />
|
||||||
</Switch>
|
</Switch>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route exact path="/(edit.*|view.*)?" render={(props) => (
|
<Route exact path="/(multi-edit.*|edit.*|view.*)?" render={(props) => (
|
||||||
<ColouringMap
|
<ColouringMap
|
||||||
{...props}
|
{...props}
|
||||||
building={this.state.building}
|
building={this.state.building}
|
||||||
|
revision_id={this.state.revision_id}
|
||||||
selectBuilding={this.selectBuilding}
|
selectBuilding={this.selectBuilding}
|
||||||
|
colourBuilding={this.colourBuilding}
|
||||||
/>
|
/>
|
||||||
) } />
|
) } />
|
||||||
<Route exact path="/about.html" component={AboutPage} />
|
<Route exact path="/about.html" component={AboutPage} />
|
||||||
|
@ -7,7 +7,6 @@ import InfoBox from './info-box';
|
|||||||
import Sidebar from './sidebar';
|
import Sidebar from './sidebar';
|
||||||
import Tooltip from './tooltip';
|
import Tooltip from './tooltip';
|
||||||
import { SaveIcon } from './icons';
|
import { SaveIcon } from './icons';
|
||||||
import { parseCategoryURL } from '../parse';
|
|
||||||
|
|
||||||
import CONFIG from './fields-config.json';
|
import CONFIG from './fields-config.json';
|
||||||
|
|
||||||
@ -15,12 +14,12 @@ const BuildingEdit = (props) => {
|
|||||||
if (!props.user){
|
if (!props.user){
|
||||||
return <Redirect to="/sign-up.html" />
|
return <Redirect to="/sign-up.html" />
|
||||||
}
|
}
|
||||||
const cat = parseCategoryURL(props.match.url);
|
const cat = props.match.params.cat;
|
||||||
if (!props.building_id){
|
if (!props.building_id){
|
||||||
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>
|
||||||
@ -45,7 +44,7 @@ const BuildingEdit = (props) => {
|
|||||||
BuildingEdit.propTypes = {
|
BuildingEdit.propTypes = {
|
||||||
user: PropTypes.object,
|
user: PropTypes.object,
|
||||||
match: PropTypes.object,
|
match: PropTypes.object,
|
||||||
building_id: PropTypes.string,
|
building_id: PropTypes.number
|
||||||
}
|
}
|
||||||
|
|
||||||
class EditForm extends Component {
|
class EditForm extends Component {
|
||||||
@ -173,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' : '')}`}>
|
||||||
@ -220,36 +221,35 @@ 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}
|
||||||
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
|
(this.props.slug === 'like')? // special-case for likes
|
||||||
null :
|
null :
|
||||||
@ -258,7 +258,9 @@ class EditForm extends Component {
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</form>
|
</form>
|
||||||
: <form><InfoBox msg={`We're not collection data on ${this.props.title.toLowerCase()} yet - check back soon.`} /></form>
|
: <form>
|
||||||
|
<InfoBox msg={`We're not collection data on ${this.props.title.toLowerCase()} yet - check back soon.`} />
|
||||||
|
</form>
|
||||||
) : null
|
) : null
|
||||||
}
|
}
|
||||||
</section>
|
</section>
|
||||||
@ -275,14 +277,17 @@ EditForm.propTypes = {
|
|||||||
like: PropTypes.bool,
|
like: PropTypes.bool,
|
||||||
building_like: PropTypes.bool,
|
building_like: PropTypes.bool,
|
||||||
selectBuilding: PropTypes.func,
|
selectBuilding: PropTypes.func,
|
||||||
building_id: PropTypes.string,
|
building_id: PropTypes.number,
|
||||||
inactive: PropTypes.bool,
|
inactive: PropTypes.bool,
|
||||||
fields: PropTypes.array
|
fields: PropTypes.array
|
||||||
}
|
}
|
||||||
|
|
||||||
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}
|
||||||
|
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 || ''}
|
||||||
@ -296,6 +301,7 @@ const TextInput = (props) => (
|
|||||||
|
|
||||||
TextInput.propTypes = {
|
TextInput.propTypes = {
|
||||||
slug: PropTypes.string,
|
slug: PropTypes.string,
|
||||||
|
cat: PropTypes.string,
|
||||||
title: PropTypes.string,
|
title: PropTypes.string,
|
||||||
tooltip: PropTypes.string,
|
tooltip: PropTypes.string,
|
||||||
value: PropTypes.string,
|
value: PropTypes.string,
|
||||||
@ -307,7 +313,8 @@ TextInput.propTypes = {
|
|||||||
|
|
||||||
const LongTextInput = (props) => (
|
const LongTextInput = (props) => (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<Label slug={props.slug} title={props.title} tooltip={props.tooltip} />
|
<Label slug={props.slug} title={props.title} cat={props.cat}
|
||||||
|
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}
|
||||||
disabled={props.disabled}
|
disabled={props.disabled}
|
||||||
@ -407,7 +414,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} disabled={props.disabled}
|
||||||
|
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 || ''}
|
||||||
@ -426,6 +436,7 @@ const TextListInput = (props) => (
|
|||||||
|
|
||||||
TextListInput.propTypes = {
|
TextListInput.propTypes = {
|
||||||
slug: PropTypes.string,
|
slug: PropTypes.string,
|
||||||
|
cat: PropTypes.string,
|
||||||
title: PropTypes.string,
|
title: PropTypes.string,
|
||||||
tooltip: PropTypes.string,
|
tooltip: PropTypes.string,
|
||||||
options: PropTypes.arrayOf(PropTypes.string),
|
options: PropTypes.arrayOf(PropTypes.string),
|
||||||
@ -436,7 +447,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} disabled={props.disabled}
|
||||||
|
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 || ''}
|
||||||
@ -448,6 +462,7 @@ const NumberInput = (props) => (
|
|||||||
|
|
||||||
NumberInput.propTypes = {
|
NumberInput.propTypes = {
|
||||||
slug: PropTypes.string,
|
slug: PropTypes.string,
|
||||||
|
cat: PropTypes.string,
|
||||||
title: PropTypes.string,
|
title: PropTypes.string,
|
||||||
tooltip: PropTypes.string,
|
tooltip: PropTypes.string,
|
||||||
step: PropTypes.number,
|
step: PropTypes.number,
|
||||||
@ -529,11 +544,18 @@ const LikeButton = (props) => (
|
|||||||
{ props.tooltip? <Tooltip text={ props.tooltip } /> : null }
|
{ props.tooltip? <Tooltip text={ props.tooltip } /> : null }
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
<p>
|
||||||
|
<NavLink
|
||||||
|
to={`/multi-edit/${props.cat}.html?k=like&v=${true}`}>
|
||||||
|
Like more buildings
|
||||||
|
</NavLink>
|
||||||
|
</p>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
|
|
||||||
LikeButton.propTypes = {
|
LikeButton.propTypes = {
|
||||||
slug: PropTypes.string,
|
slug: PropTypes.string,
|
||||||
|
cat: PropTypes.string,
|
||||||
title: PropTypes.string,
|
title: PropTypes.string,
|
||||||
tooltip: PropTypes.string,
|
tooltip: PropTypes.string,
|
||||||
value: PropTypes.number,
|
value: PropTypes.number,
|
||||||
@ -546,12 +568,24 @@ 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.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>
|
</label>
|
||||||
);
|
);
|
||||||
|
|
||||||
Label.propTypes = {
|
Label.propTypes = {
|
||||||
slug: PropTypes.string,
|
slug: PropTypes.string,
|
||||||
|
cat: PropTypes.string,
|
||||||
|
value: PropTypes.any,
|
||||||
title: PropTypes.string,
|
title: PropTypes.string,
|
||||||
|
disabled: PropTypes.bool,
|
||||||
tooltip: PropTypes.string
|
tooltip: PropTypes.string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,7 +7,6 @@ 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 { parseCategoryURL } from '../parse';
|
|
||||||
|
|
||||||
import CONFIG from './fields-config.json';
|
import CONFIG from './fields-config.json';
|
||||||
|
|
||||||
@ -22,7 +21,7 @@ const BuildingView = (props) => {
|
|||||||
</Sidebar>
|
</Sidebar>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const cat = parseCategoryURL(props.match.url);
|
const cat = props.match.params.cat;
|
||||||
return (
|
return (
|
||||||
<Sidebar title={'Data available for this building'} back={`/view/${cat}.html`}>
|
<Sidebar title={'Data available for this building'} back={`/view/${cat}.html`}>
|
||||||
{
|
{
|
||||||
@ -57,6 +56,9 @@ const BuildingView = (props) => {
|
|||||||
default:
|
default:
|
||||||
return <DataEntry
|
return <DataEntry
|
||||||
key={field.slug}
|
key={field.slug}
|
||||||
|
slug={field.slug}
|
||||||
|
disabled={field.disabled}
|
||||||
|
cat={cat}
|
||||||
title={field.title}
|
title={field.title}
|
||||||
value={props[field.slug]}
|
value={props[field.slug]}
|
||||||
tooltip={field.tooltip} />
|
tooltip={field.tooltip} />
|
||||||
@ -71,7 +73,7 @@ const BuildingView = (props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
BuildingView.propTypes = {
|
BuildingView.propTypes = {
|
||||||
building_id: PropTypes.string,
|
building_id: PropTypes.number,
|
||||||
match: PropTypes.object,
|
match: PropTypes.object,
|
||||||
uprns: PropTypes.arrayOf(PropTypes.shape({
|
uprns: PropTypes.arrayOf(PropTypes.shape({
|
||||||
uprn: PropTypes.string.isRequired,
|
uprn: PropTypes.string.isRequired,
|
||||||
@ -129,7 +131,7 @@ DataSection.propTypes = {
|
|||||||
intro: PropTypes.string,
|
intro: PropTypes.string,
|
||||||
help: PropTypes.string,
|
help: PropTypes.string,
|
||||||
inactive: PropTypes.bool,
|
inactive: PropTypes.bool,
|
||||||
building_id: PropTypes.string,
|
building_id: PropTypes.number,
|
||||||
children: PropTypes.node
|
children: PropTypes.node
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -138,6 +140,16 @@ 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.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>
|
</dt>
|
||||||
<dd>{
|
<dd>{
|
||||||
(props.value != null && props.value !== '')?
|
(props.value != null && props.value !== '')?
|
||||||
@ -150,7 +162,10 @@ const DataEntry = (props) => (
|
|||||||
|
|
||||||
DataEntry.propTypes = {
|
DataEntry.propTypes = {
|
||||||
title: PropTypes.string,
|
title: PropTypes.string,
|
||||||
|
cat: PropTypes.string,
|
||||||
|
slug: PropTypes.string,
|
||||||
tooltip: PropTypes.string,
|
tooltip: PropTypes.string,
|
||||||
|
disabled: PropTypes.bool,
|
||||||
value: PropTypes.any
|
value: PropTypes.any
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -159,6 +174,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>
|
||||||
{
|
{
|
||||||
@ -177,6 +199,7 @@ const LikeDataEntry = (props) => (
|
|||||||
|
|
||||||
LikeDataEntry.propTypes = {
|
LikeDataEntry.propTypes = {
|
||||||
title: PropTypes.string,
|
title: PropTypes.string,
|
||||||
|
cat: PropTypes.string,
|
||||||
tooltip: PropTypes.string,
|
tooltip: PropTypes.string,
|
||||||
value: PropTypes.any,
|
value: PropTypes.any,
|
||||||
user_building_like: PropTypes.bool
|
user_building_like: PropTypes.bool
|
||||||
|
@ -20,3 +20,6 @@
|
|||||||
border: 1px solid #fff;
|
border: 1px solid #fff;
|
||||||
box-shadow: 0 0 1px 1px #222;
|
box-shadow: 0 0 1px 1px #222;
|
||||||
}
|
}
|
||||||
|
.leaflet-grab {
|
||||||
|
cursor: crosshair;
|
||||||
|
}
|
||||||
|
@ -29,6 +29,7 @@ class ColouringMap extends Component {
|
|||||||
this.handleClick = this.handleClick.bind(this);
|
this.handleClick = this.handleClick.bind(this);
|
||||||
this.handleLocate = this.handleLocate.bind(this);
|
this.handleLocate = this.handleLocate.bind(this);
|
||||||
this.themeSwitch = this.themeSwitch.bind(this);
|
this.themeSwitch = this.themeSwitch.bind(this);
|
||||||
|
this.getMode = this.getMode.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleLocate(lat, lng, zoom){
|
handleLocate(lat, lng, zoom){
|
||||||
@ -39,9 +40,14 @@ class ColouringMap extends Component {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
handleClick(e) {
|
getMode() {
|
||||||
const isEdit = this.props.match.url.match('edit')
|
const isEdit = this.props.match.url.match('edit')
|
||||||
const mode = isEdit? 'edit': 'view';
|
const isMulti = this.props.match.url.match('multi')
|
||||||
|
return isEdit? (isMulti? 'multi' : 'edit') : 'view';
|
||||||
|
}
|
||||||
|
|
||||||
|
handleClick(e) {
|
||||||
|
const mode = this.getMode()
|
||||||
const lat = e.latlng.lat
|
const lat = e.latlng.lat
|
||||||
const lng = e.latlng.lng
|
const lng = e.latlng.lng
|
||||||
const newCat = parseCategoryURL(this.props.match.url);
|
const newCat = parseCategoryURL(this.props.match.url);
|
||||||
@ -53,8 +59,13 @@ class ColouringMap extends Component {
|
|||||||
).then(function(data){
|
).then(function(data){
|
||||||
if (data && data.length){
|
if (data && data.length){
|
||||||
const building = data[0];
|
const building = data[0];
|
||||||
|
if (mode === 'multi') {
|
||||||
|
// colour building directly
|
||||||
|
this.props.colourBuilding(building);
|
||||||
|
} else {
|
||||||
this.props.selectBuilding(building);
|
this.props.selectBuilding(building);
|
||||||
this.props.history.push(`/${mode}/${mapCat}/building/${building.building_id}.html`);
|
this.props.history.push(`/${mode}/${mapCat}/building/${building.building_id}.html`);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// deselect but keep/return to expected colour theme
|
// deselect but keep/return to expected colour theme
|
||||||
this.props.selectBuilding(undefined);
|
this.props.selectBuilding(undefined);
|
||||||
@ -94,7 +105,7 @@ class ColouringMap extends Component {
|
|||||||
}
|
}
|
||||||
const tileset = tilesetByCat[cat];
|
const tileset = tilesetByCat[cat];
|
||||||
// pick revision id to bust browser cache
|
// pick revision id to bust browser cache
|
||||||
const rev = this.props.building? this.props.building.revision_id : '';
|
const rev = this.props.revision_id;
|
||||||
const dataLayer = tileset?
|
const dataLayer = tileset?
|
||||||
<TileLayer
|
<TileLayer
|
||||||
key={tileset}
|
key={tileset}
|
||||||
@ -158,7 +169,9 @@ class ColouringMap extends Component {
|
|||||||
|
|
||||||
ColouringMap.propTypes = {
|
ColouringMap.propTypes = {
|
||||||
building: PropTypes.object,
|
building: PropTypes.object,
|
||||||
|
revision_id: PropTypes.number,
|
||||||
selectBuilding: PropTypes.func,
|
selectBuilding: PropTypes.func,
|
||||||
|
colourBuilding: PropTypes.func,
|
||||||
match: PropTypes.object,
|
match: PropTypes.object,
|
||||||
history: PropTypes.object
|
history: PropTypes.object
|
||||||
}
|
}
|
||||||
|
93
app/src/frontend/multi-edit.js
Normal file
93
app/src/frontend/multi-edit.js
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Link, Redirect } from 'react-router-dom';
|
||||||
|
import { parse } from 'query-string';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import Sidebar from './sidebar';
|
||||||
|
import CONFIG from './fields-config.json';
|
||||||
|
import InfoBox from './info-box';
|
||||||
|
|
||||||
|
const MultiEdit = (props) => {
|
||||||
|
if (!props.user){
|
||||||
|
return <Redirect to="/sign-up.html" />
|
||||||
|
}
|
||||||
|
const cat = props.match.params.cat;
|
||||||
|
if (cat === 'like') {
|
||||||
|
// special case for likes
|
||||||
|
return (
|
||||||
|
<Sidebar
|
||||||
|
title='Quick edit'
|
||||||
|
back={`/edit/${cat}.html`}>
|
||||||
|
<section className='data-section'>
|
||||||
|
<header className={`section-header view ${cat} active`}>
|
||||||
|
<a><h3 className="h3">Like me!</h3></a>
|
||||||
|
</header>
|
||||||
|
<form className='buttons-container'>
|
||||||
|
<InfoBox msg='Click all the buildings that you like and think contribute to the city!' />
|
||||||
|
|
||||||
|
<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>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
</Sidebar>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const q = parse(props.location.search);
|
||||||
|
const label = fieldTitleFromSlug(q.k);
|
||||||
|
const title = sectionTitleFromCat(cat);
|
||||||
|
return (
|
||||||
|
<Sidebar
|
||||||
|
title='Quick edit'
|
||||||
|
back={`/edit/${cat}.html`}>
|
||||||
|
<section className='data-section'>
|
||||||
|
<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>
|
||||||
|
<form className='buttons-container'>
|
||||||
|
<InfoBox msg='Click buildings to colour' />
|
||||||
|
|
||||||
|
<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>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
</Sidebar>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
MultiEdit.propTypes = {
|
||||||
|
user: PropTypes.object,
|
||||||
|
match: PropTypes.object,
|
||||||
|
location: PropTypes.object
|
||||||
|
}
|
||||||
|
|
||||||
|
function sectionTitleFromCat(cat) {
|
||||||
|
for (let index = 0; index < CONFIG.length; index++) {
|
||||||
|
const section = CONFIG[index];
|
||||||
|
if (section.slug === cat) {
|
||||||
|
return section.title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
function fieldTitleFromSlug(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;
|
@ -114,7 +114,7 @@ MyAccountPage.propTypes = {
|
|||||||
user: PropTypes.shape({
|
user: PropTypes.shape({
|
||||||
username: PropTypes.string,
|
username: PropTypes.string,
|
||||||
email: PropTypes.string,
|
email: PropTypes.string,
|
||||||
registered: PropTypes.string,
|
registered: PropTypes.date,
|
||||||
api_key: PropTypes.string,
|
api_key: PropTypes.string,
|
||||||
error: PropTypes.object
|
error: PropTypes.object
|
||||||
}),
|
}),
|
||||||
|
@ -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,
|
||||||
@ -334,6 +339,7 @@
|
|||||||
}
|
}
|
||||||
.data-section p {
|
.data-section p {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
|
margin: 0.5rem 0;
|
||||||
}
|
}
|
||||||
.data-section ul {
|
.data-section ul {
|
||||||
padding-left: 3.333rem;
|
padding-left: 3.333rem;
|
||||||
|
@ -54,11 +54,3 @@ form .btn {
|
|||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Like button */
|
|
||||||
.likes {
|
|
||||||
margin: 0.5rem 0;
|
|
||||||
}
|
|
||||||
form .btn-like {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
@ -43,7 +43,7 @@ function parseCategoryURL(url) {
|
|||||||
if (url === '/') {
|
if (url === '/') {
|
||||||
return defaultCat;
|
return defaultCat;
|
||||||
}
|
}
|
||||||
const matches = /^\/(view|edit)\/([^/.]+)/.exec(url);
|
const matches = /^\/(view|edit|multi-edit)\/([^/.]+)/.exec(url);
|
||||||
const cat = (matches && matches.length >= 3) ? matches[2] : defaultCat;
|
const cat = (matches && matches.length >= 3) ? matches[2] : defaultCat;
|
||||||
return cat;
|
return cat;
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user