diff --git a/.babelrc b/.babelrc index 3a9f24e750..64761adc35 100644 --- a/.babelrc +++ b/.babelrc @@ -1,12 +1,6 @@ { "presets": [ - [ - "env", - { - "targets": { "node": "7.9.0", "electron": "1.7.8" }, - "useBuiltIns": true - } - ], + ["env", { "targets": { "node": "8.2.1", "electron": "1.8.4" }, "useBuiltIns": true }], "stage-0", "react" ], diff --git a/.circleci/bash_env.sh b/.circleci/bash_env.sh new file mode 100644 index 0000000000..3c052eb798 --- /dev/null +++ b/.circleci/bash_env.sh @@ -0,0 +1,2 @@ +# nvm +source ~/.nvm/nvm.sh diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 0000000000..cc686ec917 --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,38 @@ +version: 2 +jobs: + build: + working_directory: ~/tidepool-org/chrome-uploader + parallelism: 1 + # CircleCI 2.0 does not support environment variables that refer to each other the same way as 1.0 did. + # If any of these refer to each other, rewrite them so that they don't or see https://circleci.com/docs/2.0/env-vars/#interpolating-environment-variables-to-set-other-environment-variables . + environment: + BASH_ENV: ".circleci/bash_env.sh" + macos: + xcode: '9.0' + steps: + - checkout + - run: echo 'export PATH=${PATH}:${HOME}/${CIRCLE_PROJECT_REPONAME}/node_modules/.bin' >> $BASH_ENV + - restore_cache: + key: dependency-cache-{{ checksum "package.json" }} + - run: + name: Install nvm and node + command: | + set +e + curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.5/install.sh | bash + source ~/.nvm/nvm.sh + nvm install v8.2.1 + nvm alias default v8.2.1 + - run: node -v + - run: curl -o- -L https://yarnpkg.com/install.sh | bash + - run: yarn config set cache-folder ~/.cache/yarn + - run: yarn --frozen-lockfile + - save_cache: + key: dependency-cache-{{ checksum "package.json" }} + paths: + - ~/.cache/yarn + - ./node_modules + # Test + - run: yarn lint + - run: yarn test + # Package + - run: yarn package diff --git a/.nvmrc b/.nvmrc index 4bc5d61816..2b0aa21219 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -7.9.0 +8.2.1 diff --git a/README.md b/README.md index b400074a37..8447f048e7 100644 --- a/README.md +++ b/README.md @@ -22,14 +22,14 @@ This README is focused on just the details of getting the uploader running local ## How to set it up 1. Clone this repository. -1. Make sure you have node v7.x installed. If you are managing node installations with [`nvm`](https://github.com/creationix/nvm 'GitHub: nvm'), which we **highly recommend**, you can just do `nvm use` when navigating to this repository to switch to the correct version of node. (In this repository, the correct version of node will always be the version of node packaged by the version of Electron that we are using and specified in the `.nvmrc` file.) -1. Check that you are also using npm v4.x, which should come with any node v7.x by default, but if not, run `npm install -g npm@4` to get the latest v4.x version. +1. Make sure you have node v8.x installed. If you are managing node installations with [`nvm`](https://github.com/creationix/nvm 'GitHub: nvm'), which we **highly recommend**, you can just do `nvm use` when navigating to this repository to switch to the correct version of node. (In this repository, the correct version of node will always be the version of node packaged by the version of Electron that we are using and specified in the `.nvmrc` file.) 1. Run `npm install` or, preferably, `yarn` 1. Set the config for the environment you want to target (see [Config](#config) below) 1. Run the following command: ```bash $ npm run dev ``` +or ```bash $ yarn dev ``` diff --git a/app/actions/async.js b/app/actions/async.js index e83fd8e93c..5e2dddff98 100644 --- a/app/actions/async.js +++ b/app/actions/async.js @@ -234,6 +234,7 @@ export function doDeviceUpload(driverId, opts = {}, utc) { targetId: uploadTargetUser, timezone: targetTimezones[uploadTargetUser], progress: actionUtils.makeProgressFn(dispatch), + dialogDisplay: actionUtils.makeDisplayModal(dispatch), version: version }); const { uploadsByUser } = getState(); diff --git a/app/actions/sync.js b/app/actions/sync.js index 6c684558af..b6915c9132 100644 --- a/app/actions/sync.js +++ b/app/actions/sync.js @@ -463,6 +463,16 @@ export function uploadFailure(err, errProps, device) { }; } +export function uploadCancelled(utc) { + return { + type: actionTypes.UPLOAD_CANCELLED, + payload: { utc }, + meta: { + source: actionSources[actionTypes.UPLOAD_CANCELLED] + } + }; +} + export function deviceDetectRequest() { return { type: actionTypes.DEVICE_DETECT_REQUEST, @@ -766,3 +776,25 @@ export function driverUpdateShellOpts(opts) { meta: {source: actionSources[actionTypes.DRIVER_INSTALL_SHELL_OPTS] } }; } + +export function deviceTimeIncorrect(callback, cfg, times) { + return { + type: actionTypes.DEVICE_TIME_INCORRECT, + payload: { callback, cfg, times }, + meta: { source: actionSources[actionTypes.DEVICE_TIME_INCORRECT] } + }; +} + +export function dismissedDeviceTimePromp() { + return { + type: actionTypes.DISMISS_DEVICE_TIME_PROMPT, + meta: { source: actionSources[actionTypes.DISMISS_DEVICE_TIME_PROMPT] } + }; +} + +export function timezoneBlur() { + return { + type: actionTypes.TIMEZONE_BLUR, + meta: { source: actionSources[actionTypes.TIMEZONE_BLUR] } + }; +} \ No newline at end of file diff --git a/app/actions/utils.js b/app/actions/utils.js index 26073f8f93..d03d276964 100644 --- a/app/actions/utils.js +++ b/app/actions/utils.js @@ -50,11 +50,20 @@ export function makeProgressFn(dispatch) { }; } +export function makeDisplayModal(dispatch) { + return (cb, cfg, times) => { + dispatch(syncActions.deviceTimeIncorrect(cb, cfg, times)); + }; +} + export function makeUploadCb(dispatch, getState, errCode, utc) { return (err, recs) => { const { devices, uploadsByUser, uploadTargetDevice, uploadTargetUser, version } = getState(); const targetDevice = devices[uploadTargetDevice]; if (err) { + if(err === 'deviceTimePromptClose'){ + return dispatch(syncActions.uploadCancelled(getUtc(utc))); + } // the drivers sometimes just pass a string arg as err, instead of an actual error :/ if (typeof err === 'string') { err = new Error(err); diff --git a/app/components/DeviceTimeModal.js b/app/components/DeviceTimeModal.js new file mode 100644 index 0000000000..77854d71a5 --- /dev/null +++ b/app/components/DeviceTimeModal.js @@ -0,0 +1,201 @@ +/* +* == BSD2 LICENSE == +* Copyright (c) 2014-2016, Tidepool Project +* +* This program is free software; you can redistribute it and/or modify it under +* the terms of the associated License, which is identical to the BSD 2-Clause +* License as published by the Open Source Initiative at opensource.org. +* +* This program is distributed in the hope that it will be useful, but WITHOUT +* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +* FOR A PARTICULAR PURPOSE. See the License for more details. +* +* You should have received a copy of the License along with this program; if +* not, you can obtain one from Tidepool Project at tidepool.org. +* == BSD2 LICENSE == +*/ + +import _ from 'lodash'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { bindActionCreators } from 'redux'; +import sundial from 'sundial'; + +import { sync as syncActions } from '../actions/'; + +import styles from '../../styles/components/DeviceTimeModal.module.less'; + +export class DeviceTimeModal extends Component { + determineDeviceType = () => { + const { showingDeviceTimePrompt } = this.props; + const { deviceTags } = showingDeviceTimePrompt.cfg; + if(_.indexOf(deviceTags, 'insulin-pump') !== -1){ + return 'insulin-pump'; + } + if(_.indexOf(deviceTags, 'cgm') !== -1){ + return 'cgm'; + } + if(_.indexOf(deviceTags, 'bgm') !== -1){ + return 'bgm'; + } + return 'unknown'; + } + + isAnimas = () => { + const { showingDeviceTimePrompt } = this.props; + const {deviceInfo} = showingDeviceTimePrompt.cfg; + return deviceInfo && deviceInfo.driverId && deviceInfo.driverId === 'Animas'; + } + + handleContinue = () => { + const { sync, showingDeviceTimePrompt } = this.props; + showingDeviceTimePrompt.callback(null); + sync.dismissedDeviceTimePromp(); + } + + handleCancel = () => { + const { sync, showingDeviceTimePrompt } = this.props; + showingDeviceTimePrompt.callback('deviceTimePromptClose'); + sync.dismissedDeviceTimePromp(); + } + + getActions = () => { + const buttons = []; + if ( !this.isAnimas() ) { + buttons.push( + , + ); + } + buttons.push( + + ); + + return buttons; + } + + getMessage = () => { + const type = this.determineDeviceType(); + let message; + switch (type) { + case 'insulin-pump': + message = ( +
+
+ Is your pump time set correctly? If not: +
+
1. Cancel the current upload
+
2. Check the time on your device
+
3. Check the time zone in the Uploader
+
+
+ ); + break; + case 'cgm': + message = ( +
+
+ Is your CGM time set correctly? If not: +
+
1. Cancel the current upload
+
2. Check the time on your device
+
3. Check the time zone in the Uploader
+
+
+ ); + break; + case 'bgm': + message = ( +
+
+ Is your meter time set correctly? If not: +
+
1. Cancel the current upload
+
2. Check the time on your device
+
3. Check the time zone in the Uploader
+
+
+ ); + break; + default: + break; + } + return message; + } + + getTitle = () => { + const type = this.determineDeviceType(); + let title; + switch (type) { + case 'insulin-pump': + title = 'Your pump doesn\'t appear to be in'; + break; + case 'cgm': + title = 'Your CGM doesn\'t appear to be in'; + break; + case 'bgm': + title = 'Your meter doesn\'t appear to be in'; + break; + default: + break; + } + return title; + } + + render() { + const { showingDeviceTimePrompt } = this.props; + + if(!showingDeviceTimePrompt){ + return null; + } + + const { showingDeviceTimePrompt: { cfg: { timezone }, times: { serverTime, deviceTime } } } = this.props; + + const title = this.getTitle(); + const message = this.getMessage(); + const actions = this.getActions(); + + return ( +
+
+
+
{title}
+
{`${timezone}:`}
+
+
+
+
+
{timezone}:
+
{sundial.formatInTimezone(serverTime, timezone, 'h:mm a')}
+
+
+
Device time:
+
{sundial.formatInTimezone(deviceTime, timezone, 'h:mm a')}
+
+
+
+ {message} +
+ {actions} +
+
+
+ ); + } +}; + +export default connect( + (state, ownProps) => { + return { + showingDeviceTimePrompt: state.showingDeviceTimePrompt + }; + }, + (dispatch) => { + return { + sync: bindActionCreators(syncActions, dispatch) + }; + } +)(DeviceTimeModal); diff --git a/app/components/TimezoneDropdown.js b/app/components/TimezoneDropdown.js index b34ca2b6d7..26d7b9a8bf 100644 --- a/app/components/TimezoneDropdown.js +++ b/app/components/TimezoneDropdown.js @@ -25,6 +25,16 @@ var cx = require('classnames'); var styles = require('../../styles/components/TimezoneDropdown.module.less'); class TimezoneDropdown extends React.Component { + constructor(props) { + super(props); + + this.timezoneSelect = null; + + this.setTimezoneSelect = element => { + this.timezoneSelect = element; + }; + } + static propTypes = { onTimezoneChange: PropTypes.func.isRequired, selectorLabel: PropTypes.string.isRequired, @@ -37,7 +47,9 @@ class TimezoneDropdown extends React.Component { dismissUpdateProfileError: PropTypes.func.isRequired, isClinicAccount: PropTypes.bool, userDropdownShowing: PropTypes.bool, - isUploadInProgress: PropTypes.bool.isRequired + isUploadInProgress: PropTypes.bool.isRequired, + onBlur: PropTypes.func.isRequired, + isTimezoneFocused: PropTypes.bool.isRequired }; componentWillReceiveProps(nextProps) { @@ -64,6 +76,12 @@ class TimezoneDropdown extends React.Component { clearInterval(this.updateSuggestedInterval); } + componentDidUpdate() { + if (this.timezoneSelect && this.props.isTimezoneFocused) { + this.timezoneSelect.focus(); + } + } + buildTzSelector = () => { function sortByOffset(timezones) { return _.sortBy(timezones, function(tz) { @@ -80,12 +98,14 @@ class TimezoneDropdown extends React.Component { return (