From 60371afd03dc4c1f9533771807b6e9bbc8992eea Mon Sep 17 00:00:00 2001 From: Tom Russell Date: Thu, 9 May 2019 09:16:36 +0100 Subject: [PATCH 01/13] Sketch out hardcoded multi-edit --- app/src/frontend/app.js | 50 ++++++++++++++++++++++++++++++++-- app/src/frontend/map.css | 3 ++ app/src/frontend/map.js | 23 ++++++++++++---- app/src/frontend/multi-edit.js | 25 +++++++++++++++++ 4 files changed, 92 insertions(+), 9 deletions(-) create mode 100644 app/src/frontend/multi-edit.js diff --git a/app/src/frontend/app.js b/app/src/frontend/app.js index 2581d1c7..02d00e5b 100644 --- a/app/src/frontend/app.js +++ b/app/src/frontend/app.js @@ -8,6 +8,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'; @@ -31,15 +32,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 +64,16 @@ class App extends React.Component { this.setState({user: undefined}); } + increaseRevision(revision_id) { + // bump revision id, only ever increasing + if (revision_id > this.state.revision_id){ + this.setState({revision_id: revision_id}) + } + } + 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 +115,28 @@ class App extends React.Component { }); } + colourBuilding(building) { + fetch(`/building/${building.building_id}.json`, { + method: 'POST', + body: JSON.stringify({date_year: 1999}), // TODO link to multi/pass in data + headers:{ + 'Content-Type': 'application/json' + }, + credentials: 'same-origin' + }).then( + res => res.json() + ).then(res => { + if (res.error) { + console.error({error: res.error}) + } else { + console.log(res); + this.increaseRevision(res.revision_id); + } + }).catch( + (err) => console.error({error: err}) + ); + } + render() { return ( @@ -122,6 +158,12 @@ class App extends React.Component { mode='edit' user={this.state.user} /> ) } /> + ( + + ) } /> ( - ( + ( ) } /> diff --git a/app/src/frontend/map.css b/app/src/frontend/map.css index 39950f36..bd6e4106 100644 --- a/app/src/frontend/map.css +++ b/app/src/frontend/map.css @@ -20,3 +20,6 @@ border: 1px solid #fff; box-shadow: 0 0 1px 1px #222; } +.leaflet-grab { + cursor: crosshair; +} diff --git a/app/src/frontend/map.js b/app/src/frontend/map.js index 2ce1f243..86314a32 100644 --- a/app/src/frontend/map.js +++ b/app/src/frontend/map.js @@ -29,6 +29,7 @@ class ColouringMap extends Component { this.handleClick = this.handleClick.bind(this); this.handleLocate = this.handleLocate.bind(this); this.themeSwitch = this.themeSwitch.bind(this); + this.getMode = this.getMode.bind(this); } handleLocate(lat, lng, zoom){ @@ -39,9 +40,14 @@ class ColouringMap extends Component { }) } - handleClick(e) { + getMode() { 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 lng = e.latlng.lng const newCat = parseCategoryURL(this.props.match.url); @@ -53,8 +59,13 @@ class ColouringMap extends Component { ).then(function(data){ if (data && data.length){ const building = data[0]; - this.props.selectBuilding(building); - this.props.history.push(`/${mode}/${mapCat}/building/${building.building_id}.html`); + if (mode === 'multi') { + // colour building directly + this.props.colourBuilding(building); + } else { + this.props.selectBuilding(building); + this.props.history.push(`/${mode}/${map_cat}/building/${building.building_id}.html`); + } } else { // deselect but keep/return to expected colour theme this.props.selectBuilding(undefined); @@ -94,8 +105,8 @@ class ColouringMap extends Component { } const tileset = tilesetByCat[cat]; // pick revision id to bust browser cache - const rev = this.props.building? this.props.building.revision_id : ''; - const dataLayer = tileset? + const rev = this.props.revision_id; + const dataLayer = data_tileset? { + if (!props.user){ + return + } + const cat = parseCategoryURL(props.match.url); + return ( + +
+

Click a building to colour

+

Set Year built to 1999

+
+
+ ); +} + +export default MultiEdit; From 0685cbf136fb58a568c11c679a5674e61b409381 Mon Sep 17 00:00:00 2001 From: Tom Russell Date: Fri, 10 May 2019 14:00:20 +0100 Subject: [PATCH 02/13] Cat: use match params where possible --- app/src/frontend/building-edit.js | 3 +-- app/src/frontend/building-view.js | 3 +-- app/src/frontend/multi-edit.js | 4 ++-- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/app/src/frontend/building-edit.js b/app/src/frontend/building-edit.js index 61b29efb..7d7d42ec 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,7 +14,7 @@ const BuildingEdit = (props) => { if (!props.user){ return } - const cat = parseCategoryURL(props.match.url); + const cat = props.match.params.cat; if (!props.building_id){ return ( diff --git a/app/src/frontend/building-view.js b/app/src/frontend/building-view.js index 9bf287a1..99fc3e73 100644 --- a/app/src/frontend/building-view.js +++ b/app/src/frontend/building-view.js @@ -7,7 +7,6 @@ import Sidebar from './sidebar'; import Tooltip from './tooltip'; import InfoBox from './info-box'; import { EditIcon } from './icons'; -import { parseCategoryURL } from '../parse'; import CONFIG from './fields-config.json'; @@ -22,7 +21,7 @@ const BuildingView = (props) => { ); } - const cat = parseCategoryURL(props.match.url); + const cat = props.match.params.cat; return ( { diff --git a/app/src/frontend/multi-edit.js b/app/src/frontend/multi-edit.js index 2c160ca6..e7ef67f9 100644 --- a/app/src/frontend/multi-edit.js +++ b/app/src/frontend/multi-edit.js @@ -2,14 +2,14 @@ import React from 'react'; import { Redirect } from 'react-router-dom'; import Sidebar from './sidebar'; -import { parseCategoryURL } from '../parse'; const MultiEdit = (props) => { if (!props.user){ return } - const cat = parseCategoryURL(props.match.url); + const cat = props.match.params.cat; + return ( Date: Fri, 10 May 2019 16:06:22 +0100 Subject: [PATCH 03/13] Add query-string for URL ?a=b parsing --- app/package-lock.json | 23 +++++++++++++++++++++-- app/package.json | 1 + 2 files changed, 22 insertions(+), 2 deletions(-) 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" }, From c03f716a2893f5e6a105dc663f45abc60172f7f7 Mon Sep 17 00:00:00 2001 From: Tom Russell Date: Fri, 10 May 2019 16:06:33 +0100 Subject: [PATCH 04/13] Match multi-edit categories --- app/src/parse.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/parse.js b/app/src/parse.js index 87a20ff0..712cfb59 100644 --- a/app/src/parse.js +++ b/app/src/parse.js @@ -43,7 +43,7 @@ function parseCategoryURL(url) { if (url === '/') { 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; return cat; } From 426c7ff9f6fc734ede2748b3237f954499ec5a0d Mon Sep 17 00:00:00 2001 From: Tom Russell Date: Fri, 10 May 2019 16:10:16 +0100 Subject: [PATCH 05/13] 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 --- app/src/frontend/app.js | 42 ++++++++- app/src/frontend/building-edit.js | 139 ++++++++++++++++++------------ app/src/frontend/building-view.js | 18 ++++ app/src/frontend/multi-edit.js | 49 ++++++++++- app/src/frontend/sidebar.css | 15 ++-- 5 files changed, 195 insertions(+), 68 deletions(-) diff --git a/app/src/frontend/app.js b/app/src/frontend/app.js index 02d00e5b..32949f09 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'; @@ -16,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 @@ -116,9 +118,44 @@ class App extends React.Component { } 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', - 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:{ 'Content-Type': 'application/json' }, @@ -129,7 +166,6 @@ class App extends React.Component { if (res.error) { console.error({error: res.error}) } else { - console.log(res); this.increaseRevision(res.revision_id); } }).catch( diff --git a/app/src/frontend/building-edit.js b/app/src/frontend/building-edit.js index 7d7d42ec..b8f06321 100644 --- a/app/src/frontend/building-edit.js +++ b/app/src/frontend/building-edit.js @@ -19,7 +19,7 @@ const BuildingEdit = (props) => { return ( -
+
Back to maps
@@ -172,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 (
@@ -204,61 +206,62 @@ class EditForm extends Component {
{ - match? ( - !this.props.inactive? -
- { - this.props.slug === 'location'? - - : null - } - - { - this.props.fields.map((props) => { - switch (props.type) { - case 'text': - return - case 'text_list': - return - case 'text_long': - return - case 'number': - return - case 'year_estimator': - return - case 'text_multi': - return - case 'checkbox': - return - case 'like': - return + { + this.props.slug === 'location'? + + : null + } + + { + this.props.fields.map((props) => { + switch (props.type) { + case "text": + return + case "text_list": + return + case "text_long": + return + case "number": + return + case "year_estimator": + return + case "text_multi": + return + case "checkbox": + return + case "like": + return - default: - return null - } - }) + value={this.state[props.slug]} key={props.slug} cat={cat} /> + default: + return null } - - - { - (this.props.slug === 'like')? // special-case for likes - null : -
- -
- } - - :
- ) : null + }) + } + + { + (this.props.slug === 'like')? // special-case for likes + null : +
+ +
+ } + + :
+ + + ) : null }
) @@ -281,7 +284,10 @@ EditForm.propTypes = { const TextInput = (props) => ( -