From aa59067d9eead470eb3e975dcd6f53909d6869f3 Mon Sep 17 00:00:00 2001 From: Maciej Ziarkowski Date: Mon, 21 Oct 2019 16:29:44 +0100 Subject: [PATCH 01/13] Add edit history route/controller --- app/src/api/controllers/buildingController.ts | 16 ++++++++++++++-- app/src/api/routes/buildingsRouter.ts | 5 ++++- app/src/api/services/building.ts | 1 + 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/app/src/api/controllers/buildingController.ts b/app/src/api/controllers/buildingController.ts index b2a9eb33..b53e0568 100644 --- a/app/src/api/controllers/buildingController.ts +++ b/app/src/api/controllers/buildingController.ts @@ -110,6 +110,17 @@ const getBuildingLikeById = asyncController(async (req: express.Request, res: ex } }); +const getBuildingEditHistoryById = asyncController(async (req: express.Request, res: express.Response) => { + const { building_id } = req.params; + try { + const editHistory = await buildingService.getBuildingEditHistory(building_id); + + res.send({ history: editHistory }); + } catch(error) { + res.send({ error: 'Database error' }); + } +}); + const updateBuildingLikeById = asyncController(async (req: express.Request, res: express.Response) => { if (!req.session.user_id) { return res.send({ error: 'Must be logged in' }); @@ -142,5 +153,6 @@ export default { updateBuildingById, getBuildingUPRNsById, getBuildingLikeById, - updateBuildingLikeById -}; \ No newline at end of file + updateBuildingLikeById, + getBuildingEditHistoryById +}; diff --git a/app/src/api/routes/buildingsRouter.ts b/app/src/api/routes/buildingsRouter.ts index a8921ab6..c0a4869b 100644 --- a/app/src/api/routes/buildingsRouter.ts +++ b/app/src/api/routes/buildingsRouter.ts @@ -29,4 +29,7 @@ router.route('/:building_id/like.json') .get(buildingController.getBuildingLikeById) .post(buildingController.updateBuildingLikeById); -export default router; \ No newline at end of file +router.route('/:building_id/history.json') + .get(buildingController.getBuildingEditHistoryById); + +export default router; diff --git a/app/src/api/services/building.ts b/app/src/api/services/building.ts index e97e2cf3..7cc251a9 100644 --- a/app/src/api/services/building.ts +++ b/app/src/api/services/building.ts @@ -409,6 +409,7 @@ export { queryBuildingsByReference, getBuildingById, getBuildingLikeById, + getBuildingEditHistory, getBuildingUPRNsById, saveBuilding, likeBuilding, From ab04483479d3677030fea54cbaa5bc243cd38751 Mon Sep 17 00:00:00 2001 From: Maciej Ziarkowski Date: Thu, 24 Oct 2019 12:05:52 +0100 Subject: [PATCH 02/13] Sort edit history from new to old --- app/src/api/services/building.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/api/services/building.ts b/app/src/api/services/building.ts index 7cc251a9..a90111ba 100644 --- a/app/src/api/services/building.ts +++ b/app/src/api/services/building.ts @@ -99,7 +99,8 @@ async function getBuildingEditHistory(id: number) { return await db.manyOrNone( `SELECT log_id as revision_id, forward_patch, reverse_patch, date_trunc('minute', log_timestamp), username FROM logs, users - WHERE building_id = $1 AND logs.user_id = users.user_id`, + WHERE building_id = $1 AND logs.user_id = users.user_id + ORDER BY log_timestamp DESC`, [id] ); } catch(error) { From d70f341448f6fb80617be9d538c3195517743397 Mon Sep 17 00:00:00 2001 From: Maciej Ziarkowski Date: Thu, 24 Oct 2019 12:13:07 +0100 Subject: [PATCH 03/13] Move buttons code out from ContainerHeader --- .../frontend/building/container-header.tsx | 64 +++---------------- app/src/frontend/building/data-container.tsx | 53 +++++++++++++-- .../building/header-buttons/copy-control.tsx | 35 ++++++++++ .../header-buttons/view-edit-control.tsx | 32 ++++++++++ app/src/frontend/models/building.ts | 7 ++ 5 files changed, 131 insertions(+), 60 deletions(-) create mode 100644 app/src/frontend/building/header-buttons/copy-control.tsx create mode 100644 app/src/frontend/building/header-buttons/view-edit-control.tsx create mode 100644 app/src/frontend/models/building.ts diff --git a/app/src/frontend/building/container-header.tsx b/app/src/frontend/building/container-header.tsx index 2696ceea..2f88322e 100644 --- a/app/src/frontend/building/container-header.tsx +++ b/app/src/frontend/building/container-header.tsx @@ -3,66 +3,20 @@ import { Link, NavLink } from 'react-router-dom'; import { BackIcon, EditIcon, ViewIcon }from '../components/icons'; +interface ContainerHeaderProps { + cat?: string; + backLink: string; + title: string; +} -const ContainerHeader: React.FunctionComponent = (props) => ( -
- +const ContainerHeader: React.FunctionComponent = (props) => ( +
+

{props.title}

) diff --git a/app/src/frontend/building/data-container.tsx b/app/src/frontend/building/data-container.tsx index 1547f9fc..adc8be0e 100644 --- a/app/src/frontend/building/data-container.tsx +++ b/app/src/frontend/building/data-container.tsx @@ -1,10 +1,12 @@ import React, { Fragment } from 'react'; import PropTypes from 'prop-types'; -import { Redirect } from 'react-router-dom'; +import { Redirect, NavLink } from 'react-router-dom'; import ContainerHeader from './container-header'; import ErrorBox from '../components/error-box'; import InfoBox from '../components/info-box'; +import { CopyControl } from './header-buttons/copy-control'; +import { ViewEditControl } from './header-buttons/view-edit-control'; /** * Shared functionality for view/edit forms @@ -196,15 +198,56 @@ const withCopyEdit = (WrappedComponent) => { toggleCopyAttribute: this.toggleCopyAttribute, copyingKey: (key) => this.state.keys_to_copy[key] } + + const headerBackLink = `/${this.props.mode}/categories${this.props.building != undefined ? `/${this.props.building.building_id}` : ''}`; return (
+ cat={this.props.cat} + backLink={headerBackLink} + title={this.props.title} + > + { + this.props.help && !copy.copying? + + Info + + : null + } + { + this.props.building != undefined && !this.props.inactive ? + <> + + { + !copy.copying ? + <> + History + + + : + null + } + + : null + } + { this.props.building != undefined ?
void; +} + +const CopyControl: React.FC = props => ( + props.copying ? + <> + + Copy selected + + + Cancel + + + : + + Copy + +); + +export { + CopyControl +}; diff --git a/app/src/frontend/building/header-buttons/view-edit-control.tsx b/app/src/frontend/building/header-buttons/view-edit-control.tsx new file mode 100644 index 00000000..1eb4801d --- /dev/null +++ b/app/src/frontend/building/header-buttons/view-edit-control.tsx @@ -0,0 +1,32 @@ +import React from 'react'; +import { Building } from '../../models/building'; +import { NavLink } from 'react-router-dom'; +import { ViewIcon, EditIcon } from '../../components/icons'; + +interface ViewEditControlProps { + cat: string; + mode: 'view' | 'edit'; + building: Building; +} + +const ViewEditControl: React.FC = props => ( + (props.mode === 'edit')? + + View + + + : + Edit + + +); + +export { + ViewEditControl +}; diff --git a/app/src/frontend/models/building.ts b/app/src/frontend/models/building.ts new file mode 100644 index 00000000..526d6c1a --- /dev/null +++ b/app/src/frontend/models/building.ts @@ -0,0 +1,7 @@ +interface Building { + building_id: number; +} + +export { + Building +}; From 3892191144de7b24f37ef9231854ffa6283c533f Mon Sep 17 00:00:00 2001 From: Maciej Ziarkowski Date: Thu, 24 Oct 2019 12:15:41 +0100 Subject: [PATCH 04/13] Add data fields definition, populate Age and Like --- app/src/frontend/data_fields.ts | 72 +++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 app/src/frontend/data_fields.ts diff --git a/app/src/frontend/data_fields.ts b/app/src/frontend/data_fields.ts new file mode 100644 index 00000000..bb701dd4 --- /dev/null +++ b/app/src/frontend/data_fields.ts @@ -0,0 +1,72 @@ +export enum Category { + Location = 'Location', + LandUse = 'Land Use', + Type = 'Type', + Age = 'Age', + SizeShape = 'Size & Shape', + Construction = 'Construction', + Streetscape = 'Streetscape', + Team = 'Team', + Sustainability = 'Sustainability', + Community = 'Community', + Planning = 'Planning', + Like = 'Like Me!' +} + +export const categoriesOrder: Category[] = [ + Category.Location, + Category.LandUse, + Category.Type, + Category.Age, + Category.SizeShape, + Category.Construction, + Category.Streetscape, + Category.Team, + Category.Sustainability, + Category.Community, + Category.Planning, + Category.Like, +]; + +export const dataFields = { + date_year: { + category: Category.Age, + title: "Year built (best estimate)" + }, + date_lower : { + category: Category.Age, + title: "Earliest possible start date", + tooltip: "This should be the earliest year in which building could have started." + }, + date_upper: { + category: Category.Age, + title: "Latest possible start year", + tooltip: "This should be the latest year in which building could have started." + }, + facade_year: { + category: Category.Age, + title: "Facade year", + tooltip: "Best estimate" + }, + date_source: { + category: Category.Age, + title: "Source of information", + tooltip: "Source for the main start date" + }, + date_source_detail: { + category: Category.Age, + title: "Source details", + tooltip: "References for date source (max 500 characters)" + }, + date_link: { + category: Category.Age, + title: "Text and Image Links", + tooltip: "URL for age and date reference", + }, + + likes_total: { + category: Category.Like, + title: "Total number of likes" + } + +}; From 946209282c6d8b5a95644c243a2c6f85836dc8e8 Mon Sep 17 00:00:00 2001 From: Maciej Ziarkowski Date: Thu, 24 Oct 2019 12:16:23 +0100 Subject: [PATCH 05/13] Use fields info in age container --- .../data-components/year-data-entry.tsx | 11 ++++++----- .../frontend/building/data-containers/age.tsx | 17 +++++++++-------- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/app/src/frontend/building/data-components/year-data-entry.tsx b/app/src/frontend/building/data-components/year-data-entry.tsx index c2634708..bb5fe709 100644 --- a/app/src/frontend/building/data-components/year-data-entry.tsx +++ b/app/src/frontend/building/data-components/year-data-entry.tsx @@ -2,6 +2,7 @@ import React, { Component, Fragment } from 'react'; import PropTypes from 'prop-types'; import NumericDataEntry from './numeric-data-entry'; +import { dataFields } from '../../data_fields'; class YearDataEntry extends Component { // TODO: add proper types static propTypes = { // TODO: generate propTypes from TS @@ -35,7 +36,7 @@ class YearDataEntry extends Component { // TODO: add proper types return ( { // TODO: add proper types // "type": "year_estimator" /> ) diff --git a/app/src/frontend/building/data-containers/age.tsx b/app/src/frontend/building/data-containers/age.tsx index e7baa880..69d8d082 100644 --- a/app/src/frontend/building/data-containers/age.tsx +++ b/app/src/frontend/building/data-containers/age.tsx @@ -6,6 +6,7 @@ import NumericDataEntry from '../data-components/numeric-data-entry'; import SelectDataEntry from '../data-components/select-data-entry'; import TextboxDataEntry from '../data-components/textbox-data-entry'; import YearDataEntry from '../data-components/year-data-entry'; +import { dataFields } from '../../data_fields'; /** * Age view/edit section @@ -21,23 +22,23 @@ const AgeView = (props) => ( onChange={props.onChange} /> ( ]} /> From 2e47d85faa57b9756dfef2eaf822bb534ede0c35 Mon Sep 17 00:00:00 2001 From: Maciej Ziarkowski Date: Thu, 24 Oct 2019 12:19:54 +0100 Subject: [PATCH 06/13] Add edit history components --- .../edit-history/building-edit-summary.css | 14 +++++ .../edit-history/building-edit-summary.tsx | 52 +++++++++++++++++++ .../edit-history/category-edit-summary.css | 23 ++++++++ .../edit-history/category-edit-summary.tsx | 31 +++++++++++ .../building/edit-history/edit-history.css | 4 ++ .../building/edit-history/edit-history.tsx | 46 ++++++++++++++++ .../edit-history/field-edit-summary.tsx | 20 +++++++ .../building/models/edit-history-entry.ts | 7 +++ app/src/frontend/building/sidebar.css | 10 +--- app/src/frontend/helpers.ts | 25 ++++++++- 10 files changed, 223 insertions(+), 9 deletions(-) create mode 100644 app/src/frontend/building/edit-history/building-edit-summary.css create mode 100644 app/src/frontend/building/edit-history/building-edit-summary.tsx create mode 100644 app/src/frontend/building/edit-history/category-edit-summary.css create mode 100644 app/src/frontend/building/edit-history/category-edit-summary.tsx create mode 100644 app/src/frontend/building/edit-history/edit-history.css create mode 100644 app/src/frontend/building/edit-history/edit-history.tsx create mode 100644 app/src/frontend/building/edit-history/field-edit-summary.tsx create mode 100644 app/src/frontend/building/models/edit-history-entry.ts diff --git a/app/src/frontend/building/edit-history/building-edit-summary.css b/app/src/frontend/building/edit-history/building-edit-summary.css new file mode 100644 index 00000000..2f4cba97 --- /dev/null +++ b/app/src/frontend/building/edit-history/building-edit-summary.css @@ -0,0 +1,14 @@ +.edit-history-entry { + border-bottom: 1px solid black; + padding: 1em; +} + + +.edit-history-timestamp { + font-size: 1rem; + padding: 0; +} + +.edit-history-username { + font-size: 0.9rem; +} \ No newline at end of file diff --git a/app/src/frontend/building/edit-history/building-edit-summary.tsx b/app/src/frontend/building/edit-history/building-edit-summary.tsx new file mode 100644 index 00000000..fec2df08 --- /dev/null +++ b/app/src/frontend/building/edit-history/building-edit-summary.tsx @@ -0,0 +1,52 @@ +import React from 'react'; +import { EditHistoryEntry } from '../models/edit-history-entry'; +import { arrayToDictionary, parseDate } from '../../helpers'; +import { dataFields } from '../../data_fields'; +import { CategoryEditSummary } from './category-edit-summary'; + +import './building-edit-summary.css'; + +interface BuildingEditSummaryProps { + historyEntry: EditHistoryEntry +} + +function formatDate(dt: Date) { + return dt.toLocaleString(undefined, { + weekday: 'short', + year: 'numeric', + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit' + }); +} + +const BuildingEditSummary: React.FunctionComponent = props => { + const entriesWithMetadata = Object + .entries(props.historyEntry.forward_patch) + .map(([key, value]) => { + const info = dataFields[key] || {}; + return { + title: info.title || `Unknown field (${key})`, + category: info.category || 'Unknown category', + value: value, + oldValue: props.historyEntry.reverse_patch && props.historyEntry.reverse_patch[key] + }; + }); + const entriesByCategory = arrayToDictionary(entriesWithMetadata, x => x.category); + + + return ( +
+

Edited on {formatDate(parseDate(props.historyEntry.date_trunc))}

+

By {props.historyEntry.username}

+ { + Object.entries(entriesByCategory).map(([category, fields]) => ) + } +
+ ); +} + +export { + BuildingEditSummary +}; diff --git a/app/src/frontend/building/edit-history/category-edit-summary.css b/app/src/frontend/building/edit-history/category-edit-summary.css new file mode 100644 index 00000000..a4710917 --- /dev/null +++ b/app/src/frontend/building/edit-history/category-edit-summary.css @@ -0,0 +1,23 @@ +.edit-history-category-summary ul { + list-style: none; + padding-left: 1em; +} + +.edit-history-category-title { + font-size: 1rem; +} + +.edit-history-diff { + padding: 0 0.2rem; + padding-top:0.15rem; + border-radius: 2px; +} +.edit-history-diff.old { + background-color: #f8d9bc; + color: #c24e00; + text-decoration: line-through; +} +.edit-history-diff.new { + background-color: #b6dcff; + color: #0064c2; +} \ No newline at end of file diff --git a/app/src/frontend/building/edit-history/category-edit-summary.tsx b/app/src/frontend/building/edit-history/category-edit-summary.tsx new file mode 100644 index 00000000..73a0d542 --- /dev/null +++ b/app/src/frontend/building/edit-history/category-edit-summary.tsx @@ -0,0 +1,31 @@ +import React from 'react'; + +import './category-edit-summary.css'; +import { FieldEditSummary } from './field-edit-summary'; + +interface CategoryEditSummaryProps { + category: string; + fields: { + title: string; + value: string; + oldValue: string; + }[]; +} + +const CategoryEditSummary : React.FunctionComponent = props => ( +
+

{props.category}:

+
    + { + props.fields.map(x => +
  • + +
  • ) + } +
+
+); + +export { + CategoryEditSummary +}; diff --git a/app/src/frontend/building/edit-history/edit-history.css b/app/src/frontend/building/edit-history/edit-history.css new file mode 100644 index 00000000..f79e24e3 --- /dev/null +++ b/app/src/frontend/building/edit-history/edit-history.css @@ -0,0 +1,4 @@ +.edit-history-list { + list-style: none; + padding-left: 1rem; +} \ No newline at end of file diff --git a/app/src/frontend/building/edit-history/edit-history.tsx b/app/src/frontend/building/edit-history/edit-history.tsx new file mode 100644 index 00000000..2b711c73 --- /dev/null +++ b/app/src/frontend/building/edit-history/edit-history.tsx @@ -0,0 +1,46 @@ +import React, { useState, useEffect } from 'react'; +import { EditHistoryEntry } from '../models/edit-history-entry'; +import { BuildingEditSummary } from './building-edit-summary'; + +import './edit-history.css'; +import { Building } from '../../models/building'; +import ContainerHeader from '../container-header'; + +interface EditHistoryProps { + building: Building; +} + +const EditHistory: React.FunctionComponent = (props) => { + const [history, setHistory] = useState(undefined); + + useEffect(() => { + const fetchData = async () => { + const res = await fetch(`/api/buildings/${props.building.building_id}/history.json`); + const data = await res.json(); + + setHistory(data.history); + }; + + if (props.building != undefined) { // only call fn if there is a building provided + fetchData(); // define and call, because effect cannot return anything and an async fn always returns a Promise + } + }, [props.building]); // only re-run effect on building prop change + + return ( + <> + + +
    + {history && history.map(entry => ( +
  • + +
  • + ))} +
+ + ); +} + +export { + EditHistory +}; diff --git a/app/src/frontend/building/edit-history/field-edit-summary.tsx b/app/src/frontend/building/edit-history/field-edit-summary.tsx new file mode 100644 index 00000000..f62357bf --- /dev/null +++ b/app/src/frontend/building/edit-history/field-edit-summary.tsx @@ -0,0 +1,20 @@ +import React from 'react'; + +interface FieldEditSummaryProps { + title: string; + value: any; + oldValue: any; +} + +const FieldEditSummary: React.FunctionComponent = props => ( + <> + {props.title}:  + {props.oldValue} +   + {props.value} + +); + +export { + FieldEditSummary +}; diff --git a/app/src/frontend/building/models/edit-history-entry.ts b/app/src/frontend/building/models/edit-history-entry.ts new file mode 100644 index 00000000..a38e7e0d --- /dev/null +++ b/app/src/frontend/building/models/edit-history-entry.ts @@ -0,0 +1,7 @@ +export interface EditHistoryEntry { + date_trunc: string; + username: string; + revision_id: string; + forward_patch: object; + reverse_patch: object; +} diff --git a/app/src/frontend/building/sidebar.css b/app/src/frontend/building/sidebar.css index 53caf48e..499e38ee 100644 --- a/app/src/frontend/building/sidebar.css +++ b/app/src/frontend/building/sidebar.css @@ -9,13 +9,6 @@ height: 40%; } -.info-container h2:first-child { - margin-bottom: 0.5rem; - margin-top: 0.5rem; - margin-left: -0.1em; - padding: 0 0.75rem; -} - @media (min-width: 768px){ .info-container { order: 0; @@ -99,7 +92,8 @@ color: rgb(11, 225, 225); } .icon-button.help, -.icon-button.copy { +.icon-button.copy, +.icon-button.history { margin-top: 4px; } .data-section label .icon-buttons .icon-button.copy { diff --git a/app/src/frontend/helpers.ts b/app/src/frontend/helpers.ts index b4a5d45c..d6660275 100644 --- a/app/src/frontend/helpers.ts +++ b/app/src/frontend/helpers.ts @@ -36,4 +36,27 @@ function sanitiseURL(string){ return `${url_.protocol}//${url_.hostname}${url_.pathname || ''}${url_.search || ''}${url_.hash || ''}` } -export { sanitiseURL } +function arrayToDictionary(arr: T[], keyAccessor: (obj: T) => string): {[key: string]: T[]} { + return arr.reduce((obj, item) => { + (obj[keyAccessor(item)] = obj[keyAccessor(item)] || []).push(item); + return obj; + }, {}); +} + +/** + * Parse a string containing + * @param isoUtcDate a date string in ISO8601 format + * + */ +function parseDate(isoUtcDate: string): Date { + const [year, month, day, hour, minute, second, millisecond] = isoUtcDate.match(/^(\d{4})-(\d\d)-(\d\d)T(\d\d):(\d\d):(\d\d).(\d{3})Z$/) + .splice(1) + .map(x => parseInt(x, 10)); + return new Date(Date.UTC(year, month-1, day, hour, minute, second, millisecond)); +} + +export { + sanitiseURL, + arrayToDictionary, + parseDate +}; From 8bc56fbbe2620200d2395e8ef9d94a7b8d333a74 Mon Sep 17 00:00:00 2001 From: Maciej Ziarkowski Date: Thu, 24 Oct 2019 12:20:48 +0100 Subject: [PATCH 07/13] Add edit history frontend routing --- app/src/frontend/app.tsx | 2 +- app/src/frontend/map-app.tsx | 9 ++++++++- app/src/parse.ts | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/app/src/frontend/app.tsx b/app/src/frontend/app.tsx index 5e681574..b1428cbc 100644 --- a/app/src/frontend/app.tsx +++ b/app/src/frontend/app.tsx @@ -105,7 +105,7 @@ class App extends React.Component { // TODO: add proper types - ( + ( { interface MapAppState { category: string; revision_id: number; - building: any; + building: Building; building_like: boolean; } @@ -234,6 +236,11 @@ class MapApp extends React.Component { /> + + + + + diff --git a/app/src/parse.ts b/app/src/parse.ts index db2a3493..a8c680c7 100644 --- a/app/src/parse.ts +++ b/app/src/parse.ts @@ -23,7 +23,7 @@ function strictParseInt(value) { * @returns {number|undefined} */ function parseBuildingURL(url) { - const re = /\/(\d+)$/; + const re = /\/(\d+)(\/history)?$/; const matches = re.exec(url); if (matches && matches.length >= 2) { From 0f88627ab29314ffe8794c457922561e40e5aeb0 Mon Sep 17 00:00:00 2001 From: Maciej Ziarkowski Date: Thu, 24 Oct 2019 15:57:09 +0100 Subject: [PATCH 08/13] Update code documentation --- app/src/frontend/helpers.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/app/src/frontend/helpers.ts b/app/src/frontend/helpers.ts index d6660275..ae3374a7 100644 --- a/app/src/frontend/helpers.ts +++ b/app/src/frontend/helpers.ts @@ -36,6 +36,13 @@ function sanitiseURL(string){ return `${url_.protocol}//${url_.hostname}${url_.pathname || ''}${url_.search || ''}${url_.hash || ''}` } +/** + * Transform an array of objects into a dictionary of arrays of objects, + * where the objects are grouped into arrays given an arbitrary key function + * that gives a key for each object. + * @param arr array of objects to group + * @param keyAccessor function returning the grouping key for each object in the original array + */ function arrayToDictionary(arr: T[], keyAccessor: (obj: T) => string): {[key: string]: T[]} { return arr.reduce((obj, item) => { (obj[keyAccessor(item)] = obj[keyAccessor(item)] || []).push(item); @@ -44,9 +51,9 @@ function arrayToDictionary(arr: T[], keyAccessor: (obj: T) => string): {[key: } /** - * Parse a string containing - * @param isoUtcDate a date string in ISO8601 format - * + * Parse a string containing an ISO8601 formatted date + * @param isoUtcDate a date string in ISO8601 format, assuming UTC + * @returns a JS Date object with the UTC time encoded */ function parseDate(isoUtcDate: string): Date { const [year, month, day, hour, minute, second, millisecond] = isoUtcDate.match(/^(\d{4})-(\d\d)-(\d\d)T(\d\d):(\d\d):(\d\d).(\d{3})Z$/) From 09c343f91d15b7ec62da2003b68481874278c2e4 Mon Sep 17 00:00:00 2001 From: Maciej Ziarkowski Date: Fri, 25 Oct 2019 17:43:39 +0100 Subject: [PATCH 09/13] Move all field titles/tooltips to data_fields.ts --- .../building/data-containers/location.tsx | 31 +- .../building/data-containers/planning.tsx | 43 +-- .../building/data-containers/size.tsx | 31 +- .../data-containers/sustainability.tsx | 19 +- .../building/data-containers/type.tsx | 14 +- app/src/frontend/data_fields.ts | 272 ++++++++++++++++++ 6 files changed, 344 insertions(+), 66 deletions(-) diff --git a/app/src/frontend/building/data-containers/location.tsx b/app/src/frontend/building/data-containers/location.tsx index 55d22923..46c2636c 100644 --- a/app/src/frontend/building/data-containers/location.tsx +++ b/app/src/frontend/building/data-containers/location.tsx @@ -5,23 +5,24 @@ import DataEntry from '../data-components/data-entry'; import NumericDataEntry from '../data-components/numeric-data-entry'; import UPRNsDataEntry from '../data-components/uprns-data-entry'; import InfoBox from '../../components/info-box'; +import { dataFields } from '../../data_fields'; const LocationView = (props) => ( ( step={1} /> ( disabled={true} /> ( disabled={true} /> ( onChange={props.onChange} /> ( maxLength={8} /> ( onChange={props.onChange} /> ( ( /> ( onChange={props.onChange} /> ( onChange={props.onChange} /> ( onChange={props.onChange} /> ( onChange={props.onChange} /> ( ]} /> ( ]} /> ( onChange={props.onChange} /> ( onChange={props.onChange} /> ( onChange={props.onChange} /> ( onChange={props.onChange} /> ( onChange={props.onChange} /> ( onChange={props.onChange} /> ( onChange={props.onChange} /> ( onChange={props.onChange} /> ( onChange={props.onChange} /> ( onChange={props.onChange} /> ( ( disabled={true} /> ( disabled={true} /> ( ( step={0.1} /> ( ( step={0.1} /> ( /> ( step={0.1} /> ( disabled={true} /> ( disabled={true} /> ( ]} /> { return ( { return ( diff --git a/app/src/frontend/data_fields.ts b/app/src/frontend/data_fields.ts index bb701dd4..ab3c4b81 100644 --- a/app/src/frontend/data_fields.ts +++ b/app/src/frontend/data_fields.ts @@ -29,6 +29,85 @@ export const categoriesOrder: Category[] = [ ]; export const dataFields = { + location_name: { + category: Category.Location, + title: "Building Name", + tooltip: "May not be needed for many buildings.", + }, + location_number: { + category: Category.Location, + title: "Building number", + }, + location_street: { + category: Category.Location, + title: "Street", + //tooltip: , + }, + location_line_two: { + category: Category.Location, + title: "Address line 2", + //tooltip: , + }, + location_town: { + category: Category.Location, + title: "Town", + //tooltip: , + }, + location_postcode: { + category: Category.Location, + title: "Postcode", + //tooltip: , + }, + ref_toid: { + category: Category.Location, + title: "TOID", + tooltip: "Ordnance Survey Topography Layer ID (to be filled automatically)", + }, + + /** + * UPRNs is not part of the buildings table, but the string fields + * are included here for completeness + */ + uprns: { + category: Category.Location, + title: "UPRNs", + tooltip: "Unique Property Reference Numbers (to be filled automatically)" + }, + + ref_osm_id: { + category: Category.Location, + title: "OSM ID", + tooltip: "OpenStreetMap feature ID", + }, + location_latitude: { + category: Category.Location, + title: "Latitude", + }, + location_longitude: { + category: Category.Location, + title: "Longitude", + }, + + building_attachment_form: { + category: Category.Type, + title: "Building configuration (attachment)?", + tooltip: "We have prepopulated these based on their current attachment. A building can either be detached, semi-detached or part of a terrace (middle or end)", + }, + date_change_building_use: { + category: Category.Type, + title:"When did use change?", + tooltip: "This is the date the building stopped being used for for the function it was built for. I.e. if it was Victorian warehouse which is now an office this would be when it became an office or if it was something before that, maybe a garage then the date that happened", + }, + /** + * original_building_use does not exist in database yet. + * Slug needs to be adjusted if the db column will be named differently + */ + original_building_use: { + category: Category.Type, + title: "Original building use", + tooltip: "What was the building originally used for when it was built? I.e. If it was Victorian warehouse which is now an office this would be warehouse", + }, + date_year: { category: Category.Age, title: "Year built (best estimate)" @@ -64,6 +143,199 @@ export const dataFields = { tooltip: "URL for age and date reference", }, + size_storeys_core: { + category: Category.SizeShape, + title: "Core storeys", + tooltip: "How many storeys between the pavement and start of roof?", + }, + size_storeys_attic: { + category: Category.SizeShape, + title: "Attic storeys", + tooltip: "How many storeys above start of roof?", + }, + size_storeys_basement: { + category: Category.SizeShape, + title: "Basement storeys", + tooltip: "How many storeys below pavement level?", + }, + size_height_apex: { + category: Category.SizeShape, + title: "Height to apex (m)", + //tooltip: , + }, + size_height_eaves: { + category: Category.SizeShape, + title: "Height to eaves (m)", + //tooltip: , + }, + size_floor_area_ground: { + category: Category.SizeShape, + title: "Ground floor area (m²)", + //tooltip: , + }, + size_floor_area_total: { + category: Category.SizeShape, + title: "Total floor area (m²)", + //tooltip: , + }, + size_width_frontage: { + category: Category.SizeShape, + title: "Frontage Width (m)", + //tooltip: , + }, + size_plot_area_total: { + category: Category.SizeShape, + title: "Total area of plot (m²)", + //tooltip: , + }, + size_far_ratio: { + category: Category.SizeShape, + title: "FAR ratio (percentage of plot covered by building)", + //tooltip: , + }, + size_configuration: { + category: Category.SizeShape, + title: "Configuration (semi/detached, end/terrace)", + //tooltip: , + }, + size_roof_shape: { + category: Category.SizeShape, + title: "Roof shape", + //tooltip: , + }, + + sust_breeam_rating: { + category: Category.Sustainability, + title: "BREEAM Rating", + tooltip: "(Building Research Establishment Environmental Assessment Method) May not be present for many buildings", + }, + sust_dec: { + category: Category.Sustainability, + title: "DEC Rating", + tooltip: "(Display Energy Certificate) Any public building should have (and display) a DEC. Showing how the energy use for that building compares to other buildings with same use", + }, + sust_aggregate_estimate_epc: { + category: Category.Sustainability, + title: "EPC Rating", + tooltip: "(Energy Performance Certifcate) Any premises sold or rented is required to have an EPC to show how energy efficient it is. Only buildings rate grade E or higher maybe rented", + }, + sust_retrofit_date: { + category: Category.Sustainability, + title: "Last significant retrofit", + tooltip: "Date of last major building refurbishment", + }, + sust_life_expectancy: { + category: Category.Sustainability, + title: "Expected lifespan for typology", + //tooltip: , + }, + + planning_portal_link: { + category: Category.Planning, + title: "Planning portal link", + //tooltip: , + }, + planning_in_conservation_area: { + category: Category.Planning, + title: "In a conservation area?", + //tooltip: , + }, + planning_conservation_area_name: { + category: Category.Planning, + title: "Conservation area name", + //tooltip: , + }, + planning_in_list: { + category: Category.Planning, + title: "Is listed on the National Heritage List for England?", + //tooltip: , + }, + planning_list_id: { + category: Category.Planning, + title: "National Heritage List for England list id", + //tooltip: , + }, + planning_list_cat: { + category: Category.Planning, + title: "National Heritage List for England list type", + //tooltip: , + }, + planning_list_grade: { + category: Category.Planning, + title: "Listing grade", + //tooltip: , + }, + planning_heritage_at_risk_id: { + category: Category.Planning, + title: "Heritage at risk list id", + //tooltip: , + }, + planning_world_list_id: { + category: Category.Planning, + title: "World heritage list id", + //tooltip: , + }, + planning_in_glher: { + category: Category.Planning, + title: "In the Greater London Historic Environment Record?", + //tooltip: , + }, + planning_glher_url: { + category: Category.Planning, + title: "Greater London Historic Environment Record link", + //tooltip: , + }, + planning_in_apa: { + category: Category.Planning, + title: "In an Architectural Priority Area?", + //tooltip: , + }, + planning_apa_name: { + category: Category.Planning, + title: "Architectural Priority Area name", + //tooltip: , + }, + planning_apa_tier: { + category: Category.Planning, + title: "Architectural Priority Area tier", + //tooltip: , + }, + planning_in_local_list: { + category: Category.Planning, + title: "Is locally listed?", + //tooltip: , + }, + planning_local_list_url: { + category: Category.Planning, + title: "Local list link", + //tooltip: , + }, + planning_in_historic_area_assessment: { + category: Category.Planning, + title: "Within a historic area assessment?", + //tooltip: , + }, + planning_historic_area_assessment_url: { + category: Category.Planning, + title: "Historic area assessment link", + //tooltip: , + }, + planning_demolition_proposed: { + category: Category.Planning, + title: "Is the building proposed for demolition?", + //tooltip: , + }, + planning_demolition_complete: { + category: Category.Planning, + title: "Has the building been demolished?", + //tooltip: , + }, + planning_demolition_history: { + category: Category.Planning, + title: "Dates of construction and demolition of previous buildings on site", + //tooltip: , + }, + likes_total: { category: Category.Like, title: "Total number of likes" From 86b252f186f95f51579458947fdcad7f074622b6 Mon Sep 17 00:00:00 2001 From: Maciej Ziarkowski Date: Fri, 25 Oct 2019 18:07:24 +0100 Subject: [PATCH 10/13] Add field labels to multi edit view --- app/src/frontend/building/multi-edit.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/frontend/building/multi-edit.tsx b/app/src/frontend/building/multi-edit.tsx index 28b675f6..a0712fc3 100644 --- a/app/src/frontend/building/multi-edit.tsx +++ b/app/src/frontend/building/multi-edit.tsx @@ -7,6 +7,7 @@ import Sidebar from './sidebar'; import InfoBox from '../components/info-box'; import { BackIcon }from '../components/icons'; import DataEntry from './data-components/data-entry'; +import { dataFields } from '../data_fields'; const MultiEdit = (props) => { @@ -63,9 +64,10 @@ const MultiEdit = (props) => { { Object.keys(data).map((key => { + const info = dataFields[key] || {}; return ( Date: Wed, 30 Oct 2019 12:28:10 +0000 Subject: [PATCH 11/13] Only accept view/edit modes for building view --- app/src/frontend/building/building-view.tsx | 2 +- app/src/frontend/building/data-container.tsx | 2 +- app/src/frontend/map-app.tsx | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/src/frontend/building/building-view.tsx b/app/src/frontend/building/building-view.tsx index d322d984..e66a93ee 100644 --- a/app/src/frontend/building/building-view.tsx +++ b/app/src/frontend/building/building-view.tsx @@ -19,7 +19,7 @@ import { Building } from '../models/building'; interface BuildingViewProps { cat: string; - mode: 'view' | 'edit' | 'multi-edit'; + mode: 'view' | 'edit'; building: Building; building_like: boolean; user: any; diff --git a/app/src/frontend/building/data-container.tsx b/app/src/frontend/building/data-container.tsx index a7294720..db903baa 100644 --- a/app/src/frontend/building/data-container.tsx +++ b/app/src/frontend/building/data-container.tsx @@ -20,7 +20,7 @@ interface DataContainerProps { inactive?: boolean; user: User; - mode: 'view' | 'edit' | 'multi-edit'; + mode: 'view' | 'edit'; building: Building; building_like: boolean; selectBuilding: (building: Building) => void diff --git a/app/src/frontend/map-app.tsx b/app/src/frontend/map-app.tsx index 9e5d6e05..7849a56d 100644 --- a/app/src/frontend/map-app.tsx +++ b/app/src/frontend/map-app.tsx @@ -221,6 +221,7 @@ class MapApp extends React.Component { render() { const mode = this.props.match.params.mode; + const viewEditMode = mode === 'multi-edit' ? undefined : mode; let category = this.state.category || 'age'; @@ -246,7 +247,7 @@ class MapApp extends React.Component { Date: Wed, 30 Oct 2019 12:35:23 +0000 Subject: [PATCH 12/13] Add white background on edit history header --- app/src/frontend/building/edit-history/edit-history.tsx | 2 +- app/src/frontend/building/sidebar.css | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/app/src/frontend/building/edit-history/edit-history.tsx b/app/src/frontend/building/edit-history/edit-history.tsx index 2b711c73..772be6d3 100644 --- a/app/src/frontend/building/edit-history/edit-history.tsx +++ b/app/src/frontend/building/edit-history/edit-history.tsx @@ -28,7 +28,7 @@ const EditHistory: React.FunctionComponent = (props) => { return ( <> - +
    {history && history.map(entry => ( diff --git a/app/src/frontend/building/sidebar.css b/app/src/frontend/building/sidebar.css index 80380f3c..1bcffe83 100644 --- a/app/src/frontend/building/sidebar.css +++ b/app/src/frontend/building/sidebar.css @@ -30,6 +30,10 @@ z-index: 1000; } +.edit-history { + background-color: white; +} + @media (min-width: 768px) { .section-header { position: sticky; From 5406b604165af0da355ad0cf9e3fe61404004d38 Mon Sep 17 00:00:00 2001 From: Maciej Ziarkowski Date: Wed, 30 Oct 2019 13:29:02 +0000 Subject: [PATCH 13/13] Improve edit history styling --- .../building/edit-history/building-edit-summary.css | 4 ++-- .../building/edit-history/category-edit-summary.css | 9 +++++++-- app/src/frontend/building/edit-history/edit-history.css | 4 ++++ app/src/frontend/building/sidebar.css | 4 ---- 4 files changed, 13 insertions(+), 8 deletions(-) diff --git a/app/src/frontend/building/edit-history/building-edit-summary.css b/app/src/frontend/building/edit-history/building-edit-summary.css index 2f4cba97..a2495710 100644 --- a/app/src/frontend/building/edit-history/building-edit-summary.css +++ b/app/src/frontend/building/edit-history/building-edit-summary.css @@ -5,10 +5,10 @@ .edit-history-timestamp { - font-size: 1rem; + font-size: 0.9em; padding: 0; } .edit-history-username { - font-size: 0.9rem; + font-size: 0.9em; } \ No newline at end of file diff --git a/app/src/frontend/building/edit-history/category-edit-summary.css b/app/src/frontend/building/edit-history/category-edit-summary.css index a4710917..3cfdb579 100644 --- a/app/src/frontend/building/edit-history/category-edit-summary.css +++ b/app/src/frontend/building/edit-history/category-edit-summary.css @@ -1,10 +1,15 @@ +.edit-history-category-summary { + margin-top: 1.5rem; +} + .edit-history-category-summary ul { list-style: none; - padding-left: 1em; + padding-left: 0.5em; } .edit-history-category-title { - font-size: 1rem; + font-size: 0.9em; + font-weight: 600; } .edit-history-diff { diff --git a/app/src/frontend/building/edit-history/edit-history.css b/app/src/frontend/building/edit-history/edit-history.css index f79e24e3..8aa5cfe2 100644 --- a/app/src/frontend/building/edit-history/edit-history.css +++ b/app/src/frontend/building/edit-history/edit-history.css @@ -1,3 +1,7 @@ +.edit-history { + background-color: white; +} + .edit-history-list { list-style: none; padding-left: 1rem; diff --git a/app/src/frontend/building/sidebar.css b/app/src/frontend/building/sidebar.css index 1bcffe83..80380f3c 100644 --- a/app/src/frontend/building/sidebar.css +++ b/app/src/frontend/building/sidebar.css @@ -30,10 +30,6 @@ z-index: 1000; } -.edit-history { - background-color: white; -} - @media (min-width: 768px) { .section-header { position: sticky;