diff --git a/__test__/AssignmentSummary.test.js b/__test__/AssignmentSummary.test.js index eff06773c..c050241ad 100644 --- a/__test__/AssignmentSummary.test.js +++ b/__test__/AssignmentSummary.test.js @@ -4,7 +4,6 @@ import React from 'react' import { mount } from 'enzyme' import { StyleSheetTestUtils } from 'aphrodite' -import injectTapEventPlugin from 'react-tap-event-plugin' import each from 'jest-each' import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider' import { CardActions, CardTitle } from 'material-ui/Card' @@ -64,7 +63,6 @@ describe('AssignmentSummary text', function t() { }) describe('AssignmentSummary actions inUSA and NOT AllowSendAll', () => { - injectTapEventPlugin() // prevents warning function create(unmessaged, unreplied, badTimezone, past, skipped, isDynamic) { window.NOT_IN_USA = 0 window.ALLOW_SEND_ALL = false @@ -163,10 +161,7 @@ it('renders "Send later" when there is a badTimezoneCount', () => { }) describe('contacts filters', () => { - // These are an attempt to confirm that the buttons will work. - // It would be better to simulate clicking them, but I can't - // get it to work right now because of 'react-tap-event-plugin' - // some hints are here https://github.com/mui-org/material-ui/issues/4200#issuecomment-217738345 + // TODO: material-ui switch test to clicks it('filters correctly in USA', () => { window.NOT_IN_USA = 0 diff --git a/__test__/setup.js b/__test__/setup.js index 84aa76ff9..531721cdc 100644 --- a/__test__/setup.js +++ b/__test__/setup.js @@ -1,5 +1,5 @@ import { configure } from 'enzyme' -import Adapter from 'enzyme-adapter-react-15' +import Adapter from 'enzyme-adapter-react-16' configure({ adapter: new Adapter() }) diff --git a/docs/HOWTO-upgrade-react.md b/docs/HOWTO-upgrade-react.md new file mode 100644 index 000000000..76f8b8958 --- /dev/null +++ b/docs/HOWTO-upgrade-react.md @@ -0,0 +1,50 @@ +# Upgrading to React v16 + +## Upgrade Plan + +Upgrading to React v16 requires upgrading the following dependencies at the same time due to breaking internal API changes: `material-ui`, `webpack` + `react-hot-loader`, `react-apollo`, and `react-router`. The plan is to: + +1. Make changes common to each dependency upgrade. +2. For each dependency, open a PR getting it the first ~90% of the way there (the last ~10% requiring updates from another dependency) +3. Merge these dependency-specific PRs, resolving (hopefully minimal) conflicts +4. Disable all routes and get application to compile and run. +5. Open additional PRs to enable functionality route by route + +## Dependency Notes + +### `react` + +Replace `onTouchTap` with `onClick` and drop the [now-deprecated](https://github.com/zilverline/react-tap-event-plugin#deprecated) `react-tap-event-plugin`. + +### `webpack` + `react-hot-loader` + +`react-hot-loader` upgrade is a mix of instructions from: + +- https://github.com/gaearon/react-hot-loader#getting-started +- http://gaearon.github.io/react-hot-loader/getstarted/ + +Ex. the use of the `hot` HOC is listed as preferred in the repo's readme but not mentioned at all in the official documentation. + +### `material-ui` + +v1.2.0 adds longer term support, better layout support, and more functionality to `Table` components, which would be helpful for MoveOnOrg/Spoke#579 and addresses MoveOnOrg/Spoke#480. + +**Gotchas**: + +- Functionality for `AutoComplete` has been removed in favor of one of three 3rd party choices (98f6484) +- `DatePicker` does not have a working implementation in v1.2.0 (40604ae) +- Remove `react-addons-css-transition-group` (it's not being used anywhere) (70113cd) +- Update Toggle --> Switch (401da20) +- Update DropDownMenu --> List + Menu (d294314) +- Confirm that the [`material-ui-color-picker` package](https://www.npmjs.com/package/material-ui-color-picker) works with v1.2.0 + +### `react-router` + +Update `react-router` [from v2.5 to v4.3](https://github.com/ReactTraining/react-router/blob/master/packages/react-router/docs/guides/migrating.md). +Switch from deprecated `react-router-redux` to [`connected-react-router`](https://github.com/supasate/connected-react-router). + +### `react-apollo` + +Update `react-apollo` from v0.3 to v2.1. Inspired by [MoveOnOrg/Spoke#418](https://github.com/MoveOnOrg/Spoke/pull/418). + +* v2.x [drops `redux`](https://www.apollographql.com/docs/react/recipes/2.0-migration.html#redux) for its store diff --git a/package.json b/package.json index 587fd82a2..fe3e18ffe 100644 --- a/package.json +++ b/package.json @@ -87,7 +87,7 @@ "cookie-session": "^2.0.0-alpha.1", "dataloader": "^1.2.0", "dotenv": "^2.0.0", - "draft-js": "^0.7.0", + "draft-js": "^0.10.5", "express": "^4.14.0", "faker": "^3.1.0", "fs": "^0.0.2", @@ -121,16 +121,15 @@ "pg": "^6.4.2", "prop-types": "^15.6.0", "query-string": "^4.1.0", - "react": "^15.6.1", + "react": "^16.4.1", "react-addons-css-transition-group": "^15.2.1", "react-apollo": "^0.3.15", "react-async-script": "^0.6.0", "react-chartjs": "^0.7.3", - "react-dom": "^15.6.1", - "react-formal": "^0.18.5", + "react-dom": "^16.4.1", + "react-formal": "^0.28.4", "react-router": "^2.5.2", "react-router-redux": "^4.0.5", - "react-tap-event-plugin": "^2.0.0", "redis": "^2.8.0", "redux": "^3.7.2", "redux-thunk": "^2.1.0", @@ -151,7 +150,7 @@ "babel-plugin-transform-runtime": "^6.23.0", "babel-preset-es2017": "^6.24.1", "enzyme": "^3.3.0", - "enzyme-adapter-react-15": "^1.0.5", + "enzyme-adapter-react-16": "^1.1.1", "eslint": "2.13.1", "eslint-config-airbnb": "^9.0.1", "eslint-plugin-import": "^1.10.2", @@ -166,7 +165,7 @@ "prettier": "1.13.6", "react-hot-loader": "^1.3.0", "react-scripts": "^1.1.0", - "react-test-renderer": "15", + "react-test-renderer": "16.4.1", "regenerator-runtime": "^0.10.5", "selenium-webdriver": "^3.6.0", "sqlite3": "^3.1.9", diff --git a/src/components/App.jsx b/src/components/App.jsx index a945aaafa..b3ecf1277 100644 --- a/src/components/App.jsx +++ b/src/components/App.jsx @@ -4,16 +4,12 @@ import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider' import muiTheme from '../styles/mui-theme' import theme from '../styles/theme' import { StyleSheet, css } from 'aphrodite' -import injectTapEventPlugin from 'react-tap-event-plugin' import Form from 'react-formal' import GSTextField from './forms/GSTextField' import GSDateField from './forms/GSDateField' import GSScriptField from './forms/GSScriptField' import GSSelectField from './forms/GSSelectField' -// Needed for MaterialUI -injectTapEventPlugin() - Form.addInputTypes({ string: GSTextField, number: GSTextField, diff --git a/src/components/AssignmentTexter.jsx b/src/components/AssignmentTexter.jsx index 4206e8d20..cfad7ebd4 100644 --- a/src/components/AssignmentTexter.jsx +++ b/src/components/AssignmentTexter.jsx @@ -154,13 +154,13 @@ class AssignmentTexter extends React.Component { text={title} />, , diff --git a/src/components/BulkSendButton.jsx b/src/components/BulkSendButton.jsx index 84d4d4c97..6793ca7d3 100644 --- a/src/components/BulkSendButton.jsx +++ b/src/components/BulkSendButton.jsx @@ -33,7 +33,7 @@ export default class BulkSendButton extends Component { return (
this.setState({ showForm: false })} + onClick={() => this.setState({ showForm: false })} style={{ marginLeft: 5, display: 'inline-block' diff --git a/src/components/CampaignCannedResponsesForm.jsx b/src/components/CampaignCannedResponsesForm.jsx index fe7df3bee..8daef78c8 100644 --- a/src/components/CampaignCannedResponsesForm.jsx +++ b/src/components/CampaignCannedResponsesForm.jsx @@ -78,7 +78,7 @@ export default class CampaignCannedResponsesForm extends React.Component { secondary label='Add new canned response' icon={} - onTouchTap={() => this.setState({ showForm: true })} + onClick={() => this.setState({ showForm: true })} /> ) } @@ -92,7 +92,7 @@ export default class CampaignCannedResponsesForm extends React.Component { secondaryText={response.text} rightIconButton={( { + onClick={() => { const newVals = this.props.formValues.cannedResponses.map((responseToDelete) => { if (responseToDelete.id === response.id) { return null diff --git a/src/components/CampaignInteractionStepsForm.jsx b/src/components/CampaignInteractionStepsForm.jsx index 7fea6190e..2d7ad68b8 100644 --- a/src/components/CampaignInteractionStepsForm.jsx +++ b/src/components/CampaignInteractionStepsForm.jsx @@ -135,7 +135,7 @@ export default class CampaignInteractionStepsForm extends React.Component { fullWidth hintText='Answer to the previous question' /> : ''} - {interactionStep.parentInteractionId ? : ''} + {interactionStep.parentInteractionId ? : ''} {interactionStep.parentInteractionId && this.props.availableActions && this.props.availableActions.length ? (
: ''} @@ -217,7 +217,7 @@ export default class CampaignInteractionStepsForm extends React.Component { {...dataTest('interactionSubmit')} primary label={this.props.saveLabel} - onTouchTap={this.onSave.bind(this)} + onClick={this.onSave.bind(this)} />
) diff --git a/src/components/CampaignTextersForm.jsx b/src/components/CampaignTextersForm.jsx index 0ba165583..a60ea70e8 100644 --- a/src/components/CampaignTextersForm.jsx +++ b/src/components/CampaignTextersForm.jsx @@ -396,7 +396,7 @@ export default class CampaignTextersForm extends React.Component { : ''}
{ + onClick={async () => { const currentFormValues = this.formValues() const newFormValues = { ...currentFormValues @@ -468,7 +468,7 @@ export default class CampaignTextersForm extends React.Component { this.addAllTexters())} + onClick={(() => this.addAllTexters())} />
diff --git a/src/components/Chip.jsx b/src/components/Chip.jsx index 10ee96bd5..de0119644 100644 --- a/src/components/Chip.jsx +++ b/src/components/Chip.jsx @@ -29,11 +29,11 @@ const styles = { } } -function Chip({ text, iconRightClass, onIconRightTouchTap, onTouchTap, style = {} }) { +function Chip({ text, iconRightClass, onIconRightTouchTap, onClick, style = {} }) { return ( -
+
{text} - {iconRightClass ? React.createElement(iconRightClass, { style: styles.icon, onTouchTap: onIconRightTouchTap }) : ''} + {iconRightClass ? React.createElement(iconRightClass, { style: styles.icon, onClick: onIconRightTouchTap }) : ''}
) } @@ -42,7 +42,7 @@ Chip.propTypes = { text: PropTypes.element, iconRightClass: PropTypes.string, onIconRightTouchTap: PropTypes.func, - onTouchTap: PropTypes.func, + onClick: PropTypes.func, style: PropTypes.object } diff --git a/src/components/ConfirmButton.jsx b/src/components/ConfirmButton.jsx index 2242ce3bc..270b59c7b 100644 --- a/src/components/ConfirmButton.jsx +++ b/src/components/ConfirmButton.jsx @@ -46,7 +46,7 @@ export default class ConfirmButton extends Component { return (
props.router.push(section.url)} + onClick={() => props.router.push(section.url)} /> ))} diff --git a/src/components/ScriptEditor.jsx b/src/components/ScriptEditor.jsx index ef5a33e8f..7a0a7e8fa 100644 --- a/src/components/ScriptEditor.jsx +++ b/src/components/ScriptEditor.jsx @@ -162,7 +162,7 @@ class ScriptEditor extends React.Component { this.addCustomField(field)} + onClick={() => this.addCustomField(field)} /> ))}
diff --git a/src/components/ScriptList.jsx b/src/components/ScriptList.jsx index acfde51f5..232064f4b 100644 --- a/src/components/ScriptList.jsx +++ b/src/components/ScriptList.jsx @@ -82,12 +82,12 @@ class ScriptList extends React.Component { // targetOrigin={{horizontal: 'left', vertical: 'bottom'}} // > // this.handleEditScript(script)} + // onClick={() => this.handleEditScript(script)} // /> // { // script.isUserCreated ? ( // this.handleDeleteScript(script.id)} + // onClick={() => this.handleDeleteScript(script.id)} // /> // ) : '' // } @@ -98,7 +98,7 @@ class ScriptList extends React.Component { const listItems = scripts.map((script) => ( onSelectCannedResponse(script)} + onClick={() => onSelectCannedResponse(script)} key={script.id} primaryText={script.title} secondaryText={script.text} @@ -123,7 +123,7 @@ class ScriptList extends React.Component { } - onTouchTap={this.handleOpenDialog} + onClick={this.handleOpenDialog} /> ) : ''} @@ -133,7 +133,7 @@ class ScriptList extends React.Component { actions={[ , { - - return ( -
-

Frequently Asked Questions

- {faqs.map((faq, idx) => ( - - - -

{faq.answer}

-
-
- ))} -
- ) -} - -TexterFaqs.propTypes = { - faqs: PropTypes.array -} - -export default TexterFaqs diff --git a/src/components/TexterFaqs.jsx b/src/components/TexterFaqs.jsx index ba62580e7..455c8bf5b 100644 --- a/src/components/TexterFaqs.jsx +++ b/src/components/TexterFaqs.jsx @@ -3,7 +3,6 @@ import PropTypes from 'prop-types' import { Card, CardTitle, CardText } from 'material-ui/Card' const TexterFaqs = ({ faqs }) => { - return (

Frequently Asked Questions

diff --git a/src/components/forms/GSScriptField.jsx b/src/components/forms/GSScriptField.jsx index 975af1c0a..c657bc115 100644 --- a/src/components/forms/GSScriptField.jsx +++ b/src/components/forms/GSScriptField.jsx @@ -56,12 +56,12 @@ export default class GSScriptField extends GSFormField { , ]} @@ -87,7 +87,7 @@ export default class GSScriptField extends GSFormField { { + onClick={(event) => { event.stopPropagation() }} floatingLabelText={this.floatingLabelText()} diff --git a/src/containers/AdminCampaignEdit.jsx b/src/containers/AdminCampaignEdit.jsx index ec536814d..301327f17 100644 --- a/src/containers/AdminCampaignEdit.jsx +++ b/src/containers/AdminCampaignEdit.jsx @@ -453,12 +453,12 @@ class AdminCampaignEdit extends React.Component { {this.props.campaignData.campaign.isArchived ? ( await this.props.mutations.unarchiveCampaign(this.props.campaignData.campaign.id)} + onClick={async() => await this.props.mutations.unarchiveCampaign(this.props.campaignData.campaign.id)} /> ) : ( await this.props.mutations.archiveCampaign(this.props.campaignData.campaign.id)} + onClick={async() => await this.props.mutations.archiveCampaign(this.props.campaignData.campaign.id)} /> )} { + onClick={async () => { this.setState({ startingCampaign: true }) diff --git a/src/containers/AdminCampaignList.jsx b/src/containers/AdminCampaignList.jsx index 487a4efdd..b1ae8abb6 100644 --- a/src/containers/AdminCampaignList.jsx +++ b/src/containers/AdminCampaignList.jsx @@ -78,7 +78,7 @@ class AdminCampaignList extends React.Component { ( diff --git a/src/containers/AdminCampaignStats.jsx b/src/containers/AdminCampaignStats.jsx index 537ba0c59..589d1c1c8 100644 --- a/src/containers/AdminCampaignStats.jsx +++ b/src/containers/AdminCampaignStats.jsx @@ -141,7 +141,7 @@ class AdminCampaignStats extends React.Component { return ( await this.props.mutations.copyCampaign(this.props.params.campaignId)} + onClick={async() => await this.props.mutations.copyCampaign(this.props.params.campaignId)} /> ) } @@ -175,7 +175,7 @@ class AdminCampaignStats extends React.Component { {!campaign.isArchived ? ( // edit this.props.router.push(`/admin/${organizationId}/campaigns/${campaignId}/edit`)} + onClick={() => this.props.router.push(`/admin/${organizationId}/campaigns/${campaignId}/edit`)} label='Edit' /> ) : null} @@ -183,7 +183,7 @@ class AdminCampaignStats extends React.Component { [ // Buttons for Admins (and not Supervolunteers) ( // export { + onClick={async () => { this.setState({ exportMessageOpen: true, disableExportButton: true @@ -201,19 +201,19 @@ class AdminCampaignStats extends React.Component { ( // unarchive campaign.isArchived ? await this.props.mutations.unarchiveCampaign(campaignId)} + onClick={async () => await this.props.mutations.unarchiveCampaign(campaignId)} label='Unarchive' /> : null), ( // archive !campaign.isArchived ? await this.props.mutations.archiveCampaign(campaignId)} + onClick={async () => await this.props.mutations.archiveCampaign(campaignId)} label='Archive' /> : null), ( // copy await this.props.mutations.copyCampaign(this.props.params.campaignId)} + onClick={async() => await this.props.mutations.copyCampaign(this.props.params.campaignId)} />) ] : null}
diff --git a/src/containers/AdminNavigation.jsx b/src/containers/AdminNavigation.jsx index 01abb3043..735f658cb 100644 --- a/src/containers/AdminNavigation.jsx +++ b/src/containers/AdminNavigation.jsx @@ -23,7 +23,7 @@ class AdminNavigation extends React.Component { this.props.router.push(`/app/${organizationId}/todos`)} + onClick={() => this.props.router.push(`/app/${organizationId}/todos`)} /> } /> diff --git a/src/containers/AdminPersonList.jsx b/src/containers/AdminPersonList.jsx index faae31f45..d5fd7bf34 100644 --- a/src/containers/AdminPersonList.jsx +++ b/src/containers/AdminPersonList.jsx @@ -108,7 +108,7 @@ class AdminPersonList extends React.Component { { this.editUser(person.id) }} + onClick={() => { this.editUser(person.id) }} /> @@ -127,7 +127,7 @@ class AdminPersonList extends React.Component { @@ -151,7 +151,7 @@ class AdminPersonList extends React.Component { {...dataTest('inviteOk')} label='OK' primary - onTouchTap={this.handleClose} + onClick={this.handleClose} /> ]} modal={false} diff --git a/src/containers/AssignmentTexterContact.jsx b/src/containers/AssignmentTexterContact.jsx index 0038b8ec5..bf587cdb1 100644 --- a/src/containers/AssignmentTexterContact.jsx +++ b/src/containers/AssignmentTexterContact.jsx @@ -183,7 +183,7 @@ export class AssignmentTexterContact extends React.Component { let disabled = false let disabledText = 'Sending...' - let snackbarOnTouchTap = null + let snackbarOnClick = null let snackbarActionTitle = null let snackbarError = null @@ -191,7 +191,7 @@ export class AssignmentTexterContact extends React.Component { disabledText = '' disabled = true snackbarError = 'Your assignment has changed' - snackbarOnTouchTap = this.goBackToTodos + snackbarOnClick = this.goBackToTodos snackbarActionTitle = 'Back to Todos' } else if (contact.optOut) { disabledText = 'Skipping opt-out...' @@ -209,7 +209,7 @@ export class AssignmentTexterContact extends React.Component { questionResponses, snackbarError, snackbarActionTitle, - snackbarOnTouchTap, + snackbarOnClick, optOutMessageText: "I'm opting you out of texts immediately. Have a great day.", responsePopoverOpen: false, messageText: this.getStartingMessageText(), @@ -349,7 +349,7 @@ export class AssignmentTexterContact extends React.Component { if (e.message === 'Your assignment has changed') { newState.snackbarActionTitle = 'Back to todos' - newState.snackbarOnTouchTap = this.goBackToTodos + newState.snackbarOnClick = this.goBackToTodos this.setState(newState) } else { // opt out or send message Error @@ -587,12 +587,12 @@ export class AssignmentTexterContact extends React.Component { let button = null if (messageStatus === 'closed') { button = ( this.handleEditMessageStatus('needsResponse')} + onClick={() => this.handleEditMessageStatus('needsResponse')} label='Reopen' />) } else if (messageStatus === 'needsResponse') { button = () } @@ -649,13 +649,13 @@ export class AssignmentTexterContact extends React.Component { {this.renderNeedsResponseToggleButton(contact)}
@@ -712,7 +712,7 @@ export class AssignmentTexterContact extends React.Component { onOptOut={this.handleNavigateNext} rightToolbarIcon={(
) diff --git a/src/containers/AssignmentTexterSurveyDropdown.jsx b/src/containers/AssignmentTexterSurveyDropdown.jsx index d01a686db..ed877c447 100644 --- a/src/containers/AssignmentTexterSurveyDropdown.jsx +++ b/src/containers/AssignmentTexterSurveyDropdown.jsx @@ -63,7 +63,7 @@ class AssignmentTexterSurveyDropdown extends Component { key='clear' value='clearResponse' primaryText='Clear response' - // onTouchTap={(event) => this.handleAnswerDelete(event, step.id)} + // onClick={(event) => this.handleAnswerDelete(event, step.id)} /> ) diff --git a/src/containers/CampaignList.jsx b/src/containers/CampaignList.jsx index 4a16a72c0..d9e58cd30 100644 --- a/src/containers/CampaignList.jsx +++ b/src/containers/CampaignList.jsx @@ -91,7 +91,7 @@ class CampaignList extends React.Component { style={listItemStyle} key={campaign.id} primaryText={primaryText} - onTouchTap={() => (!isStarted ? + onClick={() => (!isStarted ? this.props.router.push(`${campaignUrl}/edit`) : this.props.router.push(campaignUrl))} secondaryText={secondaryText} @@ -100,14 +100,14 @@ class CampaignList extends React.Component { (campaign.isArchived ? ( this.props.mutations.unarchiveCampaign(campaign.id)} + onClick={async () => this.props.mutations.unarchiveCampaign(campaign.id)} > ) : ( this.props.mutations.archiveCampaign(campaign.id)} + onClick={async () => this.props.mutations.archiveCampaign(campaign.id)} > diff --git a/src/containers/Settings.jsx b/src/containers/Settings.jsx index c9bf5423d..532754af8 100644 --- a/src/containers/Settings.jsx +++ b/src/containers/Settings.jsx @@ -98,7 +98,7 @@ class Settings extends React.Component { ) : ''} diff --git a/src/containers/UserMenu.jsx b/src/containers/UserMenu.jsx index 57219aca6..5eb7e47a6 100644 --- a/src/containers/UserMenu.jsx +++ b/src/containers/UserMenu.jsx @@ -88,7 +88,7 @@ class UserMenu extends Component { return (
{this.renderAvatar(currentUser, avatarSize)} diff --git a/src/store/actions/index.js b/src/store/actions/index.js deleted file mode 100644 index 40d700724..000000000 --- a/src/store/actions/index.js +++ /dev/null @@ -1,8 +0,0 @@ -export const ADD_COUNT = 'ADD_COUNT' - -export function addCount(amount) { - return { - type: ADD_COUNT, - payload: amount - } -} diff --git a/src/store/index.js b/src/store/index.js index 7da9941aa..680631ab7 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -2,12 +2,10 @@ import { createStore, combineReducers, compose, applyMiddleware } from 'redux' import { routerReducer, routerMiddleware } from 'react-router-redux' import ReduxThunk from 'redux-thunk' import ApolloClientSingleton from '../network/apollo-client-singleton' -import * as reducers from './reducers' export default class Store { constructor(history, initialState = {}) { const reducer = combineReducers({ - ...reducers, apollo: ApolloClientSingleton.reducer(), routing: routerReducer }) diff --git a/src/store/reducers/count.js b/src/store/reducers/count.js deleted file mode 100644 index 941b5f98b..000000000 --- a/src/store/reducers/count.js +++ /dev/null @@ -1,8 +0,0 @@ -import { ADD_COUNT } from '../actions' - -export default function (state = 0, action) { - if (action.type === ADD_COUNT) { - return state + action.payload - } - return state -} diff --git a/src/store/reducers/index.js b/src/store/reducers/index.js deleted file mode 100644 index 2907d1716..000000000 --- a/src/store/reducers/index.js +++ /dev/null @@ -1 +0,0 @@ -export count from './count'