Merge pull request #472 from mz8i/feature/data-container-state
Feature: store data edits in data container state
This commit is contained in:
commit
a5f2df68f1
@ -14,18 +14,28 @@ import StreetscapeContainer from './data-containers/streetscape';
|
||||
import CommunityContainer from './data-containers/community';
|
||||
import PlanningContainer from './data-containers/planning';
|
||||
import LikeContainer from './data-containers/like';
|
||||
import { Building } from '../models/building';
|
||||
|
||||
|
||||
interface BuildingViewProps {
|
||||
cat: string;
|
||||
mode: 'view' | 'edit' | 'multi-edit';
|
||||
building: Building;
|
||||
building_like: boolean;
|
||||
user: any;
|
||||
selectBuilding: (building: Building) => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Top-level container for building view/edit form
|
||||
*
|
||||
* @param props
|
||||
*/
|
||||
const BuildingView = (props) => {
|
||||
const BuildingView: React.FunctionComponent<BuildingViewProps> = (props) => {
|
||||
switch (props.cat) {
|
||||
case 'location':
|
||||
return <LocationContainer
|
||||
{...props}
|
||||
key={props.building && props.building.building_id}
|
||||
title="Location"
|
||||
help="https://pages.colouring.london/location"
|
||||
intro="Where are the buildings? Address, location and cross-references."
|
||||
@ -33,7 +43,6 @@ const BuildingView = (props) => {
|
||||
case 'use':
|
||||
return <UseContainer
|
||||
{...props}
|
||||
key={props.building && props.building.building_id}
|
||||
inactive={true}
|
||||
title="Land Use"
|
||||
intro="How are buildings used, and how does use change over time? Coming soon…"
|
||||
@ -42,7 +51,6 @@ const BuildingView = (props) => {
|
||||
case 'type':
|
||||
return <TypeContainer
|
||||
{...props}
|
||||
key={props.building && props.building.building_id}
|
||||
inactive={false}
|
||||
title="Type"
|
||||
intro="How were buildings previously used?"
|
||||
@ -51,7 +59,6 @@ const BuildingView = (props) => {
|
||||
case 'age':
|
||||
return <AgeContainer
|
||||
{...props}
|
||||
key={props.building && props.building.building_id}
|
||||
title="Age"
|
||||
help="https://pages.colouring.london/age"
|
||||
intro="Building age data can support energy analysis and help predict long-term change."
|
||||
@ -59,7 +66,6 @@ const BuildingView = (props) => {
|
||||
case 'size':
|
||||
return <SizeContainer
|
||||
{...props}
|
||||
key={props.building && props.building.building_id}
|
||||
title="Size & Shape"
|
||||
intro="How big are buildings?"
|
||||
help="https://pages.colouring.london/shapeandsize"
|
||||
@ -67,7 +73,6 @@ const BuildingView = (props) => {
|
||||
case 'construction':
|
||||
return <ConstructionContainer
|
||||
{...props}
|
||||
key={props.building && props.building.building_id}
|
||||
title="Construction"
|
||||
intro="How are buildings built? Coming soon…"
|
||||
help="https://pages.colouring.london/construction"
|
||||
@ -76,7 +81,6 @@ const BuildingView = (props) => {
|
||||
case 'team':
|
||||
return <TeamContainer
|
||||
{...props}
|
||||
key={props.building && props.building.building_id}
|
||||
title="Team"
|
||||
intro="Who built the buildings? Coming soon…"
|
||||
help="https://pages.colouring.london/team"
|
||||
@ -85,7 +89,6 @@ const BuildingView = (props) => {
|
||||
case 'sustainability':
|
||||
return <SustainabilityContainer
|
||||
{...props}
|
||||
key={props.building && props.building.building_id}
|
||||
title="Sustainability"
|
||||
intro="Are buildings energy efficient?"
|
||||
help="https://pages.colouring.london/sustainability"
|
||||
@ -94,7 +97,6 @@ const BuildingView = (props) => {
|
||||
case 'streetscape':
|
||||
return <StreetscapeContainer
|
||||
{...props}
|
||||
key={props.building && props.building.building_id}
|
||||
title="Streetscape"
|
||||
intro="What's the building's context? Coming soon…"
|
||||
help="https://pages.colouring.london/streetscape"
|
||||
@ -103,7 +105,6 @@ const BuildingView = (props) => {
|
||||
case 'community':
|
||||
return <CommunityContainer
|
||||
{...props}
|
||||
key={props.building && props.building.building_id}
|
||||
title="Community"
|
||||
intro="How does this building work for the local community?"
|
||||
help="https://pages.colouring.london/community"
|
||||
@ -112,7 +113,6 @@ const BuildingView = (props) => {
|
||||
case 'planning':
|
||||
return <PlanningContainer
|
||||
{...props}
|
||||
key={props.building && props.building.building_id}
|
||||
title="Planning"
|
||||
intro="Planning controls relating to protection and reuse."
|
||||
help="https://pages.colouring.london/planning"
|
||||
@ -120,7 +120,6 @@ const BuildingView = (props) => {
|
||||
case 'like':
|
||||
return <LikeContainer
|
||||
{...props}
|
||||
key={props.building && props.building.building_id}
|
||||
title="Like Me!"
|
||||
intro="Do you like the building and think it contributes to the city?"
|
||||
help="https://pages.colouring.london/likeme"
|
||||
|
@ -3,8 +3,18 @@ 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;
|
||||
title: string;
|
||||
copy: any;
|
||||
inactive?: boolean;
|
||||
data_string: string;
|
||||
help: string;
|
||||
}
|
||||
|
||||
const ContainerHeader: React.FunctionComponent<any> = (props) => (
|
||||
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}` : ''}`}>
|
||||
<BackIcon />
|
||||
|
@ -2,8 +2,14 @@ import React, { Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { DataTitleCopyable } from './data-title';
|
||||
import { BaseDataEntryProps } from './data-entry';
|
||||
|
||||
const DataEntry: React.FunctionComponent<any> = (props) => { // TODO: remove any
|
||||
interface CheckboxDataEntryProps extends BaseDataEntryProps {
|
||||
value: boolean;
|
||||
}
|
||||
|
||||
|
||||
const CheckboxDataEntry: React.FunctionComponent<CheckboxDataEntryProps> = (props) => {
|
||||
return (
|
||||
<Fragment>
|
||||
<DataTitleCopyable
|
||||
@ -19,7 +25,7 @@ const DataEntry: React.FunctionComponent<any> = (props) => { // TODO: remove any
|
||||
name={props.slug}
|
||||
checked={!!props.value}
|
||||
disabled={props.mode === 'view' || props.disabled}
|
||||
onChange={props.onChange}
|
||||
onChange={e => props.onChange(props.slug, e.target.checked)}
|
||||
/>
|
||||
<label
|
||||
htmlFor={props.slug}
|
||||
@ -31,14 +37,12 @@ const DataEntry: React.FunctionComponent<any> = (props) => { // TODO: remove any
|
||||
);
|
||||
}
|
||||
|
||||
DataEntry.propTypes = {
|
||||
CheckboxDataEntry.propTypes = {
|
||||
title: PropTypes.string,
|
||||
slug: PropTypes.string,
|
||||
tooltip: PropTypes.string,
|
||||
disabled: PropTypes.bool,
|
||||
value: PropTypes.any,
|
||||
placeholder: PropTypes.string,
|
||||
maxLength: PropTypes.number,
|
||||
onChange: PropTypes.func,
|
||||
copy: PropTypes.shape({
|
||||
copying: PropTypes.bool,
|
||||
@ -47,4 +51,4 @@ DataEntry.propTypes = {
|
||||
})
|
||||
}
|
||||
|
||||
export default DataEntry;
|
||||
export default CheckboxDataEntry;
|
||||
|
@ -3,7 +3,24 @@ import PropTypes from 'prop-types';
|
||||
|
||||
import { DataTitleCopyable } from './data-title';
|
||||
|
||||
const DataEntry: React.FunctionComponent<any> = (props) => { // TODO: remove any
|
||||
interface BaseDataEntryProps {
|
||||
slug: string;
|
||||
title: string;
|
||||
tooltip?: string;
|
||||
disabled?: boolean;
|
||||
copy?: any; // CopyProps clashes with propTypes
|
||||
mode?: 'view' | 'edit' | 'multi-edit';
|
||||
onChange?: (key: string, value: any) => void;
|
||||
}
|
||||
|
||||
interface DataEntryProps extends BaseDataEntryProps {
|
||||
value: string;
|
||||
maxLength?: number;
|
||||
placeholder?: string;
|
||||
valueTransform?: (string) => string
|
||||
}
|
||||
|
||||
const DataEntry: React.FunctionComponent<DataEntryProps> = (props) => {
|
||||
return (
|
||||
<Fragment>
|
||||
<DataTitleCopyable
|
||||
@ -20,7 +37,13 @@ const DataEntry: React.FunctionComponent<any> = (props) => { // TODO: remove any
|
||||
maxLength={props.maxLength}
|
||||
disabled={props.mode === 'view' || props.disabled}
|
||||
placeholder={props.placeholder}
|
||||
onChange={props.onChange}
|
||||
onChange={e => {
|
||||
const transform = props.valueTransform || (x => x);
|
||||
const val = e.target.value === '' ?
|
||||
null :
|
||||
transform(e.target.value);
|
||||
props.onChange(props.slug, val);
|
||||
}}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
@ -43,3 +66,6 @@ DataEntry.propTypes = {
|
||||
}
|
||||
|
||||
export default DataEntry;
|
||||
export {
|
||||
BaseDataEntryProps
|
||||
};
|
||||
|
@ -3,7 +3,13 @@ import PropTypes from 'prop-types';
|
||||
|
||||
import Tooltip from '../../components/tooltip';
|
||||
|
||||
const DataTitle: React.FunctionComponent<any> = (props) => {
|
||||
|
||||
interface DataTitleProps {
|
||||
title: string;
|
||||
tooltip: string;
|
||||
}
|
||||
|
||||
const DataTitle: React.FunctionComponent<DataTitleProps> = (props) => {
|
||||
return (
|
||||
<dt>
|
||||
{ props.title }
|
||||
@ -17,7 +23,16 @@ DataTitle.propTypes = {
|
||||
tooltip: PropTypes.string
|
||||
}
|
||||
|
||||
const DataTitleCopyable: React.FunctionComponent<any> = (props) => { // TODO: remove any
|
||||
|
||||
interface DataTitleCopyableProps {
|
||||
title: string;
|
||||
tooltip: string;
|
||||
slug: string;
|
||||
disabled?: boolean;
|
||||
copy?: any; // TODO: type should be CopyProps, but that clashes with propTypes in some obscure way
|
||||
}
|
||||
|
||||
const DataTitleCopyable: React.FunctionComponent<DataTitleCopyableProps> = (props) => {
|
||||
return (
|
||||
<div className="data-title">
|
||||
{ props.tooltip? <Tooltip text={ props.tooltip } /> : null }
|
||||
@ -48,7 +63,8 @@ DataTitleCopyable.propTypes = {
|
||||
copy: PropTypes.shape({
|
||||
copying: PropTypes.bool,
|
||||
copyingKey: PropTypes.func,
|
||||
toggleCopyAttribute: PropTypes.func
|
||||
toggleCopyAttribute: PropTypes.func,
|
||||
toggleCopying: PropTypes.func
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -4,7 +4,15 @@ import PropTypes from 'prop-types';
|
||||
|
||||
import Tooltip from '../../components/tooltip';
|
||||
|
||||
const LikeDataEntry: React.FunctionComponent<any> = (props) => { // TODO: remove any
|
||||
|
||||
interface LikeDataEntryProps {
|
||||
mode: 'view' | 'edit' | 'multi-edit';
|
||||
userLike: boolean;
|
||||
totalLikes: number;
|
||||
onLike: (userLike: boolean) => void;
|
||||
}
|
||||
|
||||
const LikeDataEntry: React.FunctionComponent<LikeDataEntryProps> = (props) => {
|
||||
const data_string = JSON.stringify({like: true});
|
||||
return (
|
||||
<Fragment>
|
||||
@ -21,20 +29,20 @@ const LikeDataEntry: React.FunctionComponent<any> = (props) => { // TODO: remove
|
||||
</div>
|
||||
<p>
|
||||
{
|
||||
(props.value != null)?
|
||||
(props.value === 1)?
|
||||
`${props.value} person likes this building`
|
||||
: `${props.value} people like this building`
|
||||
(props.totalLikes != null)?
|
||||
(props.totalLikes === 1)?
|
||||
`${props.totalLikes} person likes this building`
|
||||
: `${props.totalLikes} people like this building`
|
||||
: "0 people like this building so far - you could be the first!"
|
||||
}
|
||||
</p>
|
||||
<input className="form-check-input" type="checkbox"
|
||||
id="like" name="like"
|
||||
checked={!!props.building_like}
|
||||
disabled={props.mode === 'view'}
|
||||
onChange={props.onLike}
|
||||
/>
|
||||
<label htmlFor="like" className="form-check-label">
|
||||
<label className="form-check-label">
|
||||
<input className="form-check-input" type="checkbox"
|
||||
name="like"
|
||||
checked={!!props.userLike}
|
||||
disabled={props.mode === 'view'}
|
||||
onChange={e => props.onLike(e.target.checked)}
|
||||
/>
|
||||
I like this building and think it contributes to the city!
|
||||
</label>
|
||||
</Fragment>
|
||||
@ -42,8 +50,10 @@ const LikeDataEntry: React.FunctionComponent<any> = (props) => { // TODO: remove
|
||||
}
|
||||
|
||||
LikeDataEntry.propTypes = {
|
||||
value: PropTypes.any,
|
||||
user_building_like: PropTypes.bool
|
||||
}
|
||||
// mode: PropTypes.string,
|
||||
userLike: PropTypes.bool,
|
||||
totalLikes: PropTypes.number,
|
||||
onLike: PropTypes.func
|
||||
};
|
||||
|
||||
export default LikeDataEntry;
|
||||
|
@ -2,8 +2,18 @@ import React, { Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { DataTitleCopyable } from './data-title';
|
||||
import { BaseDataEntryProps } from './data-entry';
|
||||
|
||||
const NumericDataEntry: React.FunctionComponent<any> = (props) => { // TODO: remove any
|
||||
|
||||
interface NumericDataEntryProps extends BaseDataEntryProps {
|
||||
value?: number;
|
||||
placeholder?: string;
|
||||
step?: number;
|
||||
min?: number;
|
||||
max?: number;
|
||||
}
|
||||
|
||||
const NumericDataEntry: React.FunctionComponent<NumericDataEntryProps> = (props) => {
|
||||
return (
|
||||
<Fragment>
|
||||
<DataTitleCopyable
|
||||
@ -24,7 +34,12 @@ const NumericDataEntry: React.FunctionComponent<any> = (props) => { // TODO: rem
|
||||
min={props.min || 0}
|
||||
disabled={props.mode === 'view' || props.disabled}
|
||||
placeholder={props.placeholder}
|
||||
onChange={props.onChange}
|
||||
onChange={e =>
|
||||
props.onChange(
|
||||
props.slug,
|
||||
e.target.value === '' ? null : parseFloat(e.target.value)
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
|
@ -2,8 +2,16 @@ import React, { Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { DataTitleCopyable } from './data-title';
|
||||
import { BaseDataEntryProps } from './data-entry';
|
||||
|
||||
const SelectDataEntry: React.FunctionComponent<any> = (props) => { // TODO: remove any
|
||||
interface SelectDataEntryProps extends BaseDataEntryProps {
|
||||
value: string;
|
||||
placeholder?: string;
|
||||
options: string[];
|
||||
}
|
||||
|
||||
|
||||
const SelectDataEntry: React.FunctionComponent<SelectDataEntryProps> = (props) => {
|
||||
return (
|
||||
<Fragment>
|
||||
<DataTitleCopyable
|
||||
@ -17,7 +25,14 @@ const SelectDataEntry: React.FunctionComponent<any> = (props) => { // TODO: remo
|
||||
id={props.slug} name={props.slug}
|
||||
value={props.value || ''}
|
||||
disabled={props.mode === 'view' || props.disabled}
|
||||
onChange={props.onChange}>
|
||||
onChange={e =>
|
||||
props.onChange(
|
||||
props.slug,
|
||||
e.target.value === '' ?
|
||||
null :
|
||||
e.target.value
|
||||
)}
|
||||
>
|
||||
<option value="">{props.placeholder}</option>
|
||||
{
|
||||
props.options.map(option => (
|
||||
|
@ -2,8 +2,15 @@ import React, { Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { DataTitleCopyable } from './data-title';
|
||||
import { BaseDataEntryProps } from './data-entry';
|
||||
|
||||
const TextboxDataEntry: React.FunctionComponent<any> = (props) => { // TODO: remove any
|
||||
interface TextboxDataEntryProps extends BaseDataEntryProps {
|
||||
value: string;
|
||||
placeholder?: string;
|
||||
maxLength?: number;
|
||||
}
|
||||
|
||||
const TextboxDataEntry: React.FunctionComponent<TextboxDataEntryProps> = (props) => {
|
||||
return (
|
||||
<Fragment>
|
||||
<DataTitleCopyable
|
||||
@ -18,11 +25,18 @@ const TextboxDataEntry: React.FunctionComponent<any> = (props) => { // TODO: rem
|
||||
id={props.slug}
|
||||
name={props.slug}
|
||||
value={props.value || ''}
|
||||
maxLength={props.max_length}
|
||||
maxLength={props.maxLength}
|
||||
rows={5}
|
||||
disabled={props.mode === 'view' || props.disabled}
|
||||
placeholder={props.placeholder}
|
||||
onChange={props.onChange}
|
||||
onChange={e =>
|
||||
props.onChange(
|
||||
props.slug,
|
||||
e.target.value === '' ?
|
||||
null :
|
||||
e.target.value
|
||||
)
|
||||
}
|
||||
></textarea>
|
||||
</Fragment>
|
||||
);
|
||||
|
@ -5,6 +5,33 @@ import { Redirect } from 'react-router-dom';
|
||||
import ContainerHeader from './container-header';
|
||||
import ErrorBox from '../components/error-box';
|
||||
import InfoBox from '../components/info-box';
|
||||
import { Building } from '../models/building';
|
||||
import { User } from '../models/user';
|
||||
import { compareObjects } from '../helpers';
|
||||
import { CategoryViewProps, CopyProps } from './data-containers/category-view-props';
|
||||
|
||||
interface DataContainerProps {
|
||||
title: string;
|
||||
cat: string;
|
||||
intro: string;
|
||||
help: string;
|
||||
inactive?: boolean;
|
||||
|
||||
user: User;
|
||||
mode: 'view' | 'edit' | 'multi-edit';
|
||||
building: Building;
|
||||
building_like: boolean;
|
||||
selectBuilding: (building: Building) => void
|
||||
}
|
||||
|
||||
interface DataContainerState {
|
||||
error: string;
|
||||
copying: boolean;
|
||||
keys_to_copy: {[key: string]: boolean};
|
||||
currentBuildingId: number;
|
||||
currentBuildingRevisionId: number;
|
||||
buildingEdits: Partial<Building>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared functionality for view/edit forms
|
||||
@ -14,15 +41,14 @@ import InfoBox from '../components/info-box';
|
||||
*
|
||||
* @param WrappedComponent
|
||||
*/
|
||||
const withCopyEdit = (WrappedComponent) => {
|
||||
return class extends React.Component<any, any> { // TODO: add proper types
|
||||
const withCopyEdit = (WrappedComponent: React.ComponentType<CategoryViewProps>) => {
|
||||
return class DataContainer extends React.Component<DataContainerProps, DataContainerState> {
|
||||
static propTypes = { // TODO: generate propTypes from TS
|
||||
title: PropTypes.string,
|
||||
slug: PropTypes.string,
|
||||
intro: PropTypes.string,
|
||||
help: PropTypes.string,
|
||||
inactive: PropTypes.bool,
|
||||
building_id: PropTypes.number,
|
||||
children: PropTypes.node
|
||||
};
|
||||
|
||||
@ -30,23 +56,40 @@ const withCopyEdit = (WrappedComponent) => {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
error: this.props.error || undefined,
|
||||
like: this.props.like || undefined,
|
||||
error: undefined,
|
||||
copying: false,
|
||||
keys_to_copy: {},
|
||||
building: this.props.building
|
||||
buildingEdits: {},
|
||||
currentBuildingId: undefined,
|
||||
currentBuildingRevisionId: undefined
|
||||
};
|
||||
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
this.handleCheck = this.handleCheck.bind(this);
|
||||
this.handleReset = this.handleReset.bind(this);
|
||||
this.handleLike = this.handleLike.bind(this);
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
this.handleUpdate = this.handleUpdate.bind(this);
|
||||
|
||||
this.toggleCopying = this.toggleCopying.bind(this);
|
||||
this.toggleCopyAttribute = this.toggleCopyAttribute.bind(this);
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(props, state) {
|
||||
const newBuildingId = props.building == undefined ? undefined : props.building.building_id;
|
||||
const newBuildingRevisionId = props.building == undefined ? undefined : props.building.revision_id;
|
||||
if(newBuildingId !== state.currentBuildingId || newBuildingRevisionId > state.currentBuildingRevisionId) {
|
||||
return {
|
||||
error: undefined,
|
||||
copying: false,
|
||||
keys_to_copy: {},
|
||||
buildingEdits: {},
|
||||
currentBuildingId: newBuildingId,
|
||||
currentBuildingRevisionId: newBuildingRevisionId
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enter or exit "copying" state - allow user to select attributes to copy
|
||||
*/
|
||||
@ -62,7 +105,7 @@ const withCopyEdit = (WrappedComponent) => {
|
||||
* @param {string} key
|
||||
*/
|
||||
toggleCopyAttribute(key: string) {
|
||||
const keys = this.state.keys_to_copy;
|
||||
const keys = {...this.state.keys_to_copy};
|
||||
if(this.state.keys_to_copy[key]){
|
||||
delete keys[key];
|
||||
} else {
|
||||
@ -73,45 +116,34 @@ const withCopyEdit = (WrappedComponent) => {
|
||||
})
|
||||
}
|
||||
|
||||
updateBuildingState(key, value) {
|
||||
const building = {...this.state.building};
|
||||
building[key] = value;
|
||||
isEdited() {
|
||||
const edits = this.state.buildingEdits;
|
||||
// check if the edits object has any fields
|
||||
return Object.entries(edits).length !== 0;
|
||||
}
|
||||
|
||||
clearEdits() {
|
||||
this.setState({
|
||||
building: building
|
||||
buildingEdits: {}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle changes on typical inputs
|
||||
* - e.g. input[type=text], radio, select, textare
|
||||
*
|
||||
* @param {*} event
|
||||
*/
|
||||
handleChange(event) {
|
||||
const target = event.target;
|
||||
let value = (target.value === '')? null : target.value;
|
||||
const name = target.name;
|
||||
|
||||
// special transform - consider something data driven before adding 'else if's
|
||||
if (name === 'location_postcode' && value !== null) {
|
||||
value = value.toUpperCase();
|
||||
getEditedBuilding() {
|
||||
if(this.isEdited()) {
|
||||
return Object.assign({}, this.props.building, this.state.buildingEdits);
|
||||
} else {
|
||||
return {...this.props.building};
|
||||
}
|
||||
this.updateBuildingState(name, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle changes on checkboxes
|
||||
* - e.g. input[type=checkbox]
|
||||
*
|
||||
* @param {*} event
|
||||
*/
|
||||
handleCheck(event) {
|
||||
const target = event.target;
|
||||
const value = target.checked;
|
||||
const name = target.name;
|
||||
updateBuildingState(key: string, value: any) {
|
||||
const newBuilding = this.getEditedBuilding();
|
||||
newBuilding[key] = value;
|
||||
const [forwardPatch] = compareObjects(this.props.building, newBuilding);
|
||||
|
||||
this.updateBuildingState(name, value);
|
||||
this.setState({
|
||||
buildingEdits: forwardPatch
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@ -121,160 +153,166 @@ const withCopyEdit = (WrappedComponent) => {
|
||||
* @param {String} name
|
||||
* @param {*} value
|
||||
*/
|
||||
handleUpdate(name: string, value: any) {
|
||||
handleChange(name: string, value: any) {
|
||||
this.updateBuildingState(name, value);
|
||||
}
|
||||
|
||||
handleReset() {
|
||||
this.clearEdits();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle likes separately
|
||||
* - like/love reaction is limited to set/unset per user
|
||||
*
|
||||
* @param {*} event
|
||||
*/
|
||||
handleLike(event) {
|
||||
event.preventDefault();
|
||||
const like = event.target.checked;
|
||||
|
||||
fetch(`/api/buildings/${this.props.building.building_id}/like.json`, {
|
||||
method: 'POST',
|
||||
headers:{
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
credentials: 'same-origin',
|
||||
body: JSON.stringify({like: like})
|
||||
}).then(
|
||||
res => res.json()
|
||||
).then(function(res){
|
||||
if (res.error) {
|
||||
this.setState({error: res.error})
|
||||
async handleLike(like: boolean) {
|
||||
try {
|
||||
const res = await fetch(`/api/buildings/${this.props.building.building_id}/like.json`, {
|
||||
method: 'POST',
|
||||
headers:{
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
credentials: 'same-origin',
|
||||
body: JSON.stringify({like: like})
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
if (data.error) {
|
||||
this.setState({error: data.error})
|
||||
} else {
|
||||
this.props.selectBuilding(res);
|
||||
this.updateBuildingState('likes_total', res.likes_total);
|
||||
this.props.selectBuilding(data);
|
||||
this.updateBuildingState('likes_total', data.likes_total);
|
||||
}
|
||||
}.bind(this)).catch(
|
||||
(err) => this.setState({error: err})
|
||||
);
|
||||
} catch(err) {
|
||||
this.setState({error: err});
|
||||
}
|
||||
}
|
||||
|
||||
handleSubmit(event) {
|
||||
async handleSubmit(event) {
|
||||
event.preventDefault();
|
||||
this.setState({error: undefined})
|
||||
this.setState({error: undefined});
|
||||
|
||||
fetch(`/api/buildings/${this.props.building.building_id}.json`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(this.state.building),
|
||||
headers:{
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
credentials: 'same-origin'
|
||||
}).then(
|
||||
res => res.json()
|
||||
).then(function(res){
|
||||
if (res.error) {
|
||||
this.setState({error: res.error})
|
||||
try {
|
||||
const res = await fetch(`/api/buildings/${this.props.building.building_id}.json`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(this.state.buildingEdits),
|
||||
headers:{
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
credentials: 'same-origin'
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
if (data.error) {
|
||||
this.setState({error: data.error})
|
||||
} else {
|
||||
this.props.selectBuilding(res);
|
||||
this.props.selectBuilding(data);
|
||||
}
|
||||
}.bind(this)).catch(
|
||||
(err) => this.setState({error: err})
|
||||
);
|
||||
} catch(err) {
|
||||
this.setState({error: err});
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.mode === 'edit' && !this.props.user){
|
||||
if (this.props.mode === 'edit' && !this.props.user){
|
||||
return <Redirect to="/sign-up.html" />
|
||||
}
|
||||
|
||||
const currentBuilding = this.getEditedBuilding();
|
||||
|
||||
const values_to_copy = {}
|
||||
for (const key of Object.keys(this.state.keys_to_copy)) {
|
||||
values_to_copy[key] = this.state.building[key]
|
||||
values_to_copy[key] = currentBuilding[key]
|
||||
}
|
||||
const data_string = JSON.stringify(values_to_copy);
|
||||
const copy = {
|
||||
const copy: CopyProps = {
|
||||
copying: this.state.copying,
|
||||
toggleCopying: this.toggleCopying,
|
||||
toggleCopyAttribute: this.toggleCopyAttribute,
|
||||
copyingKey: (key) => this.state.keys_to_copy[key]
|
||||
}
|
||||
copyingKey: (key: string) => this.state.keys_to_copy[key]
|
||||
};
|
||||
const edited = this.isEdited();
|
||||
return (
|
||||
<section
|
||||
id={this.props.slug}
|
||||
id={this.props.cat}
|
||||
className="data-section">
|
||||
<ContainerHeader
|
||||
{...this.props}
|
||||
data_string={data_string}
|
||||
copy={copy}
|
||||
/>
|
||||
<div className="section-body">
|
||||
{
|
||||
this.props.building != undefined ?
|
||||
<form
|
||||
action={`/edit/${this.props.slug}/${this.props.building.building_id}`}
|
||||
method="POST"
|
||||
onSubmit={this.handleSubmit}>
|
||||
{
|
||||
(this.props.inactive) ?
|
||||
<InfoBox
|
||||
msg={`We're not collecting data on ${this.props.title.toLowerCase()} yet - check back soon.`}
|
||||
/>
|
||||
: null
|
||||
}
|
||||
{
|
||||
(this.props.mode === 'edit' && !this.props.inactive) ?
|
||||
<Fragment>
|
||||
<ErrorBox msg={this.state.error} />
|
||||
{
|
||||
this.props.slug === 'like' ? // special-case for likes
|
||||
null :
|
||||
<div className="buttons-container with-space">
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-primary">
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</Fragment>
|
||||
: null
|
||||
}
|
||||
<WrappedComponent
|
||||
building={this.state.building}
|
||||
building_like={this.props.building_like}
|
||||
mode={this.props.mode}
|
||||
copy={copy}
|
||||
onChange={this.handleChange}
|
||||
onCheck={this.handleCheck}
|
||||
onLike={this.handleLike}
|
||||
onUpdate={this.handleUpdate}
|
||||
/>
|
||||
</form>
|
||||
:
|
||||
<form>
|
||||
{
|
||||
(this.props.inactive)?
|
||||
<Fragment>
|
||||
this.props.inactive ?
|
||||
<Fragment>
|
||||
<InfoBox
|
||||
msg={`We're not collecting data on ${this.props.title.toLowerCase()} yet - check back soon.`}
|
||||
/>
|
||||
<WrappedComponent
|
||||
intro={this.props.intro}
|
||||
building={undefined}
|
||||
building_like={undefined}
|
||||
mode={this.props.mode}
|
||||
copy={copy}
|
||||
onChange={this.handleChange}
|
||||
onCheck={this.handleCheck}
|
||||
onLike={this.handleLike}
|
||||
onUpdate={this.handleUpdate}
|
||||
/>
|
||||
</Fragment>
|
||||
:
|
||||
</Fragment> :
|
||||
this.props.building != undefined ?
|
||||
<form
|
||||
action={`/edit/${this.props.cat}/${this.props.building.building_id}`}
|
||||
method="POST"
|
||||
onSubmit={this.handleSubmit}>
|
||||
{
|
||||
(this.props.mode === 'edit' && !this.props.inactive) ?
|
||||
<Fragment>
|
||||
<ErrorBox msg={this.state.error} />
|
||||
{
|
||||
this.props.cat !== 'like' ? // special-case for likes
|
||||
<div className="buttons-container with-space">
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-primary"
|
||||
disabled={!edited}
|
||||
aria-disabled={!edited}>
|
||||
Save
|
||||
</button>
|
||||
{
|
||||
edited ?
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-warning"
|
||||
onClick={this.handleReset}
|
||||
>
|
||||
Discard changes
|
||||
</button> :
|
||||
null
|
||||
}
|
||||
</div> :
|
||||
null
|
||||
}
|
||||
</Fragment>
|
||||
: null
|
||||
}
|
||||
<WrappedComponent
|
||||
intro={this.props.intro}
|
||||
building={currentBuilding}
|
||||
building_like={this.props.building_like}
|
||||
mode={this.props.mode}
|
||||
copy={copy}
|
||||
onChange={this.handleChange}
|
||||
onLike={this.handleLike}
|
||||
/>
|
||||
</form> :
|
||||
<InfoBox msg="Select a building to view data"></InfoBox>
|
||||
}
|
||||
</form>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default withCopyEdit;
|
||||
export default withCopyEdit;
|
@ -6,11 +6,12 @@ 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 { CategoryViewProps } from './category-view-props';
|
||||
|
||||
/**
|
||||
* Age view/edit section
|
||||
*/
|
||||
const AgeView = (props) => (
|
||||
const AgeView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
||||
<Fragment>
|
||||
<YearDataEntry
|
||||
year={props.building.date_year}
|
||||
@ -67,7 +68,7 @@ const AgeView = (props) => (
|
||||
value={props.building.date_link}
|
||||
mode={props.mode}
|
||||
copy={props.copy}
|
||||
onChange={props.onUpdate}
|
||||
onChange={props.onChange}
|
||||
tooltip="URL for age and date reference"
|
||||
placeholder="https://..."
|
||||
/>
|
||||
|
@ -0,0 +1,21 @@
|
||||
interface CopyProps {
|
||||
copying: boolean;
|
||||
toggleCopying: () => void;
|
||||
toggleCopyAttribute: (key: string) => void;
|
||||
copyingKey: (key: string) => boolean;
|
||||
}
|
||||
|
||||
interface CategoryViewProps {
|
||||
intro: string;
|
||||
building: any; // TODO: add Building type with all fields
|
||||
building_like: boolean;
|
||||
mode: 'view' | 'edit' | 'multi-edit';
|
||||
copy: CopyProps;
|
||||
onChange: (key: string, value: any) => void;
|
||||
onLike: (like: boolean) => void;
|
||||
}
|
||||
|
||||
export {
|
||||
CategoryViewProps,
|
||||
CopyProps
|
||||
};
|
@ -1,11 +1,12 @@
|
||||
import React, { Fragment } from 'react';
|
||||
|
||||
import withCopyEdit from '../data-container';
|
||||
import { CategoryViewProps } from './category-view-props';
|
||||
|
||||
/**
|
||||
* Community view/edit section
|
||||
*/
|
||||
const CommunityView = (props) => (
|
||||
const CommunityView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
||||
<Fragment>
|
||||
<p className="data-intro">{props.intro}</p>
|
||||
<ul className="data-list">
|
||||
|
@ -2,17 +2,18 @@ import React, { Fragment } from 'react';
|
||||
|
||||
import withCopyEdit from '../data-container';
|
||||
import LikeDataEntry from '../data-components/like-data-entry';
|
||||
import { CategoryViewProps } from './category-view-props';
|
||||
|
||||
/**
|
||||
* Like view/edit section
|
||||
*/
|
||||
const LikeView = (props) => (
|
||||
const LikeView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
||||
<Fragment>
|
||||
<LikeDataEntry
|
||||
value={props.building.likes_total}
|
||||
userLike={props.building_like}
|
||||
totalLikes={props.building.likes_total}
|
||||
mode={props.mode}
|
||||
onLike={props.onLike}
|
||||
building_like={props.building_like}
|
||||
/>
|
||||
</Fragment>
|
||||
)
|
||||
|
@ -5,8 +5,9 @@ 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 { CategoryViewProps } from './category-view-props';
|
||||
|
||||
const LocationView = (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
|
||||
@ -63,6 +64,7 @@ const LocationView = (props) => (
|
||||
copy={props.copy}
|
||||
onChange={props.onChange}
|
||||
maxLength={8}
|
||||
valueTransform={x=>x.toUpperCase()}
|
||||
/>
|
||||
<DataEntry
|
||||
title="TOID"
|
||||
@ -96,7 +98,7 @@ const LocationView = (props) => (
|
||||
mode={props.mode}
|
||||
copy={props.copy}
|
||||
step={0.0001}
|
||||
placeholder={51}
|
||||
placeholder="51"
|
||||
onChange={props.onChange}
|
||||
/>
|
||||
<NumericDataEntry
|
||||
@ -106,7 +108,7 @@ const LocationView = (props) => (
|
||||
mode={props.mode}
|
||||
copy={props.copy}
|
||||
step={0.0001}
|
||||
placeholder={0}
|
||||
placeholder="0"
|
||||
onChange={props.onChange}
|
||||
/>
|
||||
</Fragment>
|
||||
|
@ -5,11 +5,12 @@ 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 { CategoryViewProps } from './category-view-props';
|
||||
|
||||
/**
|
||||
* Planning view/edit section
|
||||
*/
|
||||
const PlanningView = (props) => (
|
||||
const PlanningView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
||||
<Fragment>
|
||||
<DataEntry
|
||||
title="Planning portal link"
|
||||
|
@ -4,11 +4,12 @@ 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 { CategoryViewProps } from './category-view-props';
|
||||
|
||||
/**
|
||||
* Size view/edit section
|
||||
*/
|
||||
const SizeView = (props) => (
|
||||
const SizeView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
||||
<Fragment>
|
||||
<DataEntryGroup name="Storeys" collapsed={false}>
|
||||
|
||||
|
@ -1,11 +1,12 @@
|
||||
import React, { Fragment } from 'react';
|
||||
|
||||
import withCopyEdit from '../data-container';
|
||||
import { CategoryViewProps } from './category-view-props';
|
||||
|
||||
/**
|
||||
* Streetscape view/edit section
|
||||
*/
|
||||
const StreetscapeView = (props) => (
|
||||
const StreetscapeView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
||||
<Fragment>
|
||||
<p className="data-intro">{props.intro}</p>
|
||||
<ul className="data-list">
|
||||
|
@ -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 { CategoryViewProps } from './category-view-props';
|
||||
|
||||
const EnergyCategoryOptions = ["A", "B", "C", "D", "E", "F", "G"];
|
||||
const BreeamRatingOptions = [
|
||||
@ -17,12 +18,7 @@ const BreeamRatingOptions = [
|
||||
/**
|
||||
* Sustainability view/edit section
|
||||
*/
|
||||
const SustainabilityView = (props) => {
|
||||
const dataEntryProps = {
|
||||
mode: props.mode,
|
||||
copy: props.copy,
|
||||
onChange: props.onChange
|
||||
};
|
||||
const SustainabilityView: React.FunctionComponent<CategoryViewProps> = (props) => {
|
||||
return (
|
||||
<Fragment>
|
||||
<SelectDataEntry
|
||||
@ -31,7 +27,9 @@ const SustainabilityView = (props) => {
|
||||
value={props.building.sust_breeam_rating}
|
||||
tooltip="(Building Research Establishment Environmental Assessment Method) May not be present for many buildings"
|
||||
options={BreeamRatingOptions}
|
||||
{...dataEntryProps}
|
||||
mode={props.mode}
|
||||
copy={props.copy}
|
||||
onChange={props.onChange}
|
||||
/>
|
||||
<SelectDataEntry
|
||||
title="DEC Rating"
|
||||
@ -39,7 +37,9 @@ const SustainabilityView = (props) => {
|
||||
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"
|
||||
options={EnergyCategoryOptions}
|
||||
{...dataEntryProps}
|
||||
mode={props.mode}
|
||||
copy={props.copy}
|
||||
onChange={props.onChange}
|
||||
/>
|
||||
<SelectDataEntry
|
||||
title="EPC Rating"
|
||||
@ -48,7 +48,9 @@ const SustainabilityView = (props) => {
|
||||
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"
|
||||
options={EnergyCategoryOptions}
|
||||
disabled={true}
|
||||
{...dataEntryProps}
|
||||
mode={props.mode}
|
||||
copy={props.copy}
|
||||
onChange={props.onChange}
|
||||
/>
|
||||
<NumericDataEntry
|
||||
title="Last significant retrofit"
|
||||
@ -58,7 +60,9 @@ const SustainabilityView = (props) => {
|
||||
step={1}
|
||||
min={1086}
|
||||
max={new Date().getFullYear()}
|
||||
{...dataEntryProps}
|
||||
mode={props.mode}
|
||||
copy={props.copy}
|
||||
onChange={props.onChange}
|
||||
/>
|
||||
<NumericDataEntry
|
||||
title="Expected lifespan for typology"
|
||||
@ -67,7 +71,9 @@ const SustainabilityView = (props) => {
|
||||
step={1}
|
||||
min={1}
|
||||
disabled={true}
|
||||
{...dataEntryProps}
|
||||
mode={props.mode}
|
||||
copy={props.copy}
|
||||
onChange={props.onChange}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
|
@ -2,11 +2,12 @@ import React, { Fragment } from 'react';
|
||||
|
||||
import withCopyEdit from '../data-container';
|
||||
import DataEntry from '../data-components/data-entry';
|
||||
import { CategoryViewProps } from './category-view-props';
|
||||
|
||||
/**
|
||||
* Team view/edit section
|
||||
*/
|
||||
const TeamView = (props) => (
|
||||
const TeamView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
||||
<Fragment>
|
||||
<p className="data-intro">{props.intro}</p>
|
||||
<ul>
|
||||
|
@ -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 { CategoryViewProps } from './category-view-props';
|
||||
|
||||
const AttachmentFormOptions = [
|
||||
"Detached",
|
||||
@ -15,10 +16,7 @@ const AttachmentFormOptions = [
|
||||
/**
|
||||
* Type view/edit section
|
||||
*/
|
||||
const TypeView = (props) => {
|
||||
const {mode, copy, onChange} = props;
|
||||
const dataEntryProps = { mode, copy, onChange };
|
||||
|
||||
const TypeView: React.FunctionComponent<CategoryViewProps> = (props) => {
|
||||
return (
|
||||
<Fragment>
|
||||
<SelectDataEntry
|
||||
@ -27,7 +25,9 @@ const TypeView = (props) => {
|
||||
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)"
|
||||
options={AttachmentFormOptions}
|
||||
{...dataEntryProps}
|
||||
mode={props.mode}
|
||||
copy={props.copy}
|
||||
onChange={props.onChange}
|
||||
/>
|
||||
<NumericDataEntry
|
||||
title="When did use change?"
|
||||
@ -37,11 +37,18 @@ const TypeView = (props) => {
|
||||
min={1086}
|
||||
max={new Date().getFullYear()}
|
||||
step={1}
|
||||
{...dataEntryProps}
|
||||
mode={props.mode}
|
||||
copy={props.copy}
|
||||
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"
|
||||
value={undefined}
|
||||
copy={props.copy}
|
||||
mode={props.mode}
|
||||
onChange={props.onChange}
|
||||
disabled={true}
|
||||
/>
|
||||
</Fragment>
|
||||
|
@ -1,11 +1,12 @@
|
||||
import React, { Fragment } from 'react';
|
||||
|
||||
import withCopyEdit from '../data-container';
|
||||
import { CategoryViewProps } from './category-view-props';
|
||||
|
||||
/**
|
||||
* Use view/edit section
|
||||
*/
|
||||
const UseView = (props) => (
|
||||
const UseView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
||||
<Fragment>
|
||||
<p className="data-intro">{props.intro}</p>
|
||||
<ul>
|
||||
|
@ -5,7 +5,7 @@
|
||||
order: 1;
|
||||
padding: 0 0 2em;
|
||||
background: #fff;
|
||||
overflow-y: scroll;
|
||||
overflow-y: auto;
|
||||
height: 40%;
|
||||
}
|
||||
|
||||
@ -34,6 +34,14 @@
|
||||
text-decoration: none;
|
||||
color: #222;
|
||||
padding: 0.75rem 0.25rem 0.5rem 0;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.section-header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
.section-header h2,
|
||||
.section-header .icon-buttons {
|
||||
@ -139,6 +147,11 @@
|
||||
/**
|
||||
* Data list sections
|
||||
*/
|
||||
|
||||
.section-body {
|
||||
margin-top: 0.75em;
|
||||
padding: 0 0.75em;
|
||||
}
|
||||
.data-section .h3 {
|
||||
margin: 0;
|
||||
}
|
||||
@ -162,9 +175,7 @@
|
||||
padding-left: 0.75rem;
|
||||
padding-right: 0.75rem;
|
||||
}
|
||||
.data-section form {
|
||||
padding: 0 0.75rem;
|
||||
}
|
||||
|
||||
.data-list a {
|
||||
color: #555;
|
||||
}
|
||||
|
@ -36,4 +36,16 @@ function sanitiseURL(string){
|
||||
return `${url_.protocol}//${url_.hostname}${url_.pathname || ''}${url_.search || ''}${url_.hash || ''}`
|
||||
}
|
||||
|
||||
export { sanitiseURL }
|
||||
function compareObjects(objA: object, objB: object): [object, object] {
|
||||
const reverse = {}
|
||||
const forward = {}
|
||||
for (const [key, value] of Object.entries(objB)) {
|
||||
if (objA[key] !== value) {
|
||||
reverse[key] = objA[key];
|
||||
forward[key] = value;
|
||||
}
|
||||
}
|
||||
return [forward, reverse];
|
||||
}
|
||||
|
||||
export { sanitiseURL, 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 { Building } from './models/building';
|
||||
|
||||
interface MapAppRouteParams {
|
||||
mode: 'view' | 'edit' | 'multi-edit';
|
||||
@ -17,7 +18,7 @@ interface MapAppRouteParams {
|
||||
}
|
||||
|
||||
interface MapAppProps extends RouteComponentProps<MapAppRouteParams> {
|
||||
building: any;
|
||||
building: Building;
|
||||
building_like: boolean;
|
||||
user: any;
|
||||
revisionId: number;
|
||||
@ -26,7 +27,7 @@ interface MapAppProps extends RouteComponentProps<MapAppRouteParams> {
|
||||
interface MapAppState {
|
||||
category: string;
|
||||
revision_id: number;
|
||||
building: any;
|
||||
building: Building;
|
||||
building_like: boolean;
|
||||
}
|
||||
|
||||
@ -95,7 +96,7 @@ class MapApp extends React.Component<MapAppProps, MapAppState> {
|
||||
}
|
||||
}
|
||||
|
||||
selectBuilding(building) {
|
||||
selectBuilding(building: Building) {
|
||||
const mode = this.props.match.params.mode || 'view';
|
||||
const category = this.props.match.params.category || 'age';
|
||||
|
||||
@ -218,8 +219,7 @@ class MapApp extends React.Component<MapAppProps, MapAppState> {
|
||||
}
|
||||
|
||||
render() {
|
||||
console.log(this.state.revision_id);
|
||||
const mode = this.props.match.params.mode || 'basic';
|
||||
const mode = this.props.match.params.mode;
|
||||
|
||||
let category = this.state.category || 'age';
|
||||
|
||||
@ -254,13 +254,13 @@ class MapApp extends React.Component<MapAppProps, MapAppState> {
|
||||
/>
|
||||
</Sidebar>
|
||||
</Route>
|
||||
<Route exact path="/(view|edit|multi-edit)">
|
||||
<Redirect to="/view/categories" />
|
||||
</Route>
|
||||
<Route exact path="/:mode(view|edit|multi-edit)"
|
||||
render={props => (<Redirect to={`/${props.match.params.mode}/categories`} />)}
|
||||
/>
|
||||
</Switch>
|
||||
<ColouringMap
|
||||
building={this.state.building}
|
||||
mode={mode}
|
||||
mode={mode || 'basic'}
|
||||
category={category}
|
||||
revision_id={this.state.revision_id}
|
||||
selectBuilding={this.selectBuilding}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import { Map, TileLayer, ZoomControl, AttributionControl, GeoJSON } from 'react-leaflet-universal';
|
||||
import { GeoJsonObject } from 'geojson';
|
||||
|
||||
import '../../../node_modules/leaflet/dist/leaflet.css'
|
||||
import './map.css'
|
||||
@ -9,12 +10,12 @@ import { HelpIcon } from '../components/icons';
|
||||
import Legend from './legend';
|
||||
import SearchBox from './search-box';
|
||||
import ThemeSwitcher from './theme-switcher';
|
||||
import { GeoJsonObject } from 'geojson';
|
||||
import { Building } from '../models/building';
|
||||
|
||||
const OS_API_KEY = 'NVUxtY5r8eA6eIfwrPTAGKrAAsoeI9E9';
|
||||
|
||||
interface ColouringMapProps {
|
||||
building: any;
|
||||
building: Building;
|
||||
mode: 'basic' | 'view' | 'edit' | 'multi-edit';
|
||||
category: string;
|
||||
revision_id: number;
|
||||
@ -32,7 +33,7 @@ interface ColouringMapState {
|
||||
/**
|
||||
* Map area
|
||||
*/
|
||||
class ColouringMap extends Component<ColouringMapProps, ColouringMapState> { // TODO: add proper types
|
||||
class ColouringMap extends Component<ColouringMapProps, ColouringMapState> {
|
||||
static propTypes = { // TODO: generate propTypes from TS
|
||||
building: PropTypes.object,
|
||||
mode: PropTypes.string,
|
||||
|
12
app/src/frontend/models/building.ts
Normal file
12
app/src/frontend/models/building.ts
Normal file
@ -0,0 +1,12 @@
|
||||
interface Building {
|
||||
building_id: number;
|
||||
geometry_id: number;
|
||||
revision_id: number;
|
||||
|
||||
uprns: string[];
|
||||
// TODO: add other fields as needed
|
||||
}
|
||||
|
||||
export {
|
||||
Building
|
||||
};
|
8
app/src/frontend/models/user.ts
Normal file
8
app/src/frontend/models/user.ts
Normal file
@ -0,0 +1,8 @@
|
||||
interface User {
|
||||
username: string;
|
||||
// TODO: add other fields as needed
|
||||
}
|
||||
|
||||
export {
|
||||
User
|
||||
};
|
@ -9,7 +9,7 @@
|
||||
max-height: 100%;
|
||||
border-radius: 0;
|
||||
padding: 1.5em 2.5em 2.5em;
|
||||
overflow-y: scroll;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.welcome-float.jumbotron {
|
||||
background: #fff;
|
||||
|
Loading…
Reference in New Issue
Block a user