diff --git a/app/package-lock.json b/app/package-lock.json index 08fbf6d2..f234dcc9 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -4145,8 +4145,7 @@ "decode-uri-component": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", - "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", - "dev": true + "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=" }, "decompress-response": { "version": "3.3.0", @@ -13005,6 +13004,16 @@ "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", "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": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", @@ -15168,6 +15177,11 @@ "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": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", @@ -15308,6 +15322,11 @@ "integrity": "sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI=", "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": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/string-length/-/string-length-2.0.0.tgz", diff --git a/app/package.json b/app/package.json index 56533ff4..5ead9da9 100644 --- a/app/package.json +++ b/app/package.json @@ -31,6 +31,7 @@ "react-leaflet": "^1.0.1", "react-leaflet-universal": "^1.2.0", "react-router-dom": "^4.3.1", + "query-string": "^6.2.0", "serialize-javascript": "^1.7.0", "sharp": "^0.21.3" }, diff --git a/app/src/api/building.js b/app/src/api/building.js index 8713fa9c..a2569a92 100644 --- a/app/src/api/building.js +++ b/app/src/api/building.js @@ -172,7 +172,7 @@ function likeBuilding(buildingId, userId) { // - insert changeset // - update building to latest state // commit or rollback (serializable - could be more compact?) - return db.tx({ serializable }, t => { + return db.tx({mode: serializable}, t => { return t.none( 'INSERT INTO building_user_likes ( building_id, user_id ) VALUES ($1, $2);', [buildingId, userId] @@ -226,7 +226,7 @@ function unlikeBuilding(buildingId, userId) { // - insert changeset // - update building to latest state // commit or rollback (serializable - could be more compact?) - return db.tx({ serializable }, t => { + return db.tx({mode: serializable}, t => { return t.none( 'DELETE FROM building_user_likes WHERE building_id = $1 AND user_id = $2;', [buildingId, userId] diff --git a/app/src/frontend/app.js b/app/src/frontend/app.js index 2581d1c7..857ef73f 100644 --- a/app/src/frontend/app.js +++ b/app/src/frontend/app.js @@ -1,6 +1,7 @@ import React, { Fragment } from 'react'; import { Route, Switch, Link } from 'react-router-dom'; import PropTypes from 'prop-types'; +import { parse } from 'query-string'; import '../../node_modules/bootstrap/dist/css/bootstrap.min.css'; import './app.css'; @@ -8,6 +9,7 @@ import './app.css'; import AboutPage from './about'; import BuildingEdit from './building-edit'; import BuildingView from './building-view'; +import MultiEdit from './multi-edit'; import ColouringMap from './map'; import Header from './header'; import Overview from './overview'; @@ -15,6 +17,7 @@ import Login from './login'; import MyAccountPage from './my-account'; import SignUp from './signup'; import Welcome from './welcome'; +import { parseCategoryURL } from '../parse'; /** * App component @@ -31,15 +34,20 @@ import Welcome from './welcome'; class App extends React.Component { constructor(props) { super(props); + // set building revision id, default 0 + const rev = (props.building)? +props.building.revision_id : 0; this.state = { user: props.user, building: props.building, - building_like: props.building_like + building_like: props.building_like, + revision_id: rev }; this.login = this.login.bind(this); this.updateUser = this.updateUser.bind(this); this.logout = this.logout.bind(this); this.selectBuilding = this.selectBuilding.bind(this); + this.colourBuilding = this.colourBuilding.bind(this); + this.increaseRevision = this.increaseRevision.bind(this); } login(user) { @@ -58,8 +66,17 @@ class App extends React.Component { 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) { - // get UPRNs and update + this.increaseRevision(building.revision_id); + // get UPRNs and update fetch(`/building/${building.building_id}/uprns.json`, { method: 'GET', headers:{ @@ -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() { return ( @@ -122,6 +195,12 @@ class App extends React.Component { mode='edit' user={this.state.user} /> ) } /> + ( + + ) } /> ( - ( + ( ) } /> diff --git a/app/src/frontend/building-edit.js b/app/src/frontend/building-edit.js index 61b29efb..d7e9d568 100644 --- a/app/src/frontend/building-edit.js +++ b/app/src/frontend/building-edit.js @@ -7,7 +7,6 @@ import InfoBox from './info-box'; import Sidebar from './sidebar'; import Tooltip from './tooltip'; import { SaveIcon } from './icons'; -import { parseCategoryURL } from '../parse'; import CONFIG from './fields-config.json'; @@ -15,12 +14,12 @@ const BuildingEdit = (props) => { if (!props.user){ return } - const cat = parseCategoryURL(props.match.url); + const cat = props.match.params.cat; if (!props.building_id){ return ( -
+
Back to maps
@@ -45,7 +44,7 @@ const BuildingEdit = (props) => { BuildingEdit.propTypes = { user: PropTypes.object, match: PropTypes.object, - building_id: PropTypes.string, + building_id: PropTypes.number } class EditForm extends Component { @@ -173,7 +172,9 @@ class EditForm extends Component { render() { const match = this.props.cat === this.props.slug; + const cat = this.props.cat; const buildingLike = this.props.building_like; + return (
@@ -220,36 +221,35 @@ class EditForm extends Component { switch (props.type) { case 'text': return + value={this.state[props.slug]} key={props.slug} cat={cat} /> case 'text_list': return + value={this.state[props.slug]} key={props.slug} cat={cat} /> case 'text_long': return + value={this.state[props.slug]} key={props.slug} cat={cat} /> case 'number': return + value={this.state[props.slug]} key={props.slug} cat={cat} /> case 'year_estimator': return + value={this.state[props.slug]} key={props.slug} cat={cat} /> case 'text_multi': return + value={this.state[props.slug]} key={props.slug} cat={cat} /> case 'checkbox': return + value={this.state[props.slug]} key={props.slug} cat={cat} /> case 'like': return + value={this.state[props.slug]} key={props.slug} cat={cat} /> default: return null } }) } - { (this.props.slug === 'like')? // special-case for likes null : @@ -258,7 +258,9 @@ class EditForm extends Component {
} - :
+ :
+ + ) : null } @@ -275,14 +277,17 @@ EditForm.propTypes = { like: PropTypes.bool, building_like: PropTypes.bool, selectBuilding: PropTypes.func, - building_id: PropTypes.string, + building_id: PropTypes.number, inactive: PropTypes.bool, fields: PropTypes.array } const TextInput = (props) => ( -