Avoid dependency loop

- building-view contained BuildingVIew and withCopyEdit
- and imported each data-container
- which each imported withCopyEdit to create their data-container

seemed okay from ts/webpack dev environment
but failed in jest test
This commit is contained in:
Tom Russell 2019-08-14 21:54:00 +01:00
parent 22db157e6e
commit 1997c34470
16 changed files with 115 additions and 262 deletions

View File

@ -6,7 +6,6 @@ import { parse } from 'query-string';
import '../../node_modules/bootstrap/dist/css/bootstrap.min.css';
import './app.css';
import BuildingEdit from './building/building-edit';
import BuildingView from './building/building-view';
import ColouringMap from './map/map';
import Header from './header';

View File

@ -1,13 +1,6 @@
import React, { Fragment } from 'react';
import { NavLink } from 'react-router-dom';
import PropTypes from 'prop-types';
import Tooltip from '../components/tooltip';
import { sanitiseURL } from '../helpers';
import React from 'react';
import BuildingNotFound from './building-not-found';
import ContainerHeader from './container-header';
import Sidebar from './sidebar';
import LocationContainer from './data-containers/location';
import UseContainer from './data-containers/use';
@ -125,244 +118,4 @@ const BuildingView = (props) => {
}
}
/**
* Shared functionality for view/edit forms
*
* See React Higher-order-component docs for the pattern
* - https://reactjs.org/docs/higher-order-components.html
*
* @param WrappedComponent
*/
function withCopyEdit(WrappedComponent) {
return class extends React.Component<any, any> { // TODO: add proper types
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
};
constructor(props) {
super(props);
this.state = {
copying: false,
values_to_copy: {}
};
this.toggleCopying = this.toggleCopying.bind(this);
this.toggleCopyAttribute = this.toggleCopyAttribute.bind(this);
}
/**
* Enter or exit "copying" state - allow user to select attributes to copy
*/
toggleCopying() {
this.setState({
copying: !this.state.copying
})
}
/**
* Keep track of data to copy (accumulate while in "copying" state)
*
* @param {string} key
*/
toggleCopyAttribute(key) {
const value = this.props[key];
const values = this.state.values_to_copy;
if(Object.keys(this.state.values_to_copy).includes(key)){
delete values[key];
} else {
values[key] = value;
}
this.setState({
values_to_copy: values
})
}
render() {
const data_string = JSON.stringify(this.state.values_to_copy);
const copy = {
copying: this.state.copying,
toggleCopyAttribute: this.toggleCopyAttribute,
copyingKey: (key) => Object.keys(this.state.values_to_copy).includes(key)
}
return this.props.building?
<Sidebar>
<section id={this.props.slug} className="data-section">
<ContainerHeader {...this.props} data_string={data_string} copy={copy} />
<WrappedComponent {...this.props} copy={copy} />
</section>
</Sidebar>
: <BuildingNotFound mode="view" />
}
}
}
const DataEntry: React.FunctionComponent<any> = (props) => { // TODO: remove any
return (
<Fragment>
<dt>
{ props.title }
{ props.tooltip? <Tooltip text={ props.tooltip } /> : null }
{ (props.copy.copying && props.cat && props.slug && !props.disabled)?
<div className="icon-buttons">
<label className="icon-button copy">
Copy
<input
type="checkbox"
checked={props.copy.copyingThis(props.slug)}
onChange={() => props.copy.toggleCopyAttribute(props.slug)}/>
</label>
</div>
: null
}
</dt>
<dd>{
(props.value != null && props.value !== '')?
(typeof(props.value) === 'boolean')?
(props.value)? 'Yes' : 'No'
: props.value
: '\u00A0'}</dd>
</Fragment>
);
}
DataEntry.propTypes = {
title: PropTypes.string,
cat: PropTypes.string,
slug: PropTypes.string,
tooltip: PropTypes.string,
disabled: PropTypes.bool,
value: PropTypes.any
}
const LikeDataEntry: React.FunctionComponent<any> = (props) => { // TODO: remove any
const data_string = JSON.stringify({like: true});
return (
<Fragment>
<dt>
{ props.title }
{ props.tooltip? <Tooltip text={ props.tooltip } /> : null }
<div className="icon-buttons">
<NavLink
to={`/multi-edit/${props.cat}.html?data=${data_string}`}
className="icon-button copy">
Copy
</NavLink>
</div>
</dt>
<dd>
{
(props.value != null)?
(props.value === 1)?
`${props.value} person likes this building`
: `${props.value} people like this building`
: '\u00A0'
}
</dd>
{
(props.user_building_like)? <dd>&hellip;including you!</dd> : ''
}
</Fragment>
);
}
LikeDataEntry.propTypes = {
title: PropTypes.string,
cat: PropTypes.string,
tooltip: PropTypes.string,
value: PropTypes.any,
user_building_like: PropTypes.bool
}
const MultiDataEntry: React.FunctionComponent<any> = (props) => { // TODO: remove any
let content;
if (props.value && props.value.length) {
content = <ul>{
props.value.map((item, index) => {
return <li key={index}><a href={sanitiseURL(item)}>{item}</a></li>
})
}</ul>
} else {
content = '\u00A0'
}
return (
<Fragment>
<dt>
{ props.title }
{ props.tooltip? <Tooltip text={ props.tooltip } /> : null }
{ (props.copying && props.cat && props.slug && !props.disabled)?
<div className="icon-buttons">
<label className="icon-button copy">
Copy
<input type="checkbox" checked={props.copy}
onChange={() => props.toggleCopyAttribute(props.slug)}/>
</label>
</div>
: null
}
</dt>
<dd>{ content }</dd>
</Fragment>
);
}
MultiDataEntry.propTypes = {
title: PropTypes.string,
tooltip: PropTypes.string,
value: PropTypes.arrayOf(PropTypes.string)
}
const UPRNsDataEntry = (props) => {
const uprns = props.value || [];
const noParent = uprns.filter(uprn => uprn.parent_uprn == null);
const withParent = uprns.filter(uprn => uprn.parent_uprn != null);
return (
<Fragment>
<dt>
{ props.title }
{ props.tooltip? <Tooltip text={ props.tooltip } /> : null }
</dt>
<dd><ul className="uprn-list">
<Fragment>{
noParent.length?
noParent.map(uprn => (
<li key={uprn.uprn}>{uprn.uprn}</li>
))
: '\u00A0'
}</Fragment>
{
withParent.length?
<details>
<summary>Children</summary>
{
withParent.map(uprn => (
<li key={uprn.uprn}>{uprn.uprn} (child of {uprn.parent_uprn})</li>
))
}
</details>
: null
}
</ul></dd>
</Fragment>
)
}
UPRNsDataEntry.propTypes = {
title: PropTypes.string,
tooltip: PropTypes.string,
value: PropTypes.arrayOf(PropTypes.shape({
uprn: PropTypes.string.isRequired,
parent_uprn: PropTypes.string
}))
}
export default BuildingView;
export { withCopyEdit };

View File

@ -49,7 +49,7 @@ const ContainerHeader: React.FunctionComponent<any> = (props) => (
<NavLink
className="icon-button edit"
title="Edit data"
to={`/edit/${props.slug}/building/${props.building_id}.html`}>
to={`/edit/${props.cat}/building/${props.building.building_id}.html`}>
Edit
<EditIcon />
</NavLink>

View File

@ -0,0 +1,89 @@
import React from 'react';
import PropTypes from 'prop-types';
import BuildingNotFound from './building-not-found';
import ContainerHeader from './container-header';
import Sidebar from './sidebar';
/**
* Shared functionality for view/edit forms
*
* See React Higher-order-component docs for the pattern
* - https://reactjs.org/docs/higher-order-components.html
*
* @param WrappedComponent
*/
const withCopyEdit = (WrappedComponent) => {
return class extends React.Component<any, any> { // TODO: add proper types
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
};
constructor(props) {
super(props);
this.state = {
copying: false,
values_to_copy: {}
};
this.toggleCopying = this.toggleCopying.bind(this);
this.toggleCopyAttribute = this.toggleCopyAttribute.bind(this);
}
/**
* Enter or exit "copying" state - allow user to select attributes to copy
*/
toggleCopying() {
this.setState({
copying: !this.state.copying
})
}
/**
* Keep track of data to copy (accumulate while in "copying" state)
*
* @param {string} key
*/
toggleCopyAttribute(key) {
const value = this.props.building[key];
const values = this.state.values_to_copy;
if(Object.keys(this.state.values_to_copy).includes(key)){
delete values[key];
} else {
values[key] = value;
}
this.setState({
values_to_copy: values
})
}
render() {
const data_string = JSON.stringify(this.state.values_to_copy);
const copy = {
copying: this.state.copying,
toggleCopying: this.toggleCopying,
toggleCopyAttribute: this.toggleCopyAttribute,
copyingKey: (key) => Object.keys(this.state.values_to_copy).includes(key)
}
return this.props.building?
<Sidebar>
<section id={this.props.slug} className="data-section">
<ContainerHeader
{...this.props}
data_string={data_string}
copy={copy}
/>
<WrappedComponent {...this.props} copy={copy} />
</section>
</Sidebar>
: <BuildingNotFound mode="view" />
}
}
}
export default withCopyEdit;

View File

@ -1,6 +1,7 @@
import React from 'react';
import { withCopyEdit } from '../building-view';
import withCopyEdit from '../data-container';
import DataEntry from '../data-components/data-entry';
/**
* Age view/edit section

View File

@ -1,6 +1,7 @@
import React from 'react';
import { withCopyEdit } from '../building-view';
import withCopyEdit from '../data-container';
import DataEntry from '../data-components/data-entry';
/**
* Community view/edit section

View File

@ -1,6 +1,7 @@
import React from 'react';
import { withCopyEdit } from '../building-view';
import withCopyEdit from '../data-container';
import DataEntry from '../data-components/data-entry';
/**
* Construction view/edit section

View File

@ -1,6 +1,7 @@
import React from 'react';
import { withCopyEdit } from '../building-view';
import withCopyEdit from '../data-container';
import DataEntry from '../data-components/data-entry';
/**
* Greenery view/edit section

View File

@ -1,6 +1,7 @@
import React from 'react';
import { withCopyEdit } from '../building-view';
import withCopyEdit from '../data-container';
import DataEntry from '../data-components/data-entry';
/**
* Like view/edit section

View File

@ -1,6 +1,7 @@
import React from 'react';
import { withCopyEdit } from '../building-view';
import withCopyEdit from '../data-container';
import DataEntry from '../data-components/data-entry';
const LocationView = (props) => (
<dl className="data-list">

View File

@ -1,6 +1,7 @@
import React from 'react';
import { withCopyEdit } from '../building-view';
import withCopyEdit from '../data-container';
import DataEntry from '../data-components/data-entry';
/**
* Planning view/edit section

View File

@ -1,6 +1,7 @@
import React from 'react';
import { withCopyEdit } from '../building-view';
import withCopyEdit from '../data-container';
import DataEntry from '../data-components/data-entry';
/**
* Size view/edit section

View File

@ -1,6 +1,7 @@
import React from 'react';
import { withCopyEdit } from '../building-view';
import withCopyEdit from '../data-container';
import DataEntry from '../data-components/data-entry';
/**
* Sustainability view/edit section

View File

@ -1,6 +1,7 @@
import React from 'react';
import { withCopyEdit } from '../building-view';
import withCopyEdit from '../data-container';
import DataEntry from '../data-components/data-entry';
/**
* Team view/edit section

View File

@ -1,6 +1,7 @@
import React from 'react';
import { withCopyEdit } from '../building-view';
import withCopyEdit from '../data-container';
import DataEntry from '../data-components/data-entry';
/**
* Type view/edit section

View File

@ -1,6 +1,7 @@
import React from 'react';
import { withCopyEdit } from '../building-view';
import withCopyEdit from '../data-container';
import DataEntry from '../data-components/data-entry';
/**
* Use view/edit section