Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

start reduxing activity state #426

Merged
merged 5 commits into from
Apr 19, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions web/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,9 @@
"d3-selection": "^1.3.0",
"d3-transition": "^1.1.1",
"draft-js": "^0.10.5",
"draftjs-to-html": "^0.8.3",
"history": "^4.7.2",
"html-to-draftjs": "^1.3.0",
"i18n-js": "^3.0.3",
"lodash.kebabcase": "^4.1.1",
"numeral": "^2.0.6",
Expand Down
5 changes: 5 additions & 0 deletions web/src/actions/activities.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const ADD_ACTIVITY = 'ADD_ACTIVITY';
export const UPDATE_ACTIVITY = 'UPDATE_ACTIVITY';

export const addActivity = () => ({ type: ADD_ACTIVITY });
export const updateActivity = data => ({ type: UPDATE_ACTIVITY, data });
4 changes: 2 additions & 2 deletions web/src/components/Collapsible.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ class Collapsible extends Component {
const id = kebabCase(title);

return (
<div className={`mb2 bg-${bgColor} border border-gray`}>
<div className={`mb2 bg-${bgColor} border border-silver`}>
<button
type="button"
className="btn block col-12 left-align h3 py2 line-height-1"
Expand All @@ -40,7 +40,7 @@ class Collapsible extends Component {
{title}
</button>
<div
className={`p2 border-top border-gray ${
className={`p2 border-top border-silver ${
isOpen ? '' : 'display-none'
}`}
id={id}
Expand Down
51 changes: 51 additions & 0 deletions web/src/containers/Activities.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import PropTypes from 'prop-types';
import React from 'react';
import { connect } from 'react-redux';

import ActivityDetailAll from './ActivityDetailAll';
import ActivityListEntry from './ActivityListEntry';
import { addActivity as addActivityAction } from '../actions/activities';
import Collapsible from '../components/Collapsible';
import Container from '../components/Container';
import Section from '../components/Section';
import SectionTitle from '../components/SectionTitle';

const Activities = ({ activityIds, addActivity }) => (
<Container>
<Section>
<SectionTitle>Program Activities</SectionTitle>
<Collapsible title="Activity List" open>
<div className="mb2">
{activityIds.map((aId, idx) => (
<ActivityListEntry key={aId} aId={aId} num={idx + 1} />
))}
</div>
<button
type="button"
className="mt2 btn btn-primary"
onClick={addActivity}
>
Add activity
</button>
</Collapsible>
{activityIds.map((aId, idx) => (
<ActivityDetailAll key={aId} aId={aId} num={idx + 1} />
))}
</Section>
</Container>
);

Activities.propTypes = {
activityIds: PropTypes.array.isRequired,
addActivity: PropTypes.func.isRequired
};

const mapStateToProps = ({ activities }) => ({
activityIds: activities.allIds
});

const mapDispatchToProps = {
addActivity: addActivityAction
};

export default connect(mapStateToProps, mapDispatchToProps)(Activities);
35 changes: 35 additions & 0 deletions web/src/containers/ActivityDetailAll.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import PropTypes from 'prop-types';
import React from 'react';
import { connect } from 'react-redux';

import ActivityDetailDescription from './ActivityDetailDescription';
import ActivityDetailGoals from './ActivityDetailGoals';
import Collapsible from '../components/Collapsible';

const activityTitle = (a, i) => {
let title = `Activity ${i}`;
if (a.name) title += `: ${a.name}`;
if (a.types.length) title += ` (${a.types.join(', ')})`;
return title;
};

const ActivityDetailAll = ({ aId, title }) => (
<Collapsible title={title} bgColor="darken-1" open>
<ActivityDetailDescription aId={aId} />
<ActivityDetailGoals aId={aId} />
</Collapsible>
);

ActivityDetailAll.propTypes = {
aId: PropTypes.number.isRequired,
title: PropTypes.string.isRequired
};

const mapStateToProps = ({ activities: { byId } }, { aId, num }) => {
const activity = byId[aId];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm curious why you get the activity from redux store by ID rather than taking it as a param?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, i wasn't really sold on this either; i read that it was better performance to pass down the id and look up the data object via connect, let me track down the article

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fascinating. Really gets into the weeds of how React handles re-rendering as state changes.

const title = `Program Activities › ${activityTitle(activity, num)}`;

return { title };
};

export default connect(mapStateToProps)(ActivityDetailAll);
122 changes: 122 additions & 0 deletions web/src/containers/ActivityDetailDescription.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import PropTypes from 'prop-types';
import { EditorState, ContentState, convertToRaw } from 'draft-js';
import draftToHtml from 'draftjs-to-html';
import htmlToDraft from 'html-to-draftjs';
import React, { Component } from 'react';
import { Editor } from 'react-draft-wysiwyg';
import { connect } from 'react-redux';

import { updateActivity as updateActivityAction } from '../actions/activities';
import Collapsible from '../components/Collapsible';
import Icon, { faHelp } from '../components/Icons';
import { EDITOR_CONFIG } from '../util';

const htmlToEditor = html => {
if (!html) return EditorState.createEmpty();

const { contentBlocks, entityMap } = htmlToDraft(html);
const content = ContentState.createFromBlockArray(contentBlocks, entityMap);
return EditorState.createWithContent(content);
};

const editorToHtml = editorState => {
const content = convertToRaw(editorState.getCurrentContent());
return draftToHtml(content);
};

class ActivityDetailDescription extends Component {
constructor(props) {
super(props);

const { descLong, altApproach } = props.activity;

this.state = {
descLong: htmlToEditor(descLong),
altApproach: htmlToEditor(altApproach)
};
}

onEditorChange = name => editorState => {
this.setState({ [name]: editorState });
};

syncEditorState = name => () => {
const html = editorToHtml(this.state[name]);
const { activity, updateActivity } = this.props;
const data = { id: activity.id, name, value: html };

updateActivity(data);
};

render() {
const { activity, updateActivity } = this.props;
const { descLong, altApproach } = this.state;

return (
<Collapsible title="Activity Description" open>
<div className="mb1 bold">
Summary
<Icon icon={faHelp} className="ml-tiny teal" size="sm" />
</div>
<div className="mb3">
<textarea
className="m0 textarea"
rows="5"
maxLength="280"
spellCheck="true"
value={activity.descShort}
onChange={e =>
updateActivity({
id: activity.id,
name: 'descShort',
value: e.target.value
})
}
/>
</div>

<div className="mb3">
<div className="mb1 bold">
Please describe the activity in detail
<Icon icon={faHelp} className="ml-tiny teal" size="sm" />
</div>
<Editor
toolbar={EDITOR_CONFIG}
editorState={descLong}
onEditorStateChange={this.onEditorChange('descLong')}
onBlur={this.syncEditorState('descLong')}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To make sure I'm clear on this: as the editor changes, update the component state so the editor stays editable; when the editor blurs, grab the HTML and sync that to redux state?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

exactly!

/>
</div>

<div className="mb3">
<div className="mb1 bold">
Statement of alternative considerations and supporting justification
</div>
<Editor
toolbar={EDITOR_CONFIG}
editorState={altApproach}
onEditorStateChange={this.onEditorChange('altApproach')}
onBlur={this.syncEditorState('altApproach')}
/>
</div>
</Collapsible>
);
}
}

ActivityDetailDescription.propTypes = {
activity: PropTypes.object.isRequired,
updateActivity: PropTypes.func.isRequired
};

const mapStateToProps = ({ activities: { byId } }, { aId }) => ({
activity: byId[aId]
});

const mapDispatchToProps = {
updateActivity: updateActivityAction
};

export default connect(mapStateToProps, mapDispatchToProps)(
ActivityDetailDescription
);
16 changes: 16 additions & 0 deletions web/src/containers/ActivityDetailGoals.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import React from 'react';
import { connect } from 'react-redux';

import Collapsible from '../components/Collapsible';

const ActivityDetailDescription = () => (
<Collapsible title="Needs and Objectives">
<div>...</div>
</Collapsible>
);

const mapStateToProps = ({ activities: { byId } }, { aId }) => ({
activity: byId[aId]
});

export default connect(mapStateToProps)(ActivityDetailDescription);
80 changes: 80 additions & 0 deletions web/src/containers/ActivityListEntry.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';

import { updateActivity as updateActivityAction } from '../actions/activities';

const ACTIVITY_TYPES = ['HIT', 'HIE', 'MMIS'];

class ActivityListEntry extends Component {
handleTypes = id => e => {
const { value } = e.target;
const { activity, updateActivity } = this.props;
const { types } = activity;

const newValue = types.includes(value)
? types.filter(t => t !== value)
: [...types, value].sort();

const data = { id, name: 'types', value: newValue };

updateActivity(data);
};

handleName = id => e => {
const { value } = e.target;
const { updateActivity } = this.props;
const name = 'name';
const data = { id, name, value };

updateActivity(data);
};

render() {
const { activity, num } = this.props;
const { id } = activity;

return (
<div className="flex items-center mb1">
<div className="mr1 bold mono">{num}.</div>
<div className="mr1 col-4">
<input
type="text"
className="col-12 input m0"
value={activity.name}
onChange={this.handleName(id)}
/>
</div>
<div>
{ACTIVITY_TYPES.map(option => (
<label key={option} className="mr1">
<input
type="checkbox"
value={option}
checked={activity.types.includes(option)}
onChange={this.handleTypes(id)}
/>
{option}
</label>
))}
</div>
</div>
);
}
}

ActivityListEntry.propTypes = {
activity: PropTypes.object.isRequired,
num: PropTypes.number.isRequired,
updateActivity: PropTypes.func.isRequired
};

const mapStateToProps = ({ activities: { byId } }, props) => ({
activity: byId[props.aId]
});

const mapDispatchToProps = {
updateActivity: updateActivityAction
};

export default connect(mapStateToProps, mapDispatchToProps)(ActivityListEntry);
Loading