Merge pull request #477 from mz8i/feature/84-show-edit-history
Feature 84: show edit history
This commit is contained in:
commit
8da20a47b4
@ -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' });
|
||||
@ -152,5 +163,6 @@ export default {
|
||||
getBuildingUPRNsById,
|
||||
getBuildingLikeById,
|
||||
updateBuildingLikeById,
|
||||
getBuildingEditHistoryById,
|
||||
getLatestRevisionId
|
||||
};
|
||||
};
|
||||
|
@ -31,4 +31,7 @@ router.route('/:building_id/like.json')
|
||||
.get(buildingController.getBuildingLikeById)
|
||||
.post(buildingController.updateBuildingLikeById);
|
||||
|
||||
export default router;
|
||||
router.route('/:building_id/history.json')
|
||||
.get(buildingController.getBuildingEditHistoryById);
|
||||
|
||||
export default router;
|
||||
|
@ -112,7 +112,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) {
|
||||
@ -422,6 +423,7 @@ export {
|
||||
queryBuildingsByReference,
|
||||
getBuildingById,
|
||||
getBuildingLikeById,
|
||||
getBuildingEditHistory,
|
||||
getBuildingUPRNsById,
|
||||
saveBuilding,
|
||||
likeBuilding,
|
||||
|
@ -50,7 +50,7 @@ class App extends React.Component<AppProps, any> { // TODO: add proper types
|
||||
building_like: PropTypes.bool
|
||||
};
|
||||
|
||||
static mapAppPaths = ['/', '/:mode(view|edit|multi-edit)/:category/:building(\\d+)?'];
|
||||
static mapAppPaths = ['/', '/:mode(view|edit|multi-edit)/:category/:building(\\d+)?/(history)?'];
|
||||
|
||||
constructor(props: Readonly<AppProps>) {
|
||||
super(props);
|
||||
|
@ -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;
|
||||
|
@ -4,75 +4,19 @@ import { Link, NavLink } from 'react-router-dom';
|
||||
import { BackIcon, EditIcon, ViewIcon }from '../components/icons';
|
||||
|
||||
interface ContainerHeaderProps {
|
||||
cat: string;
|
||||
mode: 'view' | 'edit' | 'multi-edit';
|
||||
building: any;
|
||||
cat?: string;
|
||||
backLink: string;
|
||||
title: string;
|
||||
copy: any;
|
||||
inactive?: boolean;
|
||||
data_string: string;
|
||||
help: string;
|
||||
}
|
||||
|
||||
const ContainerHeader: React.FunctionComponent<ContainerHeaderProps> = (props) => (
|
||||
<header className={`section-header view ${props.cat} background-${props.cat}`}>
|
||||
<Link className="icon-button back" to={`/${props.mode}/categories${props.building != undefined ? `/${props.building.building_id}` : ''}`}>
|
||||
<header className={`section-header view ${props.cat ? props.cat : ''} ${props.cat ? `background-${props.cat}` : ''}`}>
|
||||
<Link className="icon-button back" to={props.backLink}>
|
||||
<BackIcon />
|
||||
</Link>
|
||||
<h2 className="h2">{props.title}</h2>
|
||||
<nav className="icon-buttons">
|
||||
{
|
||||
props.building != undefined && !props.inactive ?
|
||||
props.copy.copying?
|
||||
<Fragment>
|
||||
<NavLink
|
||||
to={`/multi-edit/${props.cat}?data=${props.data_string}`}
|
||||
className="icon-button copy">
|
||||
Copy selected
|
||||
</NavLink>
|
||||
<a
|
||||
className="icon-button copy"
|
||||
onClick={props.copy.toggleCopying}>
|
||||
Cancel
|
||||
</a>
|
||||
</Fragment>
|
||||
:
|
||||
<a
|
||||
className="icon-button copy"
|
||||
onClick={props.copy.toggleCopying}>
|
||||
Copy
|
||||
</a>
|
||||
: null
|
||||
}
|
||||
{
|
||||
props.help && !props.copy.copying?
|
||||
<a
|
||||
className="icon-button help"
|
||||
title="Find out more"
|
||||
href={props.help}>
|
||||
Info
|
||||
</a>
|
||||
: null
|
||||
}
|
||||
{
|
||||
props.building != undefined && !props.inactive && !props.copy.copying?
|
||||
(props.mode === 'edit')?
|
||||
<NavLink
|
||||
className="icon-button view"
|
||||
title="View data"
|
||||
to={`/view/${props.cat}/${props.building.building_id}`}>
|
||||
View
|
||||
<ViewIcon />
|
||||
</NavLink>
|
||||
: <NavLink
|
||||
className="icon-button edit"
|
||||
title="Edit data"
|
||||
to={`/edit/${props.cat}/${props.building.building_id}`}>
|
||||
Edit
|
||||
<EditIcon />
|
||||
</NavLink>
|
||||
: null
|
||||
}
|
||||
{props.children}
|
||||
</nav>
|
||||
</header>
|
||||
)
|
||||
|
@ -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<any, any> { // TODO: add proper types
|
||||
static propTypes = { // TODO: generate propTypes from TS
|
||||
@ -35,7 +36,7 @@ class YearDataEntry extends Component<any, any> { // TODO: add proper types
|
||||
return (
|
||||
<Fragment>
|
||||
<NumericDataEntry
|
||||
title="Year built (best estimate)"
|
||||
title={dataFields.date_year.title}
|
||||
slug="date_year"
|
||||
value={props.year}
|
||||
mode={props.mode}
|
||||
@ -44,24 +45,24 @@ class YearDataEntry extends Component<any, any> { // TODO: add proper types
|
||||
// "type": "year_estimator"
|
||||
/>
|
||||
<NumericDataEntry
|
||||
title="Latest possible start year"
|
||||
title={dataFields.date_upper.title}
|
||||
slug="date_upper"
|
||||
value={props.upper}
|
||||
mode={props.mode}
|
||||
copy={props.copy}
|
||||
onChange={props.onChange}
|
||||
step={1}
|
||||
tooltip="This should be the latest year in which building could have started."
|
||||
tooltip={dataFields.date_upper.tooltip}
|
||||
/>
|
||||
<NumericDataEntry
|
||||
title="Earliest possible start date"
|
||||
title={dataFields.date_lower.title}
|
||||
slug="date_lower"
|
||||
value={props.lower}
|
||||
mode={props.mode}
|
||||
copy={props.copy}
|
||||
onChange={props.onChange}
|
||||
step={1}
|
||||
tooltip="This should be the earliest year in which building could have started."
|
||||
tooltip={dataFields.date_lower.tooltip}
|
||||
/>
|
||||
</Fragment>
|
||||
)
|
||||
|
@ -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';
|
||||
import { Building } from '../models/building';
|
||||
import { User } from '../models/user';
|
||||
import { compareObjects } from '../helpers';
|
||||
@ -18,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
|
||||
@ -232,17 +234,59 @@ const withCopyEdit = (WrappedComponent: React.ComponentType<CategoryViewProps>)
|
||||
toggleCopying: this.toggleCopying,
|
||||
toggleCopyAttribute: this.toggleCopyAttribute,
|
||||
copyingKey: (key: string) => this.state.keys_to_copy[key]
|
||||
};
|
||||
}
|
||||
|
||||
const headerBackLink = `/${this.props.mode}/categories${this.props.building != undefined ? `/${this.props.building.building_id}` : ''}`;
|
||||
const edited = this.isEdited();
|
||||
|
||||
return (
|
||||
<section
|
||||
id={this.props.cat}
|
||||
className="data-section">
|
||||
<ContainerHeader
|
||||
{...this.props}
|
||||
data_string={data_string}
|
||||
copy={copy}
|
||||
/>
|
||||
cat={this.props.cat}
|
||||
backLink={headerBackLink}
|
||||
title={this.props.title}
|
||||
>
|
||||
{
|
||||
this.props.help && !copy.copying?
|
||||
<a
|
||||
className="icon-button help"
|
||||
title="Find out more"
|
||||
href={this.props.help}>
|
||||
Info
|
||||
</a>
|
||||
: null
|
||||
}
|
||||
{
|
||||
this.props.building != undefined && !this.props.inactive ?
|
||||
<>
|
||||
<CopyControl
|
||||
cat={this.props.cat}
|
||||
data_string={data_string}
|
||||
copying={copy.copying}
|
||||
toggleCopying={copy.toggleCopying}
|
||||
/>
|
||||
{
|
||||
!copy.copying ?
|
||||
<>
|
||||
<NavLink
|
||||
className="icon-button history"
|
||||
to={`/${this.props.mode}/${this.props.cat}/${this.props.building.building_id}/history`}
|
||||
>History</NavLink>
|
||||
<ViewEditControl
|
||||
cat={this.props.cat}
|
||||
mode={this.props.mode}
|
||||
building={this.props.building}
|
||||
/>
|
||||
</>
|
||||
:
|
||||
null
|
||||
}
|
||||
</>
|
||||
: null
|
||||
}
|
||||
</ContainerHeader>
|
||||
<div className="section-body">
|
||||
{
|
||||
this.props.inactive ?
|
||||
|
@ -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';
|
||||
import { CategoryViewProps } from './category-view-props';
|
||||
|
||||
/**
|
||||
@ -22,23 +23,23 @@ const AgeView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
||||
onChange={props.onChange}
|
||||
/>
|
||||
<NumericDataEntry
|
||||
title="Facade year"
|
||||
title={dataFields.facade_year.title}
|
||||
slug="facade_year"
|
||||
value={props.building.facade_year}
|
||||
mode={props.mode}
|
||||
copy={props.copy}
|
||||
onChange={props.onChange}
|
||||
step={1}
|
||||
tooltip="Best estimate"
|
||||
tooltip={dataFields.facade_year.tooltip}
|
||||
/>
|
||||
<SelectDataEntry
|
||||
title="Source of information"
|
||||
title={dataFields.date_source.title}
|
||||
slug="date_source"
|
||||
value={props.building.date_source}
|
||||
mode={props.mode}
|
||||
copy={props.copy}
|
||||
onChange={props.onChange}
|
||||
tooltip="Source for the main start date"
|
||||
tooltip={dataFields.date_source.tooltip}
|
||||
placeholder=""
|
||||
options={[
|
||||
"Survey of London",
|
||||
@ -54,22 +55,22 @@ const AgeView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
||||
]}
|
||||
/>
|
||||
<TextboxDataEntry
|
||||
title="Source details"
|
||||
title={dataFields.date_source_detail.title}
|
||||
slug="date_source_detail"
|
||||
value={props.building.date_source_detail}
|
||||
mode={props.mode}
|
||||
copy={props.copy}
|
||||
onChange={props.onChange}
|
||||
tooltip="References for date source (max 500 characters)"
|
||||
tooltip={dataFields.date_source_detail.tooltip}
|
||||
/>
|
||||
<MultiDataEntry
|
||||
title="Text and Image Links"
|
||||
title={dataFields.date_link.title}
|
||||
slug="date_link"
|
||||
value={props.building.date_link}
|
||||
mode={props.mode}
|
||||
copy={props.copy}
|
||||
onChange={props.onChange}
|
||||
tooltip="URL for age and date reference"
|
||||
tooltip={dataFields.date_link.tooltip}
|
||||
placeholder="https://..."
|
||||
/>
|
||||
</Fragment>
|
||||
|
@ -5,24 +5,25 @@ 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';
|
||||
import { CategoryViewProps } from './category-view-props';
|
||||
|
||||
const LocationView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
||||
<Fragment>
|
||||
<InfoBox msg="Text-based address fields are disabled at the moment. We're looking into how best to collect this data." />
|
||||
<DataEntry
|
||||
title="Building Name"
|
||||
title={dataFields.location_name.title}
|
||||
slug="location_name"
|
||||
value={props.building.location_name}
|
||||
mode={props.mode}
|
||||
copy={props.copy}
|
||||
onChange={props.onChange}
|
||||
tooltip="May not be needed for many buildings."
|
||||
tooltip={dataFields.location_name.tooltip}
|
||||
placeholder="Building name (if any)"
|
||||
disabled={true}
|
||||
/>
|
||||
<NumericDataEntry
|
||||
title="Building number"
|
||||
title={dataFields.location_number.title}
|
||||
slug="location_number"
|
||||
value={props.building.location_number}
|
||||
mode={props.mode}
|
||||
@ -31,7 +32,7 @@ const LocationView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
||||
step={1}
|
||||
/>
|
||||
<DataEntry
|
||||
title="Street"
|
||||
title={dataFields.location_street.title}
|
||||
slug="location_street"
|
||||
value={props.building.location_street}
|
||||
mode={props.mode}
|
||||
@ -40,7 +41,7 @@ const LocationView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
||||
disabled={true}
|
||||
/>
|
||||
<DataEntry
|
||||
title="Address line 2"
|
||||
title={dataFields.location_line_two.title}
|
||||
slug="location_line_two"
|
||||
value={props.building.location_line_two}
|
||||
mode={props.mode}
|
||||
@ -49,7 +50,7 @@ const LocationView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
||||
disabled={true}
|
||||
/>
|
||||
<DataEntry
|
||||
title="Town"
|
||||
title={dataFields.location_town.title}
|
||||
slug="location_town"
|
||||
value={props.building.location_town}
|
||||
mode={props.mode}
|
||||
@ -57,7 +58,7 @@ const LocationView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
||||
onChange={props.onChange}
|
||||
/>
|
||||
<DataEntry
|
||||
title="Postcode"
|
||||
title={dataFields.location_postcode.title}
|
||||
slug="location_postcode"
|
||||
value={props.building.location_postcode}
|
||||
mode={props.mode}
|
||||
@ -67,32 +68,32 @@ const LocationView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
||||
valueTransform={x=>x.toUpperCase()}
|
||||
/>
|
||||
<DataEntry
|
||||
title="TOID"
|
||||
title={dataFields.ref_toid.title}
|
||||
slug="ref_toid"
|
||||
value={props.building.ref_toid}
|
||||
mode={props.mode}
|
||||
copy={props.copy}
|
||||
tooltip="Ordnance Survey Topography Layer ID (to be filled automatically)"
|
||||
tooltip={dataFields.ref_toid.tooltip}
|
||||
onChange={props.onChange}
|
||||
disabled={true}
|
||||
/>
|
||||
<UPRNsDataEntry
|
||||
title="UPRNs"
|
||||
title={dataFields.uprns.title}
|
||||
value={props.building.uprns}
|
||||
tooltip="Unique Property Reference Numbers (to be filled automatically)"
|
||||
tooltip={dataFields.uprns.tooltip}
|
||||
/>
|
||||
<DataEntry
|
||||
title="OSM ID"
|
||||
title={dataFields.ref_osm_id.title}
|
||||
slug="ref_osm_id"
|
||||
value={props.building.ref_osm_id}
|
||||
mode={props.mode}
|
||||
copy={props.copy}
|
||||
tooltip="OpenStreetMap feature ID"
|
||||
tooltip={dataFields.ref_osm_id.tooltip}
|
||||
maxLength={20}
|
||||
onChange={props.onChange}
|
||||
/>
|
||||
<NumericDataEntry
|
||||
title="Latitude"
|
||||
title={dataFields.location_latitude.title}
|
||||
slug="location_latitude"
|
||||
value={props.building.location_latitude}
|
||||
mode={props.mode}
|
||||
@ -102,7 +103,7 @@ const LocationView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
||||
onChange={props.onChange}
|
||||
/>
|
||||
<NumericDataEntry
|
||||
title="Longitude"
|
||||
title={dataFields.location_longitude.title}
|
||||
slug="location_longitude"
|
||||
value={props.building.location_longitude}
|
||||
mode={props.mode}
|
||||
|
@ -5,6 +5,7 @@ import DataEntry from '../data-components/data-entry';
|
||||
import CheckboxDataEntry from '../data-components/checkbox-data-entry';
|
||||
import SelectDataEntry from '../data-components/select-data-entry';
|
||||
import { DataEntryGroup } from '../data-components/data-entry-group';
|
||||
import { dataFields } from '../../data_fields';
|
||||
import { CategoryViewProps } from './category-view-props';
|
||||
|
||||
/**
|
||||
@ -13,7 +14,7 @@ import { CategoryViewProps } from './category-view-props';
|
||||
const PlanningView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
||||
<Fragment>
|
||||
<DataEntry
|
||||
title="Planning portal link"
|
||||
title={dataFields.planning_portal_link.title}
|
||||
slug="planning_portal_link"
|
||||
value={props.building.planning_portal_link}
|
||||
mode={props.mode}
|
||||
@ -22,7 +23,7 @@ const PlanningView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
||||
/>
|
||||
<DataEntryGroup name="Listing and protections" >
|
||||
<CheckboxDataEntry
|
||||
title="In a conservation area?"
|
||||
title={dataFields.planning_in_conservation_area.title}
|
||||
slug="planning_in_conservation_area"
|
||||
value={props.building.planning_in_conservation_area}
|
||||
mode={props.mode}
|
||||
@ -30,7 +31,7 @@ const PlanningView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
||||
onChange={props.onChange}
|
||||
/>
|
||||
<DataEntry
|
||||
title="Conservation area name"
|
||||
title={dataFields.planning_conservation_area_name.title}
|
||||
slug="planning_conservation_area_name"
|
||||
value={props.building.planning_conservation_area_name}
|
||||
mode={props.mode}
|
||||
@ -38,7 +39,7 @@ const PlanningView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
||||
onChange={props.onChange}
|
||||
/>
|
||||
<CheckboxDataEntry
|
||||
title="Is listed on the National Heritage List for England?"
|
||||
title={dataFields.planning_in_list.title}
|
||||
slug="planning_in_list"
|
||||
value={props.building.planning_in_list}
|
||||
mode={props.mode}
|
||||
@ -46,7 +47,7 @@ const PlanningView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
||||
onChange={props.onChange}
|
||||
/>
|
||||
<DataEntry
|
||||
title="National Heritage List for England list id"
|
||||
title={dataFields.planning_list_id.title}
|
||||
slug="planning_list_id"
|
||||
value={props.building.planning_list_id}
|
||||
mode={props.mode}
|
||||
@ -54,7 +55,7 @@ const PlanningView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
||||
onChange={props.onChange}
|
||||
/>
|
||||
<SelectDataEntry
|
||||
title="National Heritage List for England list type"
|
||||
title={dataFields.planning_list_cat.title}
|
||||
slug="planning_list_cat"
|
||||
value={props.building.planning_list_cat}
|
||||
mode={props.mode}
|
||||
@ -69,7 +70,7 @@ const PlanningView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
||||
]}
|
||||
/>
|
||||
<SelectDataEntry
|
||||
title="Listing grade"
|
||||
title={dataFields.planning_list_grade.title}
|
||||
slug="planning_list_grade"
|
||||
value={props.building.planning_list_grade}
|
||||
mode={props.mode}
|
||||
@ -83,7 +84,7 @@ const PlanningView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
||||
]}
|
||||
/>
|
||||
<DataEntry
|
||||
title="Heritage at risk list id"
|
||||
title={dataFields.planning_heritage_at_risk_id.title}
|
||||
slug="planning_heritage_at_risk_id"
|
||||
value={props.building.planning_heritage_at_risk_id}
|
||||
mode={props.mode}
|
||||
@ -91,7 +92,7 @@ const PlanningView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
||||
onChange={props.onChange}
|
||||
/>
|
||||
<DataEntry
|
||||
title="World heritage list id"
|
||||
title={dataFields.planning_world_list_id.title}
|
||||
slug="planning_world_list_id"
|
||||
value={props.building.planning_world_list_id}
|
||||
mode={props.mode}
|
||||
@ -99,7 +100,7 @@ const PlanningView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
||||
onChange={props.onChange}
|
||||
/>
|
||||
<CheckboxDataEntry
|
||||
title="In the Greater London Historic Environment Record?"
|
||||
title={dataFields.planning_in_glher.title}
|
||||
slug="planning_in_glher"
|
||||
value={props.building.planning_in_glher}
|
||||
mode={props.mode}
|
||||
@ -107,7 +108,7 @@ const PlanningView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
||||
onChange={props.onChange}
|
||||
/>
|
||||
<DataEntry
|
||||
title="Greater London Historic Environment Record link"
|
||||
title={dataFields.planning_glher_url.title}
|
||||
slug="planning_glher_url"
|
||||
value={props.building.planning_glher_url}
|
||||
mode={props.mode}
|
||||
@ -115,7 +116,7 @@ const PlanningView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
||||
onChange={props.onChange}
|
||||
/>
|
||||
<CheckboxDataEntry
|
||||
title="In an Architectural Priority Area?"
|
||||
title={dataFields.planning_in_apa.title}
|
||||
slug="planning_in_apa"
|
||||
value={props.building.planning_in_apa}
|
||||
mode={props.mode}
|
||||
@ -123,7 +124,7 @@ const PlanningView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
||||
onChange={props.onChange}
|
||||
/>
|
||||
<DataEntry
|
||||
title="Architectural Priority Area name"
|
||||
title={dataFields.planning_apa_name.title}
|
||||
slug="planning_apa_name"
|
||||
value={props.building.planning_apa_name}
|
||||
mode={props.mode}
|
||||
@ -131,7 +132,7 @@ const PlanningView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
||||
onChange={props.onChange}
|
||||
/>
|
||||
<DataEntry
|
||||
title="Architectural Priority Area tier"
|
||||
title={dataFields.planning_apa_tier.title}
|
||||
slug="planning_apa_tier"
|
||||
value={props.building.planning_apa_tier}
|
||||
mode={props.mode}
|
||||
@ -139,7 +140,7 @@ const PlanningView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
||||
onChange={props.onChange}
|
||||
/>
|
||||
<CheckboxDataEntry
|
||||
title="Is locally listed?"
|
||||
title={dataFields.planning_in_local_list.title}
|
||||
slug="planning_in_local_list"
|
||||
value={props.building.planning_in_local_list}
|
||||
mode={props.mode}
|
||||
@ -147,7 +148,7 @@ const PlanningView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
||||
onChange={props.onChange}
|
||||
/>
|
||||
<DataEntry
|
||||
title="Local list link"
|
||||
title={dataFields.planning_local_list_url.title}
|
||||
slug="planning_local_list_url"
|
||||
value={props.building.planning_local_list_url}
|
||||
mode={props.mode}
|
||||
@ -155,7 +156,7 @@ const PlanningView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
||||
onChange={props.onChange}
|
||||
/>
|
||||
<CheckboxDataEntry
|
||||
title="Within a historic area assessment?"
|
||||
title={dataFields.planning_in_historic_area_assessment.title}
|
||||
slug="planning_in_historic_area_assessment"
|
||||
value={props.building.planning_in_historic_area_assessment}
|
||||
mode={props.mode}
|
||||
@ -163,7 +164,7 @@ const PlanningView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
||||
onChange={props.onChange}
|
||||
/>
|
||||
<DataEntry
|
||||
title="Historic area assessment link"
|
||||
title={dataFields.planning_historic_area_assessment_url.title}
|
||||
slug="planning_historic_area_assessment_url"
|
||||
value={props.building.planning_historic_area_assessment_url}
|
||||
mode={props.mode}
|
||||
@ -173,7 +174,7 @@ const PlanningView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
||||
</DataEntryGroup>
|
||||
<DataEntryGroup name="Demolition and demolition history">
|
||||
<CheckboxDataEntry
|
||||
title="Is the building proposed for demolition?"
|
||||
title={dataFields.planning_demolition_proposed.title}
|
||||
slug="planning_demolition_proposed"
|
||||
value={props.building.planning_demolition_proposed}
|
||||
mode={props.mode}
|
||||
@ -182,7 +183,7 @@ const PlanningView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
||||
disabled={true}
|
||||
/>
|
||||
<CheckboxDataEntry
|
||||
title="Has the building been demolished?"
|
||||
title={dataFields.planning_demolition_complete.title}
|
||||
slug="planning_demolition_complete"
|
||||
value={props.building.planning_demolition_complete}
|
||||
mode={props.mode}
|
||||
@ -191,7 +192,7 @@ const PlanningView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
||||
disabled={true}
|
||||
/>
|
||||
<DataEntry
|
||||
title="Dates of construction and demolition of previous buildings on site"
|
||||
title={dataFields.planning_demolition_history.title}
|
||||
slug="planning_demolition_history"
|
||||
value={props.building.planning_demolition_history}
|
||||
mode={props.mode}
|
||||
|
@ -4,6 +4,7 @@ import withCopyEdit from '../data-container';
|
||||
import NumericDataEntry from '../data-components/numeric-data-entry';
|
||||
import SelectDataEntry from '../data-components/select-data-entry';
|
||||
import { DataEntryGroup } from '../data-components/data-entry-group';
|
||||
import { dataFields } from '../../data_fields';
|
||||
import { CategoryViewProps } from './category-view-props';
|
||||
|
||||
/**
|
||||
@ -14,39 +15,39 @@ const SizeView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
||||
<DataEntryGroup name="Storeys" collapsed={false}>
|
||||
|
||||
<NumericDataEntry
|
||||
title="Core storeys"
|
||||
title={dataFields.size_storeys_core.title}
|
||||
slug="size_storeys_core"
|
||||
value={props.building.size_storeys_core}
|
||||
mode={props.mode}
|
||||
copy={props.copy}
|
||||
tooltip="How many storeys between the pavement and start of roof?"
|
||||
tooltip={dataFields.size_storeys_core.tooltip}
|
||||
onChange={props.onChange}
|
||||
step={1}
|
||||
/>
|
||||
<NumericDataEntry
|
||||
title="Attic storeys"
|
||||
title={dataFields.size_storeys_attic.title}
|
||||
slug="size_storeys_attic"
|
||||
value={props.building.size_storeys_attic}
|
||||
mode={props.mode}
|
||||
copy={props.copy}
|
||||
tooltip="How many storeys above start of roof?"
|
||||
tooltip={dataFields.size_storeys_attic.tooltip}
|
||||
onChange={props.onChange}
|
||||
step={1}
|
||||
/>
|
||||
<NumericDataEntry
|
||||
title="Basement storeys"
|
||||
title={dataFields.size_storeys_basement.title}
|
||||
slug="size_storeys_basement"
|
||||
value={props.building.size_storeys_basement}
|
||||
mode={props.mode}
|
||||
copy={props.copy}
|
||||
tooltip="How many storeys below pavement level?"
|
||||
tooltip={dataFields.size_storeys_basement.tooltip}
|
||||
onChange={props.onChange}
|
||||
step={1}
|
||||
/>
|
||||
</DataEntryGroup>
|
||||
<DataEntryGroup name="Height">
|
||||
<NumericDataEntry
|
||||
title="Height to apex (m)"
|
||||
title={dataFields.size_height_apex.title}
|
||||
slug="size_height_apex"
|
||||
value={props.building.size_height_apex}
|
||||
mode={props.mode}
|
||||
@ -55,7 +56,7 @@ const SizeView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
||||
step={0.1}
|
||||
/>
|
||||
<NumericDataEntry
|
||||
title="Height to eaves (m)"
|
||||
title={dataFields.size_height_eaves.title}
|
||||
slug="size_height_eaves"
|
||||
disabled={true}
|
||||
value={props.building.size_height_eaves}
|
||||
@ -67,7 +68,7 @@ const SizeView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
||||
</DataEntryGroup>
|
||||
<DataEntryGroup name="Floor area">
|
||||
<NumericDataEntry
|
||||
title="Ground floor area (m²)"
|
||||
title={dataFields.size_floor_area_ground.title}
|
||||
slug="size_floor_area_ground"
|
||||
value={props.building.size_floor_area_ground}
|
||||
mode={props.mode}
|
||||
@ -76,7 +77,7 @@ const SizeView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
||||
step={0.1}
|
||||
/>
|
||||
<NumericDataEntry
|
||||
title="Total floor area (m²)"
|
||||
title={dataFields.size_floor_area_total.title}
|
||||
slug="size_floor_area_total"
|
||||
value={props.building.size_floor_area_total}
|
||||
mode={props.mode}
|
||||
@ -86,7 +87,7 @@ const SizeView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
||||
/>
|
||||
</DataEntryGroup>
|
||||
<NumericDataEntry
|
||||
title="Frontage Width (m)"
|
||||
title={dataFields.size_width_frontage.title}
|
||||
slug="size_width_frontage"
|
||||
value={props.building.size_width_frontage}
|
||||
mode={props.mode}
|
||||
@ -95,7 +96,7 @@ const SizeView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
||||
step={0.1}
|
||||
/>
|
||||
<NumericDataEntry
|
||||
title="Total area of plot (m²)"
|
||||
title={dataFields.size_plot_area_total.title}
|
||||
slug="size_plot_area_total"
|
||||
value={props.building.size_plot_area_total}
|
||||
mode={props.mode}
|
||||
@ -105,7 +106,7 @@ const SizeView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
||||
disabled={true}
|
||||
/>
|
||||
<NumericDataEntry
|
||||
title="FAR ratio (percentage of plot covered by building)"
|
||||
title={dataFields.size_far_ratio.title}
|
||||
slug="size_far_ratio"
|
||||
value={props.building.size_far_ratio}
|
||||
mode={props.mode}
|
||||
@ -115,7 +116,23 @@ const SizeView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
||||
disabled={true}
|
||||
/>
|
||||
<SelectDataEntry
|
||||
title="Roof shape"
|
||||
title={dataFields.size_configuration.title}
|
||||
slug="size_configuration"
|
||||
value={props.building.size_configuration}
|
||||
mode={props.mode}
|
||||
copy={props.copy}
|
||||
onChange={props.onChange}
|
||||
disabled={true}
|
||||
options={[
|
||||
"Detached",
|
||||
"Semi-detached",
|
||||
"Terrace",
|
||||
"End terrace",
|
||||
"Block"
|
||||
]}
|
||||
/>
|
||||
<SelectDataEntry
|
||||
title={dataFields.size_roof_shape.title}
|
||||
slug="size_roof_shape"
|
||||
value={props.building.size_roof_shape}
|
||||
mode={props.mode}
|
||||
|
@ -4,6 +4,7 @@ import withCopyEdit from '../data-container';
|
||||
import DataEntry from '../data-components/data-entry';
|
||||
import SelectDataEntry from '../data-components/select-data-entry';
|
||||
import NumericDataEntry from '../data-components/numeric-data-entry';
|
||||
import { dataFields } from '../../data_fields';
|
||||
import { CategoryViewProps } from './category-view-props';
|
||||
|
||||
const EnergyCategoryOptions = ["A", "B", "C", "D", "E", "F", "G"];
|
||||
@ -22,30 +23,30 @@ const SustainabilityView: React.FunctionComponent<CategoryViewProps> = (props) =
|
||||
return (
|
||||
<Fragment>
|
||||
<SelectDataEntry
|
||||
title="BREEAM Rating"
|
||||
title={dataFields.sust_breeam_rating.title}
|
||||
slug="sust_breeam_rating"
|
||||
value={props.building.sust_breeam_rating}
|
||||
tooltip="(Building Research Establishment Environmental Assessment Method) May not be present for many buildings"
|
||||
tooltip={dataFields.sust_breeam_rating.tooltip}
|
||||
options={BreeamRatingOptions}
|
||||
mode={props.mode}
|
||||
copy={props.copy}
|
||||
onChange={props.onChange}
|
||||
/>
|
||||
<SelectDataEntry
|
||||
title="DEC Rating"
|
||||
title={dataFields.sust_dec.title}
|
||||
slug="sust_dec"
|
||||
value={props.building.sust_dec}
|
||||
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"
|
||||
tooltip={dataFields.sust_dec.tooltip}
|
||||
options={EnergyCategoryOptions}
|
||||
mode={props.mode}
|
||||
copy={props.copy}
|
||||
onChange={props.onChange}
|
||||
/>
|
||||
<SelectDataEntry
|
||||
title="EPC Rating"
|
||||
title={dataFields.sust_aggregate_estimate_epc.title}
|
||||
slug="sust_aggregate_estimate_epc"
|
||||
value={props.building.sust_aggregate_estimate_epc}
|
||||
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"
|
||||
tooltip={dataFields.sust_aggregate_estimate_epc.tooltip}
|
||||
options={EnergyCategoryOptions}
|
||||
disabled={true}
|
||||
mode={props.mode}
|
||||
@ -53,10 +54,10 @@ const SustainabilityView: React.FunctionComponent<CategoryViewProps> = (props) =
|
||||
onChange={props.onChange}
|
||||
/>
|
||||
<NumericDataEntry
|
||||
title="Last significant retrofit"
|
||||
title={dataFields.sust_retrofit_date.title}
|
||||
slug="sust_retrofit_date"
|
||||
value={props.building.sust_retrofit_date}
|
||||
tooltip="Date of last major building refurbishment"
|
||||
tooltip={dataFields.sust_retrofit_date.tooltip}
|
||||
step={1}
|
||||
min={1086}
|
||||
max={new Date().getFullYear()}
|
||||
@ -65,7 +66,7 @@ const SustainabilityView: React.FunctionComponent<CategoryViewProps> = (props) =
|
||||
onChange={props.onChange}
|
||||
/>
|
||||
<NumericDataEntry
|
||||
title="Expected lifespan for typology"
|
||||
title={dataFields.sust_life_expectancy.title}
|
||||
slug="sust_life_expectancy"
|
||||
value={props.building.sust_life_expectancy}
|
||||
step={1}
|
||||
|
@ -4,6 +4,7 @@ import withCopyEdit from '../data-container';
|
||||
import SelectDataEntry from '../data-components/select-data-entry';
|
||||
import NumericDataEntry from '../data-components/numeric-data-entry';
|
||||
import DataEntry from '../data-components/data-entry';
|
||||
import { dataFields } from '../../data_fields';
|
||||
import { CategoryViewProps } from './category-view-props';
|
||||
|
||||
const AttachmentFormOptions = [
|
||||
@ -20,20 +21,20 @@ const TypeView: React.FunctionComponent<CategoryViewProps> = (props) => {
|
||||
return (
|
||||
<Fragment>
|
||||
<SelectDataEntry
|
||||
title="Building configuration (attachment)?"
|
||||
title={dataFields.building_attachment_form.title}
|
||||
slug="building_attachment_form"
|
||||
value={props.building.building_attachment_form}
|
||||
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)"
|
||||
tooltip={dataFields.building_attachment_form.tooltip}
|
||||
options={AttachmentFormOptions}
|
||||
mode={props.mode}
|
||||
copy={props.copy}
|
||||
onChange={props.onChange}
|
||||
/>
|
||||
<NumericDataEntry
|
||||
title="When did use change?"
|
||||
title={dataFields.date_change_building_use.title}
|
||||
slug="date_change_building_use"
|
||||
value={props.building.date_change_building_use}
|
||||
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"
|
||||
tooltip={dataFields.date_change_building_use.tooltip}
|
||||
min={1086}
|
||||
max={new Date().getFullYear()}
|
||||
step={1}
|
||||
@ -42,9 +43,9 @@ const TypeView: React.FunctionComponent<CategoryViewProps> = (props) => {
|
||||
onChange={props.onChange}
|
||||
/>
|
||||
<DataEntry
|
||||
title="Original building use"
|
||||
slug=""
|
||||
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"
|
||||
title={dataFields.original_building_use.title}
|
||||
slug="original_building_use" // doesn't exist in database yet
|
||||
tooltip={dataFields.original_building_use.tooltip}
|
||||
value={undefined}
|
||||
copy={props.copy}
|
||||
mode={props.mode}
|
||||
|
@ -0,0 +1,14 @@
|
||||
.edit-history-entry {
|
||||
border-bottom: 1px solid black;
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
|
||||
.edit-history-timestamp {
|
||||
font-size: 0.9em;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.edit-history-username {
|
||||
font-size: 0.9em;
|
||||
}
|
@ -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<BuildingEditSummaryProps> = 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 (
|
||||
<div className="edit-history-entry">
|
||||
<h2 className="edit-history-timestamp">Edited on {formatDate(parseDate(props.historyEntry.date_trunc))}</h2>
|
||||
<h3 className="edit-history-username">By {props.historyEntry.username}</h3>
|
||||
{
|
||||
Object.entries(entriesByCategory).map(([category, fields]) => <CategoryEditSummary category={category} fields={fields} />)
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
BuildingEditSummary
|
||||
};
|
@ -0,0 +1,28 @@
|
||||
.edit-history-category-summary {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.edit-history-category-summary ul {
|
||||
list-style: none;
|
||||
padding-left: 0.5em;
|
||||
}
|
||||
|
||||
.edit-history-category-title {
|
||||
font-size: 0.9em;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
@ -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<CategoryEditSummaryProps> = props => (
|
||||
<div className='edit-history-category-summary'>
|
||||
<h3 className='edit-history-category-title'>{props.category}:</h3>
|
||||
<ul>
|
||||
{
|
||||
props.fields.map(x =>
|
||||
<li key={x.title}>
|
||||
<FieldEditSummary title={x.title} value={x.value} oldValue={x.oldValue} />
|
||||
</li>)
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
|
||||
export {
|
||||
CategoryEditSummary
|
||||
};
|
8
app/src/frontend/building/edit-history/edit-history.css
Normal file
8
app/src/frontend/building/edit-history/edit-history.css
Normal file
@ -0,0 +1,8 @@
|
||||
.edit-history {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.edit-history-list {
|
||||
list-style: none;
|
||||
padding-left: 1rem;
|
||||
}
|
46
app/src/frontend/building/edit-history/edit-history.tsx
Normal file
46
app/src/frontend/building/edit-history/edit-history.tsx
Normal file
@ -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<EditHistoryProps> = (props) => {
|
||||
const [history, setHistory] = useState<EditHistoryEntry[]>(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 (
|
||||
<>
|
||||
<ContainerHeader title="Edit history" backLink='.' cat='edit-history' />
|
||||
|
||||
<ul className="edit-history-list">
|
||||
{history && history.map(entry => (
|
||||
<li key={`${entry.revision_id}`} className="edit-history-list-element">
|
||||
<BuildingEditSummary historyEntry={entry} />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
EditHistory
|
||||
};
|
@ -0,0 +1,20 @@
|
||||
import React from 'react';
|
||||
|
||||
interface FieldEditSummaryProps {
|
||||
title: string;
|
||||
value: any;
|
||||
oldValue: any;
|
||||
}
|
||||
|
||||
const FieldEditSummary: React.FunctionComponent<FieldEditSummaryProps> = props => (
|
||||
<>
|
||||
{props.title}:
|
||||
<code title="Value before edit" className='edit-history-diff old'>{props.oldValue}</code>
|
||||
|
||||
<code title="Value after edit" className='edit-history-diff new'>{props.value}</code>
|
||||
</>
|
||||
);
|
||||
|
||||
export {
|
||||
FieldEditSummary
|
||||
};
|
35
app/src/frontend/building/header-buttons/copy-control.tsx
Normal file
35
app/src/frontend/building/header-buttons/copy-control.tsx
Normal file
@ -0,0 +1,35 @@
|
||||
import React from 'react';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
|
||||
interface CopyControlProps {
|
||||
cat: string;
|
||||
data_string: string;
|
||||
copying: boolean;
|
||||
toggleCopying: () => void;
|
||||
}
|
||||
|
||||
const CopyControl: React.FC<CopyControlProps> = props => (
|
||||
props.copying ?
|
||||
<>
|
||||
<NavLink
|
||||
to={`/multi-edit/${props.cat}?data=${props.data_string}`}
|
||||
className="icon-button copy">
|
||||
Copy selected
|
||||
</NavLink>
|
||||
<a
|
||||
className="icon-button copy"
|
||||
onClick={props.toggleCopying}>
|
||||
Cancel
|
||||
</a>
|
||||
</>
|
||||
:
|
||||
<a
|
||||
className="icon-button copy"
|
||||
onClick={props.toggleCopying}>
|
||||
Copy
|
||||
</a>
|
||||
);
|
||||
|
||||
export {
|
||||
CopyControl
|
||||
};
|
@ -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<ViewEditControlProps> = props => (
|
||||
(props.mode === 'edit')?
|
||||
<NavLink
|
||||
className="icon-button view"
|
||||
title="View data"
|
||||
to={`/view/${props.cat}/${props.building.building_id}`}>
|
||||
View
|
||||
<ViewIcon />
|
||||
</NavLink>
|
||||
: <NavLink
|
||||
className="icon-button edit"
|
||||
title="Edit data"
|
||||
to={`/edit/${props.cat}/${props.building.building_id}`}>
|
||||
Edit
|
||||
<EditIcon />
|
||||
</NavLink>
|
||||
);
|
||||
|
||||
export {
|
||||
ViewEditControl
|
||||
};
|
7
app/src/frontend/building/models/edit-history-entry.ts
Normal file
7
app/src/frontend/building/models/edit-history-entry.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export interface EditHistoryEntry {
|
||||
date_trunc: string;
|
||||
username: string;
|
||||
revision_id: string;
|
||||
forward_patch: object;
|
||||
reverse_patch: object;
|
||||
}
|
@ -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) => {
|
||||
<InfoBox msg='Click buildings one at a time to colour using the data below' />
|
||||
{
|
||||
Object.keys(data).map((key => {
|
||||
const info = dataFields[key] || {};
|
||||
return (
|
||||
<DataEntry
|
||||
title={key}
|
||||
title={info.title || `Unknown field (${key})`}
|
||||
slug={key}
|
||||
disabled={true}
|
||||
value={data[key]}
|
||||
|
@ -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;
|
||||
@ -107,7 +100,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 {
|
||||
|
344
app/src/frontend/data_fields.ts
Normal file
344
app/src/frontend/data_fields.ts
Normal file
@ -0,0 +1,344 @@
|
||||
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 = {
|
||||
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)"
|
||||
},
|
||||
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",
|
||||
},
|
||||
|
||||
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"
|
||||
}
|
||||
|
||||
};
|
@ -36,6 +36,32 @@ 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<T>(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 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$/)
|
||||
.splice(1)
|
||||
.map(x => parseInt(x, 10));
|
||||
return new Date(Date.UTC(year, month-1, day, hour, minute, second, millisecond));
|
||||
}
|
||||
|
||||
function compareObjects(objA: object, objB: object): [object, object] {
|
||||
const reverse = {}
|
||||
const forward = {}
|
||||
@ -48,4 +74,9 @@ function compareObjects(objA: object, objB: object): [object, object] {
|
||||
return [forward, reverse];
|
||||
}
|
||||
|
||||
export { sanitiseURL, compareObjects }
|
||||
export {
|
||||
sanitiseURL,
|
||||
arrayToDictionary,
|
||||
parseDate,
|
||||
compareObjects
|
||||
};
|
||||
|
@ -9,6 +9,7 @@ import MultiEdit from './building/multi-edit';
|
||||
import BuildingView from './building/building-view';
|
||||
import ColouringMap from './map/map';
|
||||
import { parse } from 'query-string';
|
||||
import { EditHistory } from './building/edit-history/edit-history';
|
||||
import { Building } from './models/building';
|
||||
|
||||
interface MapAppRouteParams {
|
||||
@ -220,6 +221,7 @@ class MapApp extends React.Component<MapAppProps, MapAppState> {
|
||||
|
||||
render() {
|
||||
const mode = this.props.match.params.mode;
|
||||
const viewEditMode = mode === 'multi-edit' ? undefined : mode;
|
||||
|
||||
let category = this.state.category || 'age';
|
||||
|
||||
@ -245,7 +247,7 @@ class MapApp extends React.Component<MapAppProps, MapAppState> {
|
||||
<Route exact path="/:mode/:cat/:building?">
|
||||
<Sidebar>
|
||||
<BuildingView
|
||||
mode={mode}
|
||||
mode={viewEditMode}
|
||||
cat={category}
|
||||
building={this.state.building}
|
||||
building_like={this.state.building_like}
|
||||
@ -254,6 +256,11 @@ class MapApp extends React.Component<MapAppProps, MapAppState> {
|
||||
/>
|
||||
</Sidebar>
|
||||
</Route>
|
||||
<Route exact path="/:mode/:cat/:building/history">
|
||||
<Sidebar>
|
||||
<EditHistory building={this.state.building} />
|
||||
</Sidebar>
|
||||
</Route>
|
||||
<Route exact path="/:mode(view|edit|multi-edit)"
|
||||
render={props => (<Redirect to={`/${props.match.params.mode}/categories`} />)}
|
||||
/>
|
||||
|
@ -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) {
|
||||
|
Loading…
Reference in New Issue
Block a user