From 251c10d6de6fbff9637fa0004b939f8689bf7a2a Mon Sep 17 00:00:00 2001 From: Aaron Imming Date: Tue, 19 Mar 2019 11:40:04 +0800 Subject: [PATCH 01/42] Add integrations button to toolbar --- templates/bot.mustache | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/templates/bot.mustache b/templates/bot.mustache index 339b21e47d..5c29c8d3c4 100644 --- a/templates/bot.mustache +++ b/templates/bot.mustache @@ -14,7 +14,9 @@
+ + @@ -109,8 +111,9 @@
- + + From ff1d3691627dd0b677ca65f9af8215f8bb71c3ee Mon Sep 17 00:00:00 2001 From: Aaron Imming Date: Tue, 19 Mar 2019 11:41:23 +0800 Subject: [PATCH 02/42] Add icon and create rules for integrations icon --- static/css/_fontello.scss | 6 ++++++ static/image/integrations.svg | 1 + 2 files changed, 7 insertions(+) create mode 100644 static/image/integrations.svg diff --git a/static/css/_fontello.scss b/static/css/_fontello.scss index 01fa493b74..055d26307e 100755 --- a/static/css/_fontello.scss +++ b/static/css/_fontello.scss @@ -89,3 +89,9 @@ background: url("../image/clear-disabled.svg") no-repeat center; } } +.icon-integrations { + &:before { + content: ' '; + } + background: url("../image/integrations.svg") no-repeat center; +} diff --git a/static/image/integrations.svg b/static/image/integrations.svg new file mode 100644 index 0000000000..7bf82226a1 --- /dev/null +++ b/static/image/integrations.svg @@ -0,0 +1 @@ + \ No newline at end of file From 4a406073f1f89b3c6d8ad1e502e03afc1995593d Mon Sep 17 00:00:00 2001 From: Aaron Imming Date: Tue, 19 Mar 2019 11:42:34 +0800 Subject: [PATCH 03/42] Export loadWorkspace & loadBlocks functions and create common cleanBeforeExport function --- src/botPage/view/blockly/index.js | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/src/botPage/view/blockly/index.js b/src/botPage/view/blockly/index.js index a4fc3583d5..f464a78d63 100644 --- a/src/botPage/view/blockly/index.js +++ b/src/botPage/view/blockly/index.js @@ -13,6 +13,7 @@ import { fixArgumentAttribute, removeUnavailableMarkets, strategyHasValidTradeTypeCategory, + cleanBeforeExport, } from './utils'; import Interpreter from '../../bot/Interpreter'; import createError from '../../common/error'; @@ -75,7 +76,7 @@ const marketsWereRemoved = xml => { } return false; }; -const loadWorkspace = xml => { +export const loadWorkspace = xml => { if (!strategyHasValidTradeTypeCategory(xml)) return; if (marketsWereRemoved(xml)) return; @@ -101,7 +102,7 @@ const loadWorkspace = xml => { ); }; -const loadBlocks = (xml, dropEvent = {}) => { +export const loadBlocks = (xml, dropEvent = {}) => { if (!strategyHasValidTradeTypeCategory(xml)) return; if (marketsWereRemoved(xml)) return; @@ -293,7 +294,7 @@ export default class _Blockly { try { xml = Blockly.Xml.textToDom(blockStr); } catch (e) { - throw createError('FileLoad', translate('Unrecognized file format.')); + throw createError('FileLoad', translate('Unrecognized file format')); } try { @@ -303,7 +304,7 @@ export default class _Blockly { loadWorkspace(xml); } } catch (e) { - throw createError('FileLoad', translate('Unable to load the block file.')); + throw createError('FileLoad', translate('Unable to load the block file')); } } /* eslint-disable class-methods-use-this */ @@ -311,15 +312,10 @@ export default class _Blockly { const { filename, collection } = arg; setBeforeUnload(true); + const xml = Blockly.Xml.workspaceToDom(Blockly.mainWorkspace); - Array.from(xml.children).forEach(blockDom => { - const blockId = blockDom.getAttribute('id'); - if (!blockId) return; - const block = Blockly.mainWorkspace.getBlockById(blockId); - if ('loaderId' in block) { - blockDom.remove(); - } - }); + cleanBeforeExport(xml); + save(filename, collection, xml); } run(limitations = {}) { From b39b9e5230a0d183c47d9b39bfde92261de74dc2 Mon Sep 17 00:00:00 2001 From: Aaron Imming Date: Tue, 19 Mar 2019 11:43:09 +0800 Subject: [PATCH 04/42] Create logic for cleanBeforeExport function --- src/botPage/view/blockly/utils.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/botPage/view/blockly/utils.js b/src/botPage/view/blockly/utils.js index 237b7af0c9..cacb8aa752 100644 --- a/src/botPage/view/blockly/utils.js +++ b/src/botPage/view/blockly/utils.js @@ -495,3 +495,14 @@ export const hideInteractionsFromBlockly = callback => { callback(); Blockly.Events.recordUndo = true; }; + +export const cleanBeforeExport = xml => { + Array.from(xml.children).forEach(blockDom => { + const blockId = blockDom.getAttribute('id'); + if (!blockId) return; + const block = Blockly.mainWorkspace.getBlockById(blockId); + if ('loaderId' in block) { + blockDom.remove(); + } + }); +}; From 66b55144f0885e6dbce038bbda6f55753b24bf75 Mon Sep 17 00:00:00 2001 From: Aaron Imming Date: Tue, 19 Mar 2019 11:44:56 +0800 Subject: [PATCH 05/42] Rename Save to SaveDialog, add LoadDialog + IntegrationsDialog --- .../view/Dialogs/IntegrationsDialog.js | 42 +++++ src/botPage/view/Dialogs/LoadDialog.js | 90 +++++++++ src/botPage/view/Dialogs/Save.js | 95 ---------- src/botPage/view/Dialogs/SaveDialog.js | 176 ++++++++++++++++++ 4 files changed, 308 insertions(+), 95 deletions(-) create mode 100644 src/botPage/view/Dialogs/IntegrationsDialog.js create mode 100644 src/botPage/view/Dialogs/LoadDialog.js delete mode 100644 src/botPage/view/Dialogs/Save.js create mode 100644 src/botPage/view/Dialogs/SaveDialog.js diff --git a/src/botPage/view/Dialogs/IntegrationsDialog.js b/src/botPage/view/Dialogs/IntegrationsDialog.js new file mode 100644 index 0000000000..812919f2fd --- /dev/null +++ b/src/botPage/view/Dialogs/IntegrationsDialog.js @@ -0,0 +1,42 @@ +import React, { PureComponent } from 'react'; +import Dialog from './Dialog'; +import { translate } from '../../../common/i18n'; +import GoogleDriveIntegration from '../react-components/Integrations/GoogleDriveIntegration'; +import * as style from '../style'; +import { observer as globalObserver } from '../../../common/utils/observer'; + +class IntegrationsContent extends PureComponent { + constructor() { + super(); + } + + componentDidMount() { + globalObserver.register('dialog.load.opened', this.props.closeDialog); + globalObserver.register('dialog.save.opened', this.props.closeDialog); + } + + render() { + return ( +
+ +
+ ); + } +} + +export default class IntegrationsDialog extends Dialog { + constructor() { + const closeDialog = () => { + this.close(); + }; + super('integrations-dialog', translate('Integrations'), , { + width : 500, + height: 'auto', + }); + } + + open() { + super.open(); + globalObserver.emit('dialog.integrations.opened'); + } +} diff --git a/src/botPage/view/Dialogs/LoadDialog.js b/src/botPage/view/Dialogs/LoadDialog.js new file mode 100644 index 0000000000..934faf701e --- /dev/null +++ b/src/botPage/view/Dialogs/LoadDialog.js @@ -0,0 +1,90 @@ +import React, { PureComponent } from 'react'; +import Dialog from './Dialog'; +import { translate } from '../../../common/i18n'; +import * as style from '../style'; +import googleDrive from '../../../common/integrations/gdrive'; +import { observer as globalObserver } from '../../../common/utils/observer'; + +class LoadContent extends PureComponent { + constructor() { + super(); + this.state = { loadType: 'local' }; + } + + componentDidMount() { + globalObserver.register('dialog.save.opened', this.props.closeDialog); + globalObserver.register('dialog.integrations.opened', this.props.closeDialog); + } + + onChange(event) { + this.setState({ loadType: event.target.value }); + } + + submit() { + switch (this.state.loadType) { + case 'google-drive': + googleDrive.createFolderPicker().then(() => this.props.closeDialog()); + break; + default: + $('#files').click(); + this.props.closeDialog(); + break; + } + } + + render() { + return ( +
this.submit()} + > +
+ + this.onChange(e)} + /> + + + + this.onChange(e)} + /> + + +
+
+ +
+
+ ); + } + static props: { closeDialog: PropTypes.func }; +} + +export default class LoadDialog extends Dialog { + constructor() { + const closeDialog = () => { + this.close(); + }; + super('load-dialog', translate('Load blocks'), , style.dialogLayout); + } + + open() { + super.open(); + globalObserver.emit('dialog.load.opened'); + } +} diff --git a/src/botPage/view/Dialogs/Save.js b/src/botPage/view/Dialogs/Save.js deleted file mode 100644 index 43343851fa..0000000000 --- a/src/botPage/view/Dialogs/Save.js +++ /dev/null @@ -1,95 +0,0 @@ -import React, { PureComponent } from 'react'; -import PropTypes from 'prop-types'; -import { translate } from '../../../common/i18n'; -import * as style from '../style'; -import Dialog from './Dialog'; - -class SaveContent extends PureComponent { - constructor() { - super(); - this.state = { - error: null, - }; - } - submit() { - const filename = $(this.filename).val(); - const collection = $(this.isCollection).prop('checked'); - - this.props.onSave({ - filename, - collection, - }); - } - render() { - return ( -
this.submit()} - className="dialog-content" - style={style.content} - > -
-
- -
-
- { - this.isCollection = el; - }} - style={style.checkbox} - /> - -
-
-
- -
-
- ); - } - static props: { - onSave: PropTypes.func, - }; -} - -export default class Save extends Dialog { - constructor() { - const onSave = arg => { - this.limitsPromise(arg); - this.close(); - }; - super('save-dialog', translate('Save blocks as'), , style.dialogLayout); - } - save() { - this.open(); - return new Promise(resolve => { - this.limitsPromise = resolve; - }); - } -} diff --git a/src/botPage/view/Dialogs/SaveDialog.js b/src/botPage/view/Dialogs/SaveDialog.js new file mode 100644 index 0000000000..ce2def9314 --- /dev/null +++ b/src/botPage/view/Dialogs/SaveDialog.js @@ -0,0 +1,176 @@ +import React, { PureComponent } from 'react'; +import PropTypes from 'prop-types'; +import { translate } from '../../../common/i18n'; +import * as style from '../style'; +import Dialog from './Dialog'; +import googleDrive from '../../../common/integrations/gdrive'; +import { cleanBeforeExport } from '../blockly/utils'; +import { observer as globalObserver } from '../../../common/utils/observer'; + +class SaveContent extends PureComponent { + constructor() { + super(); + this.state = { + error : null, + saveType: 'local', + }; + } + + componentDidMount() { + globalObserver.register('dialog.load.opened', this.props.closeDialog); + globalObserver.register('dialog.integrations.opened', this.props.closeDialog); + } + + submit() { + const filename = $(this.filename).val() || 'binary-bot'; + const collection = $(this.isCollection).prop('checked'); + + switch (this.state.saveType) { + case 'google-drive': + const xml = Blockly.Xml.workspaceToDom(Blockly.mainWorkspace); + cleanBeforeExport(xml); + + xml.setAttribute('xmlns', 'http://www.w3.org/1999/xhtml'); + xml.setAttribute('collection', collection); + + googleDrive + .saveFile({ + name : filename, + content : Blockly.Xml.domToPrettyText(xml), + mimeType: 'application/xml', + }) + .then(() => { + globalObserver.emit('ui.log.success', translate('Successfully uploaded to Google Drive')); + this.props.closeDialog(); + }) + .catch(e => { + globalObserver.emit('ui.log.warn', e.message); + }); + break; + + default: + this.props.onSave({ + filename, + collection, + }); + break; + } + } + + onChange(event) { + this.setState({ saveType: event.target.value }); + } + + render() { + return ( +
this.submit()} + className="dialog-content" + style={style.content} + > +
+ { + this.filename = el; + }} + defaultValue="binary-bot" + data-lpignore="true" + autoComplete="false" + /> +
+
+ + this.onChange(e)} + /> + + + + this.onChange(e)} + /> + + +
+
+ { + this.isCollection = el; + }} + style={style.checkbox} + /> + +
+ {translate('Save your blocks and settings for re-use in other strategies')} +
+
+
+ +
+
+ ); + } + + static props: { + onSave: PropTypes.func, + closeDialog: PropTypes.func, + }; +} + +export default class SaveDialog extends Dialog { + constructor() { + const closeDialog = () => { + this.close(); + }; + const onSave = arg => { + this.limitsPromise(arg); + closeDialog(); + }; + super( + 'save-dialog', + translate('Save blocks'), + , + style.dialogLayout + ); + } + + save() { + this.open(); + return new Promise(resolve => { + this.limitsPromise = resolve; + }); + } + + open() { + super.open(); + globalObserver.emit('dialog.save.opened'); + } +} From 2597c220e872d00cac2bd014ce4c4267288ce8de Mon Sep 17 00:00:00 2001 From: Aaron Imming Date: Tue, 19 Mar 2019 11:46:43 +0800 Subject: [PATCH 06/42] Update import paths --- src/botPage/view/Dialogs/LoadDialog.js | 2 +- src/botPage/view/Dialogs/SaveDialog.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/botPage/view/Dialogs/LoadDialog.js b/src/botPage/view/Dialogs/LoadDialog.js index 934faf701e..956108161c 100644 --- a/src/botPage/view/Dialogs/LoadDialog.js +++ b/src/botPage/view/Dialogs/LoadDialog.js @@ -2,7 +2,7 @@ import React, { PureComponent } from 'react'; import Dialog from './Dialog'; import { translate } from '../../../common/i18n'; import * as style from '../style'; -import googleDrive from '../../../common/integrations/gdrive'; +import googleDrive from '../../../common/integrations/GoogleDrive'; import { observer as globalObserver } from '../../../common/utils/observer'; class LoadContent extends PureComponent { diff --git a/src/botPage/view/Dialogs/SaveDialog.js b/src/botPage/view/Dialogs/SaveDialog.js index ce2def9314..090036682c 100644 --- a/src/botPage/view/Dialogs/SaveDialog.js +++ b/src/botPage/view/Dialogs/SaveDialog.js @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import { translate } from '../../../common/i18n'; import * as style from '../style'; import Dialog from './Dialog'; -import googleDrive from '../../../common/integrations/gdrive'; +import googleDrive from '../../../common/integrations/GoogleDrive'; import { cleanBeforeExport } from '../blockly/utils'; import { observer as globalObserver } from '../../../common/utils/observer'; From 2a97b7cbd9aaa9c85ceeeec71b401e697204d039 Mon Sep 17 00:00:00 2001 From: Aaron Imming Date: Tue, 19 Mar 2019 11:47:02 +0800 Subject: [PATCH 07/42] Create GoogleDriveIntegration component --- .../Integrations/GoogleDriveIntegration.js | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 src/botPage/view/react-components/Integrations/GoogleDriveIntegration.js diff --git a/src/botPage/view/react-components/Integrations/GoogleDriveIntegration.js b/src/botPage/view/react-components/Integrations/GoogleDriveIntegration.js new file mode 100644 index 0000000000..ac145e9666 --- /dev/null +++ b/src/botPage/view/react-components/Integrations/GoogleDriveIntegration.js @@ -0,0 +1,45 @@ +import React, { PureComponent } from 'react'; +import { translate } from '../../../../common/i18n'; +import googleDrive from '../../../../common/integrations/GoogleDrive'; +import { observer as globalObserver } from '../../../../common/utils/observer'; + +export default class GoogleDriveIntegration extends PureComponent { + constructor() { + super(); + this.state = { isAuthorised: false }; + } + + componentDidMount() { + globalObserver.register('integrations.googledrive', data => this.setState(data)); + } + + render() { + return ( +
+
+

Google Drive

+
{translate('Save blocks and strategies to')} Google Drive
+ {googleDrive.isAuthorised && ( +
+ {`${translate('You are logged in as')} ${googleDrive.profile.getEmail()}`} +
+ )} +
+ +
+ ); + } +} From cdcadb4fcbbe1ec6895dfa80620accdfb602e77e Mon Sep 17 00:00:00 2001 From: Aaron Imming Date: Tue, 19 Mar 2019 11:47:46 +0800 Subject: [PATCH 08/42] Create styling rules for load-dialog, save-dialog, integrations-dialog --- static/css/_panel.scss | 68 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/static/css/_panel.scss b/static/css/_panel.scss index b0c85397ad..bb4f27cddc 100644 --- a/static/css/_panel.scss +++ b/static/css/_panel.scss @@ -321,3 +321,71 @@ $disabled-color: #F2F2F2; .ui-dialog-content { padding: 0px !important; } + +#load-dialog, #save-dialog { + .integration-option { + margin: 0.5em; + } +} + +#load-dialog, #save-dialog, #integrations-dialog { + .input-row { + margin: 1em 0; + } + .input-row.last { + margin-bottom: 0; + } + & .description { + font-size: 75%; + } +} + +#save-dialog { + #save-filename { + width: 100%; + height: 40px; + } + #collection { + padding: 1.2em; + border: 1px solid $brand-gray; + border-radius: 10px; + & .description { + margin-left: 21px; + margin-top: 0.5em; + margin-bottom: 0; + } + } +} + +#integrations-dialog { + .integration { + display: table; + width: 100%; + + h2 { + color: $black; + font-size: 20px; + margin: 0 0 0.1em 0; + } + .description { + margin: 0 0 0.1em 0; + } + .left { + width: 60%; + display: table-cell; + vertical-align: top; + } + .right { + width: 39%; + display: table-cell; + text-align: right; + & > a { + display: block; + } + } + .integration-user { + color: $brand-dark-gray; + font-size: 75%; + } + } +} From 6524861015b3c7cdfab10e81762897c9a90e7d9f Mon Sep 17 00:00:00 2001 From: Aaron Imming Date: Tue, 19 Mar 2019 11:51:35 +0800 Subject: [PATCH 09/42] Logout Google account on logout, add binds for toolbox buttons --- src/botPage/view/View.js | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/botPage/view/View.js b/src/botPage/view/View.js index be027629c8..ef8ce478c3 100644 --- a/src/botPage/view/View.js +++ b/src/botPage/view/View.js @@ -4,7 +4,9 @@ import 'jquery-ui/ui/widgets/dialog'; import _Blockly from './blockly'; import Chart from './Dialogs/Chart'; import Limits from './Dialogs/Limits'; -import Save from './Dialogs/Save'; +import IntegrationsDialog from './Dialogs/IntegrationsDialog'; +import LoadDialog from './Dialogs/LoadDialog'; +import SaveDialog from './Dialogs/SaveDialog'; import TradingView from './Dialogs/TradingView'; import logHandler from './logger'; import LogTable from './LogTable'; @@ -26,6 +28,7 @@ import { addTokenIfValid, } from '../../common/appId'; import { translate } from '../../common/i18n'; +import googleDrive from '../../common/integrations/GoogleDrive'; import { getLanguage } from '../../common/lang'; import { observer as globalObserver } from '../../common/utils/observer'; import { @@ -139,7 +142,9 @@ const clearRealityCheck = () => { }; const limits = new Limits(); -const saveDialog = new Save(); +const integrationsDialog = new IntegrationsDialog(); +const loadDialog = new LoadDialog(); +const saveDialog = new SaveDialog(); const getLandingCompanyForToken = id => { let landingCompany; @@ -346,6 +351,7 @@ export default class View { .then(() => { this.stop(); Elevio.logoutUser(); + googleDrive.signOut(); removeTokens(); }) .catch(() => {}); @@ -382,6 +388,10 @@ export default class View { classes : { 'ui-dialog-titlebar-close': 'icon-close' }, }); + $('#integrations').click(() => integrationsDialog.open()); + + $('#load-xml').click(() => loadDialog.open()); + $('#save-xml').click(() => saveDialog.save().then(arg => this.blockly.save(arg))); $('#undo').click(() => { @@ -449,10 +459,6 @@ export default class View { $('#toggleHeaderButton').click(() => this.showHeader($('#header').is(':hidden'))); - $('#loadXml').click(() => { - $('#files').click(); - }); - $('#logout, #toolbox-logout').click(() => { setBeforeUnload(true); logout(); From 43c4a7ba2c402b594c4f2d436e3c9f7c1fbbcddc Mon Sep 17 00:00:00 2001 From: Aaron Imming Date: Tue, 19 Mar 2019 11:52:00 +0800 Subject: [PATCH 10/42] Rename class method --- src/botPage/view/Dialogs/LoadDialog.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/botPage/view/Dialogs/LoadDialog.js b/src/botPage/view/Dialogs/LoadDialog.js index 956108161c..f35c4a93b3 100644 --- a/src/botPage/view/Dialogs/LoadDialog.js +++ b/src/botPage/view/Dialogs/LoadDialog.js @@ -23,7 +23,7 @@ class LoadContent extends PureComponent { submit() { switch (this.state.loadType) { case 'google-drive': - googleDrive.createFolderPicker().then(() => this.props.closeDialog()); + googleDrive.createFilePicker().then(() => this.props.closeDialog()); break; default: $('#files').click(); From 9aff00163ebd0656560d697d6627a21f2d7bbc61 Mon Sep 17 00:00:00 2001 From: Aaron Imming Date: Tue, 19 Mar 2019 13:58:18 +0800 Subject: [PATCH 11/42] Increase/decrease spacing for consistency --- static/css/_panel.scss | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/static/css/_panel.scss b/static/css/_panel.scss index bb4f27cddc..27cc5c61ca 100644 --- a/static/css/_panel.scss +++ b/static/css/_panel.scss @@ -351,7 +351,7 @@ $disabled-color: #F2F2F2; border-radius: 10px; & .description { margin-left: 21px; - margin-top: 0.5em; + margin-top: 0.2em; margin-bottom: 0; } } @@ -365,10 +365,10 @@ $disabled-color: #F2F2F2; h2 { color: $black; font-size: 20px; - margin: 0 0 0.1em 0; + margin: 0 0 0.2em 0; } .description { - margin: 0 0 0.1em 0; + margin: 0 0 0.2em 0; } .left { width: 60%; @@ -384,7 +384,7 @@ $disabled-color: #F2F2F2; } } .integration-user { - color: $brand-dark-gray; + color: #b9b9b9; font-size: 75%; } } From 7c423f01e5cae25156f2753199430cce5418243b Mon Sep 17 00:00:00 2001 From: Aaron Imming Date: Tue, 19 Mar 2019 14:02:11 +0800 Subject: [PATCH 12/42] Re-order imports --- src/botPage/view/Dialogs/IntegrationsDialog.js | 2 +- src/botPage/view/Dialogs/LoadDialog.js | 2 +- src/botPage/view/Dialogs/SaveDialog.js | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/botPage/view/Dialogs/IntegrationsDialog.js b/src/botPage/view/Dialogs/IntegrationsDialog.js index 812919f2fd..e4e9aa4b31 100644 --- a/src/botPage/view/Dialogs/IntegrationsDialog.js +++ b/src/botPage/view/Dialogs/IntegrationsDialog.js @@ -1,8 +1,8 @@ import React, { PureComponent } from 'react'; import Dialog from './Dialog'; -import { translate } from '../../../common/i18n'; import GoogleDriveIntegration from '../react-components/Integrations/GoogleDriveIntegration'; import * as style from '../style'; +import { translate } from '../../../common/i18n'; import { observer as globalObserver } from '../../../common/utils/observer'; class IntegrationsContent extends PureComponent { diff --git a/src/botPage/view/Dialogs/LoadDialog.js b/src/botPage/view/Dialogs/LoadDialog.js index f35c4a93b3..1ef4e6c6c2 100644 --- a/src/botPage/view/Dialogs/LoadDialog.js +++ b/src/botPage/view/Dialogs/LoadDialog.js @@ -1,7 +1,7 @@ import React, { PureComponent } from 'react'; import Dialog from './Dialog'; -import { translate } from '../../../common/i18n'; import * as style from '../style'; +import { translate } from '../../../common/i18n'; import googleDrive from '../../../common/integrations/GoogleDrive'; import { observer as globalObserver } from '../../../common/utils/observer'; diff --git a/src/botPage/view/Dialogs/SaveDialog.js b/src/botPage/view/Dialogs/SaveDialog.js index 090036682c..3cf3c4abd9 100644 --- a/src/botPage/view/Dialogs/SaveDialog.js +++ b/src/botPage/view/Dialogs/SaveDialog.js @@ -1,10 +1,10 @@ -import React, { PureComponent } from 'react'; import PropTypes from 'prop-types'; -import { translate } from '../../../common/i18n'; -import * as style from '../style'; +import React, { PureComponent } from 'react'; import Dialog from './Dialog'; -import googleDrive from '../../../common/integrations/GoogleDrive'; import { cleanBeforeExport } from '../blockly/utils'; +import * as style from '../style'; +import { translate } from '../../../common/i18n'; +import googleDrive from '../../../common/integrations/GoogleDrive'; import { observer as globalObserver } from '../../../common/utils/observer'; class SaveContent extends PureComponent { From 2facd4dc493b77fdc1f82e4ae2011d17ba00911e Mon Sep 17 00:00:00 2001 From: Aaron Imming Date: Wed, 20 Mar 2019 11:27:28 +0800 Subject: [PATCH 13/42] Catch rejected promise, do nothing --- src/botPage/view/Dialogs/LoadDialog.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/botPage/view/Dialogs/LoadDialog.js b/src/botPage/view/Dialogs/LoadDialog.js index 1ef4e6c6c2..1084f886ae 100644 --- a/src/botPage/view/Dialogs/LoadDialog.js +++ b/src/botPage/view/Dialogs/LoadDialog.js @@ -23,7 +23,10 @@ class LoadContent extends PureComponent { submit() { switch (this.state.loadType) { case 'google-drive': - googleDrive.createFilePicker().then(() => this.props.closeDialog()); + googleDrive + .createFilePicker() + .then(() => this.props.closeDialog()) + .catch(() => {}); break; default: $('#files').click(); From e404cb3746390f2d46fcd8b4a4f821fdcda1d1ed Mon Sep 17 00:00:00 2001 From: Aaron Imming Date: Wed, 20 Mar 2019 11:30:58 +0800 Subject: [PATCH 14/42] Update observer event name --- src/botPage/view/index.js | 2 +- .../react-components/Integrations/GoogleDriveIntegration.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/botPage/view/index.js b/src/botPage/view/index.js index 2dbf151553..f0b04e60e8 100644 --- a/src/botPage/view/index.js +++ b/src/botPage/view/index.js @@ -2,9 +2,9 @@ import 'babel-polyfill'; import 'jquery-ui/ui/widgets/dialog'; import 'notifyjs-browser'; +import View from './View'; import '../../common/binary-ui/dropdown'; import Elevio from '../../common/elevio'; -import View from './View'; $.ajaxSetup({ cache: false, diff --git a/src/botPage/view/react-components/Integrations/GoogleDriveIntegration.js b/src/botPage/view/react-components/Integrations/GoogleDriveIntegration.js index ac145e9666..a40bedd99a 100644 --- a/src/botPage/view/react-components/Integrations/GoogleDriveIntegration.js +++ b/src/botPage/view/react-components/Integrations/GoogleDriveIntegration.js @@ -10,7 +10,7 @@ export default class GoogleDriveIntegration extends PureComponent { } componentDidMount() { - globalObserver.register('integrations.googledrive', data => this.setState(data)); + globalObserver.register('googledrive.authorise', data => this.setState(data)); } render() { From a2e3f405833507a3150a8544832ff0d868218174 Mon Sep 17 00:00:00 2001 From: Aaron Imming Date: Wed, 20 Mar 2019 15:58:11 +0800 Subject: [PATCH 15/42] Create styling rules for barspinner in button --- static/css/_toolbox.scss | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/static/css/_toolbox.scss b/static/css/_toolbox.scss index ec1dd85849..79daa64c74 100644 --- a/static/css/_toolbox.scss +++ b/static/css/_toolbox.scss @@ -110,3 +110,13 @@ } } } + +button { + & > .barspinner.white { + position: relative; + margin: 3px auto; + height: 13px; + top: initial; + left: initial; + } +} From 6694d8cecb50276c0ba5b4a5b9946d5202fbd1ce Mon Sep 17 00:00:00 2001 From: Aaron Imming Date: Wed, 20 Mar 2019 16:10:50 +0800 Subject: [PATCH 16/42] Show barspinner when making requests to Google Drive --- src/botPage/view/Dialogs/LoadDialog.js | 19 ++++++++++++++++--- src/botPage/view/Dialogs/SaveDialog.js | 22 ++++++++++++++++++---- src/common/utils/tools.js | 17 +++++++++++++++++ 3 files changed, 51 insertions(+), 7 deletions(-) diff --git a/src/botPage/view/Dialogs/LoadDialog.js b/src/botPage/view/Dialogs/LoadDialog.js index 1084f886ae..a8dfa7e7d5 100644 --- a/src/botPage/view/Dialogs/LoadDialog.js +++ b/src/botPage/view/Dialogs/LoadDialog.js @@ -4,6 +4,7 @@ import * as style from '../style'; import { translate } from '../../../common/i18n'; import googleDrive from '../../../common/integrations/GoogleDrive'; import { observer as globalObserver } from '../../../common/utils/observer'; +import { showSpinnerInButton, removeSpinnerInButton } from '../../../common/utils/tools'; class LoadContent extends PureComponent { constructor() { @@ -23,10 +24,16 @@ class LoadContent extends PureComponent { submit() { switch (this.state.loadType) { case 'google-drive': + showSpinnerInButton($(this.submitButton)); googleDrive .createFilePicker() - .then(() => this.props.closeDialog()) - .catch(() => {}); + .then(() => { + this.props.closeDialog(); + removeSpinnerInButton($(this.submitButton), translate('Load')); + }) + .catch(() => { + removeSpinnerInButton($(this.submitButton), translate('Load')); + }); break; default: $('#files').click(); @@ -68,7 +75,13 @@ class LoadContent extends PureComponent {
-
diff --git a/src/botPage/view/Dialogs/SaveDialog.js b/src/botPage/view/Dialogs/SaveDialog.js index 3cf3c4abd9..bb0bda5ecf 100644 --- a/src/botPage/view/Dialogs/SaveDialog.js +++ b/src/botPage/view/Dialogs/SaveDialog.js @@ -6,6 +6,7 @@ import * as style from '../style'; import { translate } from '../../../common/i18n'; import googleDrive from '../../../common/integrations/GoogleDrive'; import { observer as globalObserver } from '../../../common/utils/observer'; +import { showSpinnerInButton, removeSpinnerInButton } from '../../../common/utils/tools'; class SaveContent extends PureComponent { constructor() { @@ -27,6 +28,8 @@ class SaveContent extends PureComponent { switch (this.state.saveType) { case 'google-drive': + showSpinnerInButton($(this.submitButton)); + const xml = Blockly.Xml.workspaceToDom(Blockly.mainWorkspace); cleanBeforeExport(xml); @@ -42,9 +45,13 @@ class SaveContent extends PureComponent { .then(() => { globalObserver.emit('ui.log.success', translate('Successfully uploaded to Google Drive')); this.props.closeDialog(); + removeSpinnerInButton($(this.submitButton), translate('Save')); }) - .catch(e => { - globalObserver.emit('ui.log.warn', e.message); + .catch(error => { + if (error !== google.picker.Action.CANCEL) { + globalObserver.emit('ui.log.warn', error.message); + } + removeSpinnerInButton($(this.submitButton), translate('Save')); }); break; @@ -132,8 +139,15 @@ class SaveContent extends PureComponent { {translate('Save your blocks and settings for re-use in other strategies')} -
- +
+
); diff --git a/src/common/utils/tools.js b/src/common/utils/tools.js index 6d61978926..64da2c3b69 100644 --- a/src/common/utils/tools.js +++ b/src/common/utils/tools.js @@ -94,3 +94,20 @@ export const getExtension = () => { const extension = host.split('.').slice(-1)[0]; return host !== extension ? extension : ''; }; + +export const showSpinnerInButton = $buttonElement => { + $buttonElement + .html(() => { + const barspinner = $('
'); + Array.from(new Array(5)).forEach((x, i) => { + const rect = $(`
`); + barspinner.append(rect); + }); + return barspinner; + }) + .prop('disabled', true); +}; + +export const removeSpinnerInButton = ($buttonElement, initialText) => { + $buttonElement.html(() => initialText).prop('disabled', false); +}; From 95a62017a394cf831887fd08436a0f4962c81c34 Mon Sep 17 00:00:00 2001 From: Aaron Imming Date: Wed, 20 Mar 2019 16:29:55 +0800 Subject: [PATCH 17/42] Refactor closing dialogs when opening other dialogs --- src/botPage/view/Dialogs/Dialog.js | 9 +++++++++ src/botPage/view/Dialogs/IntegrationsDialog.js | 11 +---------- src/botPage/view/Dialogs/LoadDialog.js | 16 +++++----------- src/botPage/view/Dialogs/SaveDialog.js | 11 +---------- 4 files changed, 16 insertions(+), 31 deletions(-) diff --git a/src/botPage/view/Dialogs/Dialog.js b/src/botPage/view/Dialogs/Dialog.js index 42d542ae07..5abebda1f5 100644 --- a/src/botPage/view/Dialogs/Dialog.js +++ b/src/botPage/view/Dialogs/Dialog.js @@ -1,6 +1,7 @@ import React from 'react'; import ReactDOM from 'react-dom'; import DialogComponent from './DialogComponent'; +import { observer as globalObserver } from '../../../common/utils/observer'; export default class Dialog { constructor(id, title, content, options = {}) { @@ -13,8 +14,16 @@ export default class Dialog { } open() { $(`#${this.componentId}`).dialog('open'); + globalObserver.emit('dialog.opened', this.componentId); } close() { $(`#${this.componentId}`).dialog('close'); } + registerCloseOnOtherDialog() { + globalObserver.register('dialog.opened', dialogId => { + if (dialogId !== this.componentId) { + this.close(); + } + }); + } } diff --git a/src/botPage/view/Dialogs/IntegrationsDialog.js b/src/botPage/view/Dialogs/IntegrationsDialog.js index e4e9aa4b31..5842f24c6b 100644 --- a/src/botPage/view/Dialogs/IntegrationsDialog.js +++ b/src/botPage/view/Dialogs/IntegrationsDialog.js @@ -10,11 +10,6 @@ class IntegrationsContent extends PureComponent { super(); } - componentDidMount() { - globalObserver.register('dialog.load.opened', this.props.closeDialog); - globalObserver.register('dialog.save.opened', this.props.closeDialog); - } - render() { return (
@@ -33,10 +28,6 @@ export default class IntegrationsDialog extends Dialog { width : 500, height: 'auto', }); - } - - open() { - super.open(); - globalObserver.emit('dialog.integrations.opened'); + this.registerCloseOnOtherDialog(); } } diff --git a/src/botPage/view/Dialogs/LoadDialog.js b/src/botPage/view/Dialogs/LoadDialog.js index a8dfa7e7d5..7c4a5e2046 100644 --- a/src/botPage/view/Dialogs/LoadDialog.js +++ b/src/botPage/view/Dialogs/LoadDialog.js @@ -9,12 +9,10 @@ import { showSpinnerInButton, removeSpinnerInButton } from '../../../common/util class LoadContent extends PureComponent { constructor() { super(); - this.state = { loadType: 'local' }; - } - - componentDidMount() { - globalObserver.register('dialog.save.opened', this.props.closeDialog); - globalObserver.register('dialog.integrations.opened', this.props.closeDialog); + this.state = { + loadType : 'local', + closeOtherDialogs: true, + }; } onChange(event) { @@ -97,10 +95,6 @@ export default class LoadDialog extends Dialog { this.close(); }; super('load-dialog', translate('Load blocks'), , style.dialogLayout); - } - - open() { - super.open(); - globalObserver.emit('dialog.load.opened'); + this.registerCloseOnOtherDialog(); } } diff --git a/src/botPage/view/Dialogs/SaveDialog.js b/src/botPage/view/Dialogs/SaveDialog.js index bb0bda5ecf..eee0c4328a 100644 --- a/src/botPage/view/Dialogs/SaveDialog.js +++ b/src/botPage/view/Dialogs/SaveDialog.js @@ -17,11 +17,6 @@ class SaveContent extends PureComponent { }; } - componentDidMount() { - globalObserver.register('dialog.load.opened', this.props.closeDialog); - globalObserver.register('dialog.integrations.opened', this.props.closeDialog); - } - submit() { const filename = $(this.filename).val() || 'binary-bot'; const collection = $(this.isCollection).prop('checked'); @@ -174,6 +169,7 @@ export default class SaveDialog extends Dialog { , style.dialogLayout ); + this.registerCloseOnOtherDialog(); } save() { @@ -182,9 +178,4 @@ export default class SaveDialog extends Dialog { this.limitsPromise = resolve; }); } - - open() { - super.open(); - globalObserver.emit('dialog.save.opened'); - } } From cb62b804a33a6f4889555ee352a1b661e67e8502 Mon Sep 17 00:00:00 2001 From: Aaron Imming Date: Wed, 20 Mar 2019 17:50:19 +0800 Subject: [PATCH 18/42] Linting cleanup + Fixes --- .../view/Dialogs/IntegrationsDialog.js | 21 ++----- src/botPage/view/Dialogs/LoadDialog.js | 38 ++++++------ src/botPage/view/Dialogs/SaveDialog.js | 62 +++++++++---------- .../Integrations/GoogleDriveIntegration.js | 1 + 4 files changed, 55 insertions(+), 67 deletions(-) diff --git a/src/botPage/view/Dialogs/IntegrationsDialog.js b/src/botPage/view/Dialogs/IntegrationsDialog.js index 5842f24c6b..809a75af9c 100644 --- a/src/botPage/view/Dialogs/IntegrationsDialog.js +++ b/src/botPage/view/Dialogs/IntegrationsDialog.js @@ -1,23 +1,14 @@ -import React, { PureComponent } from 'react'; +import React from 'react'; import Dialog from './Dialog'; import GoogleDriveIntegration from '../react-components/Integrations/GoogleDriveIntegration'; import * as style from '../style'; import { translate } from '../../../common/i18n'; -import { observer as globalObserver } from '../../../common/utils/observer'; -class IntegrationsContent extends PureComponent { - constructor() { - super(); - } - - render() { - return ( -
- -
- ); - } -} +const IntegrationsContent = () => ( +
+ +
+); export default class IntegrationsDialog extends Dialog { constructor() { diff --git a/src/botPage/view/Dialogs/LoadDialog.js b/src/botPage/view/Dialogs/LoadDialog.js index 7c4a5e2046..d95827fd42 100644 --- a/src/botPage/view/Dialogs/LoadDialog.js +++ b/src/botPage/view/Dialogs/LoadDialog.js @@ -1,9 +1,9 @@ +import PropTypes from 'prop-types'; import React, { PureComponent } from 'react'; import Dialog from './Dialog'; import * as style from '../style'; import { translate } from '../../../common/i18n'; import googleDrive from '../../../common/integrations/GoogleDrive'; -import { observer as globalObserver } from '../../../common/utils/observer'; import { showSpinnerInButton, removeSpinnerInButton } from '../../../common/utils/tools'; class LoadContent extends PureComponent { @@ -20,23 +20,21 @@ class LoadContent extends PureComponent { } submit() { - switch (this.state.loadType) { - case 'google-drive': - showSpinnerInButton($(this.submitButton)); - googleDrive - .createFilePicker() - .then(() => { - this.props.closeDialog(); - removeSpinnerInButton($(this.submitButton), translate('Load')); - }) - .catch(() => { - removeSpinnerInButton($(this.submitButton), translate('Load')); - }); - break; - default: - $('#files').click(); - this.props.closeDialog(); - break; + if (this.state.loadType === 'google-drive') { + const initialButtonText = $(this.submitButton).text(); + showSpinnerInButton($(this.submitButton)); + googleDrive + .createFilePicker() + .then(() => { + this.props.closeDialog(); + removeSpinnerInButton($(this.submitButton), initialButtonText); + }) + .catch(() => { + removeSpinnerInButton($(this.submitButton), initialButtonText); + }); + } else { + $('#files').click(); + this.props.closeDialog(); } } @@ -59,7 +57,7 @@ class LoadContent extends PureComponent { defaultChecked={true} onChange={e => this.onChange(e)} /> - + - {translate('Load')} + {translate('Save')}
diff --git a/src/botPage/view/Dialogs/SaveDialog.js b/src/botPage/view/Dialogs/SaveDialog.js index eee0c4328a..372fb9b7b8 100644 --- a/src/botPage/view/Dialogs/SaveDialog.js +++ b/src/botPage/view/Dialogs/SaveDialog.js @@ -1,3 +1,4 @@ +/* global google */ import PropTypes from 'prop-types'; import React, { PureComponent } from 'react'; import Dialog from './Dialog'; @@ -21,41 +22,38 @@ class SaveContent extends PureComponent { const filename = $(this.filename).val() || 'binary-bot'; const collection = $(this.isCollection).prop('checked'); - switch (this.state.saveType) { - case 'google-drive': - showSpinnerInButton($(this.submitButton)); + if (this.state.saveType === 'google-drive') { + const initialButtonText = $(this.submitButton).text(); + showSpinnerInButton($(this.submitButton)); - const xml = Blockly.Xml.workspaceToDom(Blockly.mainWorkspace); - cleanBeforeExport(xml); + const xml = Blockly.Xml.workspaceToDom(Blockly.mainWorkspace); + cleanBeforeExport(xml); - xml.setAttribute('xmlns', 'http://www.w3.org/1999/xhtml'); - xml.setAttribute('collection', collection); + xml.setAttribute('xmlns', 'http://www.w3.org/1999/xhtml'); + xml.setAttribute('collection', collection); - googleDrive - .saveFile({ - name : filename, - content : Blockly.Xml.domToPrettyText(xml), - mimeType: 'application/xml', - }) - .then(() => { - globalObserver.emit('ui.log.success', translate('Successfully uploaded to Google Drive')); - this.props.closeDialog(); - removeSpinnerInButton($(this.submitButton), translate('Save')); - }) - .catch(error => { - if (error !== google.picker.Action.CANCEL) { - globalObserver.emit('ui.log.warn', error.message); - } - removeSpinnerInButton($(this.submitButton), translate('Save')); - }); - break; - - default: - this.props.onSave({ - filename, - collection, + googleDrive + .saveFile({ + name : filename, + content : Blockly.Xml.domToPrettyText(xml), + mimeType: 'application/xml', + }) + .then(() => { + globalObserver.emit('ui.log.success', translate('Successfully uploaded to Google Drive')); + this.props.closeDialog(); + removeSpinnerInButton($(this.submitButton), initialButtonText); + }) + .catch(error => { + if (error !== google.picker.Action.CANCEL) { + globalObserver.emit('ui.log.warn', error.message); + } + removeSpinnerInButton($(this.submitButton), initialButtonText); }); - break; + } else { + this.props.onSave({ + filename, + collection, + }); } } @@ -96,7 +94,7 @@ class SaveContent extends PureComponent { defaultChecked={true} onChange={e => this.onChange(e)} /> - + this.setState(data)); } + // eslint-disable-next-line class-methods-use-this render() { return (
From acead1098d31de247657b35a764da6fe6a2c06a3 Mon Sep 17 00:00:00 2001 From: Aaron Imming Date: Wed, 20 Mar 2019 17:52:01 +0800 Subject: [PATCH 19/42] Remove unused closeOtherDialogs prop --- src/botPage/view/Dialogs/LoadDialog.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/botPage/view/Dialogs/LoadDialog.js b/src/botPage/view/Dialogs/LoadDialog.js index d95827fd42..dfda8f6f13 100644 --- a/src/botPage/view/Dialogs/LoadDialog.js +++ b/src/botPage/view/Dialogs/LoadDialog.js @@ -9,10 +9,7 @@ import { showSpinnerInButton, removeSpinnerInButton } from '../../../common/util class LoadContent extends PureComponent { constructor() { super(); - this.state = { - loadType : 'local', - closeOtherDialogs: true, - }; + this.state = { loadType: 'local' }; } onChange(event) { From f43c54975e75e065e2cd38d223617bc54925601d Mon Sep 17 00:00:00 2001 From: Aaron Imming Date: Wed, 20 Mar 2019 18:31:49 +0800 Subject: [PATCH 20/42] Create common trackAndEmitError function, let rejected promises bubble up --- src/botPage/view/Dialogs/LoadDialog.js | 2 +- src/botPage/view/Dialogs/SaveDialog.js | 6 +----- src/common/utils/tools.js | 8 ++++++++ 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/botPage/view/Dialogs/LoadDialog.js b/src/botPage/view/Dialogs/LoadDialog.js index dfda8f6f13..13a95c3859 100644 --- a/src/botPage/view/Dialogs/LoadDialog.js +++ b/src/botPage/view/Dialogs/LoadDialog.js @@ -75,7 +75,7 @@ class LoadContent extends PureComponent { this.submitButton = el; }} > - {translate('Save')} + {translate('Load')}
diff --git a/src/botPage/view/Dialogs/SaveDialog.js b/src/botPage/view/Dialogs/SaveDialog.js index 372fb9b7b8..f53480b552 100644 --- a/src/botPage/view/Dialogs/SaveDialog.js +++ b/src/botPage/view/Dialogs/SaveDialog.js @@ -1,4 +1,3 @@ -/* global google */ import PropTypes from 'prop-types'; import React, { PureComponent } from 'react'; import Dialog from './Dialog'; @@ -43,10 +42,7 @@ class SaveContent extends PureComponent { this.props.closeDialog(); removeSpinnerInButton($(this.submitButton), initialButtonText); }) - .catch(error => { - if (error !== google.picker.Action.CANCEL) { - globalObserver.emit('ui.log.warn', error.message); - } + .catch(() => { removeSpinnerInButton($(this.submitButton), initialButtonText); }); } else { diff --git a/src/common/utils/tools.js b/src/common/utils/tools.js index 64da2c3b69..45a9ba741f 100644 --- a/src/common/utils/tools.js +++ b/src/common/utils/tools.js @@ -1,4 +1,5 @@ import RenderHTML from 'react-render-html'; +import { observer as globalObserver } from './observer'; import { translate as i18nTranslate } from '../../common/i18n'; import { getLanguage } from '../../common/lang'; import AppIdMap from '../../common/appIdResolver'; @@ -111,3 +112,10 @@ export const showSpinnerInButton = $buttonElement => { export const removeSpinnerInButton = ($buttonElement, initialText) => { $buttonElement.html(() => initialText).prop('disabled', false); }; + +export const trackAndEmitError = (message, object = {}) => { + globalObserver.emit('ui.log.warn', message); + if (window.trackJs) { + trackJs.console.error(message, object); + } +}; From 4130c91d8481e01efc3dd9bf51777570bc47f064 Mon Sep 17 00:00:00 2001 From: Aaron Imming Date: Thu, 21 Mar 2019 09:59:25 +0800 Subject: [PATCH 21/42] Initially hide Google Drive features --- src/botPage/view/Dialogs/LoadDialog.js | 2 +- src/botPage/view/Dialogs/SaveDialog.js | 2 +- templates/bot.mustache | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/botPage/view/Dialogs/LoadDialog.js b/src/botPage/view/Dialogs/LoadDialog.js index 13a95c3859..145366798f 100644 --- a/src/botPage/view/Dialogs/LoadDialog.js +++ b/src/botPage/view/Dialogs/LoadDialog.js @@ -56,7 +56,7 @@ class LoadContent extends PureComponent { />
- + - + - + From 1f001323b2e3dcf1421e5ffa0b86d235b7418dc5 Mon Sep 17 00:00:00 2001 From: Aaron Imming Date: Thu, 21 Mar 2019 10:26:28 +0800 Subject: [PATCH 22/42] Create Google Drive integration logic --- src/common/integrations/GoogleDrive.js | 267 +++++++++++++++++++++++++ 1 file changed, 267 insertions(+) create mode 100644 src/common/integrations/GoogleDrive.js diff --git a/src/common/integrations/GoogleDrive.js b/src/common/integrations/GoogleDrive.js new file mode 100644 index 0000000000..0c3239ed3d --- /dev/null +++ b/src/common/integrations/GoogleDrive.js @@ -0,0 +1,267 @@ +/* global google,gapi */ +import { getLanguage } from '../lang'; +import { observer as globalObserver } from '../utils/observer'; +import { translate, trackAndEmitError } from '../utils/tools'; +import { loadWorkspace, loadBlocks } from '../../botPage/view/blockly'; + +class GoogleDrive { + constructor() { + this.botFolderName = `Binary Bot - ${translate('Strategies')}`; + this.appId = ''; + this.apiKey = ''; + this.clientId = ''; + + this.googleAuth = null; + this.isAuthorised = null; + this.profile = null; + + $.getScript('https://apis.google.com/js/api.js', () => this.init()); + } + + init() { + gapi.load('client:auth2:picker', { + callback: () => { + gapi.client + .init({ + apiKey : this.apiKey, + clientId : this.clientId, + scope : 'https://www.googleapis.com/auth/drive.file', + discoveryDocs: ['https://www.googleapis.com/discovery/v1/apis/drive/v3/rest'], + }) + .then( + () => { + this.googleAuth = gapi.auth2.getAuthInstance(); + this.googleAuth.isSignedIn.listen(isSignedIn => this.updateSigninStatus(isSignedIn)); + this.updateSigninStatus(this.googleAuth.isSignedIn.get()); + + $('#integrations').removeClass('invisible'); + $('#save-google-drive') + .parent() + .removeClass('invisible'); + $('#load-google-drive') + .parent() + .removeClass('invisible'); + }, + error => { + trackAndEmitError(translate('There was an error initialising Google Drive'), error); + } + ); + }, + onerror: error => { + trackAndEmitError(translate('There was an error loading Google Drive libraries'), error); + }, + }); + } + + updateSigninStatus(isSignedIn) { + if (isSignedIn) { + this.profile = this.googleAuth.currentUser.get().getBasicProfile(); + } else { + this.profile = null; + } + this.isAuthorised = isSignedIn; + globalObserver.emit('googledrive.authorise', { isAuthorised: isSignedIn }); + } + + authorise() { + if (this.isAuthorised) { + return Promise.resolve(); + } + return this.googleAuth.signIn({ prompt: 'select_account' }); + } + + signOut() { + if (this.isAuthorised) { + return this.googleAuth.signOut(); + } + return Promise.resolve(); + } + + // eslint-disable-next-line class-methods-use-this + getPickerLanguage() { + const language = getLanguage(); + + if (language === 'zhTw') { + return 'zh-TW'; + } else if (language === 'zhCn') { + return 'zh-CN'; + } + return language; + } + + createFilePicker() { + return new Promise((resolve, reject) => { + // eslint-disable-next-line consistent-return + const userPickedFile = data => { + if (data.action === google.picker.Action.CANCEL) { + return reject(google.picker.Action.CANCEL); + } else if (data.action !== google.picker.Action.PICKED) { + return; // eslint-disable-line consistent-return + } + + const fileId = data.docs[0].id; + gapi.client.drive.files + .get({ + alt : 'media', + fileId, + mimeType: 'text/plain', + }) + .then(response => { + try { + const xmlDom = Blockly.Xml.textToDom(response.body); + const loadFunction = + xmlDom.hasAttribute('collection') && xmlDom.getAttribute('collection') === 'true' + ? loadBlocks + : loadWorkspace; + try { + loadFunction(xmlDom); + resolve(); + } catch (error) { + trackAndEmitError(translate('Could not load Google Drive blocks'), error); + reject(error); + } + } catch (error) { + trackAndEmitError(translate('Unrecognized file format'), error); + reject(error); + } + }) + .catch(error => { + trackAndEmitError(translate('There was an error retrieving data from Google Drive'), error); + reject(error); + }); + }; + + this.authorise().then(() => { + const mimeTypes = ['application/xml']; + const docsView = new google.picker.DocsView(); + docsView.setMimeTypes(mimeTypes.join(',')); + docsView.setIncludeFolders(true); + docsView.setOwnedByMe(true); + + const picker = new google.picker.PickerBuilder(); + picker + .setTitle(translate('Select a Binary Bot strategy')) + .setLocale(this.getPickerLanguage()) + .setAppId(this.appId) + .setOAuthToken(gapi.auth.getToken().access_token) + .addView(docsView) + .setDeveloperKey(this.apiKey) + .setCallback(userPickedFile) + .build() + .setVisible(true); + }); + }); + } + + getDefaultFolderId() { + return new Promise((resolve, reject) => { + // Avoid duplicate auth flow by checking if user is already authed + const authorisePromise = []; + if (!this.isAuthorised) { + authorisePromise.push(this.authorise); + } + Promise.all(authorisePromise).then(() => { + gapi.client.drive.files + .list({ q: 'trashed=false' }) + // eslint-disable-next-line consistent-return + .then(response => { + const botFolder = response.result.files.find( + file => + file.name === this.botFolderName && + file.mimeType === 'application/vnd.google-apps.folder' + ); + if (botFolder) { + return resolve(botFolder.id); + } + gapi.client.drive.files + .create({ + resource: { + name : this.botFolderName, + mimeType: 'application/vnd.google-apps.folder', + fields : 'id', + }, + }) + .then(createFileResponse => resolve(createFileResponse.result.id)) + .catch(error => { + trackAndEmitError( + translate('There was an error retrieving files from Google Drive'), + error + ); + reject(error); + }); + }) + .catch(error => { + trackAndEmitError(translate('There was an error listing files from Google Drive'), error); + reject(error); + }); + }); + }); + } + + saveFile(options) { + return new Promise((resolve, reject) => { + // eslint-disable-next-line consistent-return + const savePickerCallback = data => { + if (data.action === google.picker.Action.CANCEL) { + return reject(google.picker.Action.CANCEL); + } else if (data.action !== google.picker.Action.PICKED) { + return; // eslint-disable-line consistent-return + } + + const folderId = data.docs[0].id; + const strategyFile = new Blob([options.content], { type: options.mimeType }); + const strategyFileMetadata = JSON.stringify({ + name : options.name, + mimeType: options.mimeType, + parents : [folderId], + }); + + const formData = new FormData(); + formData.append('metadata', new Blob([strategyFileMetadata], { type: 'application/json' })); + formData.append('file', strategyFile); + + const xhr = new XMLHttpRequest(); + xhr.responseType = 'json'; + xhr.open('POST', 'https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart'); + xhr.setRequestHeader('Authorization', `Bearer ${gapi.auth.getToken().access_token}`); + xhr.onload = () => { + if (xhr.status === 200) { + resolve(xhr.response); + } else { + trackAndEmitError(translate('There was an error processing your request'), xhr.status); + reject(); + } + }; + xhr.send(formData); + }; + + this.authorise().then(() => { + // Calling getDefaultFolderId() ensures there's at least one folder available to save to. + // FilePicker doesn't allow for folder creation, so a user without any folder in + // their drive couldn't select anything. + this.getDefaultFolderId().then(() => { + const view = new google.picker.DocsView(); + view.setIncludeFolders(true) + .setSelectFolderEnabled(true) + .setMimeTypes('application/vnd.google-apps.folder'); + + const picker = new google.picker.PickerBuilder(); + picker + .setTitle(translate('Select a folder')) + .addView(view) + .setLocale(this.getPickerLanguage()) + .setAppId(this.appId) + .setOAuthToken(gapi.auth.getToken().access_token) + .setDeveloperKey(this.apiKey) + .setCallback(savePickerCallback) + .build() + .setVisible(true); + }); + }); + }); + } +} + +const googleDrive = new GoogleDrive(); + +export default googleDrive; From ed3147a95fc3e90aa18628f73537de03ec718b2d Mon Sep 17 00:00:00 2001 From: Aaron Imming Date: Thu, 21 Mar 2019 11:17:33 +0800 Subject: [PATCH 23/42] Change trackJs.console.error to trackJs.track for proper logging --- src/common/utils/tools.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/utils/tools.js b/src/common/utils/tools.js index 45a9ba741f..ea45df8bdc 100644 --- a/src/common/utils/tools.js +++ b/src/common/utils/tools.js @@ -116,6 +116,6 @@ export const removeSpinnerInButton = ($buttonElement, initialText) => { export const trackAndEmitError = (message, object = {}) => { globalObserver.emit('ui.log.warn', message); if (window.trackJs) { - trackJs.console.error(message, object); + trackJs.track(`${message} - Error: ${JSON.stringify(object)}`); } }; From 1112771855bc4e698823496a2fd3e945111ef7f6 Mon Sep 17 00:00:00 2001 From: Aaron Imming Date: Thu, 21 Mar 2019 11:20:18 +0800 Subject: [PATCH 24/42] Change trackJs.console.error to trackJs.track for proper logging --- src/common/utils/tools.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/utils/tools.js b/src/common/utils/tools.js index 45a9ba741f..ea45df8bdc 100644 --- a/src/common/utils/tools.js +++ b/src/common/utils/tools.js @@ -116,6 +116,6 @@ export const removeSpinnerInButton = ($buttonElement, initialText) => { export const trackAndEmitError = (message, object = {}) => { globalObserver.emit('ui.log.warn', message); if (window.trackJs) { - trackJs.console.error(message, object); + trackJs.track(`${message} - Error: ${JSON.stringify(object)}`); } }; From d2217717b829a65ad9ba1cb11ddf472b1a38eec0 Mon Sep 17 00:00:00 2001 From: Aaron Imming Date: Thu, 21 Mar 2019 11:27:20 +0800 Subject: [PATCH 25/42] Assume user cancelled action when nothing was picked in FilePicker --- src/common/integrations/GoogleDrive.js | 263 +++++++++++++++++++++++++ 1 file changed, 263 insertions(+) create mode 100644 src/common/integrations/GoogleDrive.js diff --git a/src/common/integrations/GoogleDrive.js b/src/common/integrations/GoogleDrive.js new file mode 100644 index 0000000000..dff4300d01 --- /dev/null +++ b/src/common/integrations/GoogleDrive.js @@ -0,0 +1,263 @@ +/* global google,gapi */ +import { getLanguage } from '../lang'; +import { observer as globalObserver } from '../utils/observer'; +import { translate, trackAndEmitError } from '../utils/tools'; +import { loadWorkspace, loadBlocks } from '../../botPage/view/blockly'; + +class GoogleDrive { + constructor() { + this.botFolderName = `Binary Bot - ${translate('Strategies')}`; + this.appId = ''; + this.apiKey = ''; + this.clientId = ''; + + this.googleAuth = null; + this.isAuthorised = null; + this.profile = null; + + $.getScript('https://apis.google.com/js/api.js', () => this.init()); + } + + init() { + gapi.load('client:auth2:picker', { + callback: () => { + gapi.client + .init({ + apiKey : this.apiKey, + clientId : this.clientId, + scope : 'https://www.googleapis.com/auth/drive.file', + discoveryDocs: ['https://www.googleapis.com/discovery/v1/apis/drive/v3/rest'], + }) + .then( + () => { + this.googleAuth = gapi.auth2.getAuthInstance(); + this.googleAuth.isSignedIn.listen(isSignedIn => this.updateSigninStatus(isSignedIn)); + this.updateSigninStatus(this.googleAuth.isSignedIn.get()); + + $('#integrations').removeClass('invisible'); + $('#save-google-drive') + .parent() + .removeClass('invisible'); + $('#load-google-drive') + .parent() + .removeClass('invisible'); + }, + error => { + trackAndEmitError(translate('There was an error initialising Google Drive'), error); + } + ); + }, + onerror: error => { + trackAndEmitError(translate('There was an error loading Google Drive libraries'), error); + }, + }); + } + + updateSigninStatus(isSignedIn) { + if (isSignedIn) { + this.profile = this.googleAuth.currentUser.get().getBasicProfile(); + } else { + this.profile = null; + } + this.isAuthorised = isSignedIn; + globalObserver.emit('googledrive.authorise', { isAuthorised: isSignedIn }); + } + + authorise() { + if (this.isAuthorised) { + return Promise.resolve(); + } + return this.googleAuth.signIn({ prompt: 'select_account' }); + } + + signOut() { + if (this.isAuthorised) { + return this.googleAuth.signOut(); + } + return Promise.resolve(); + } + + // eslint-disable-next-line class-methods-use-this + getPickerLanguage() { + const language = getLanguage(); + + if (language === 'zhTw') { + return 'zh-TW'; + } else if (language === 'zhCn') { + return 'zh-CN'; + } + return language; + } + + createFilePicker() { + return new Promise((resolve, reject) => { + // eslint-disable-next-line consistent-return + const userPickedFile = data => { + if (data.action !== google.picker.Action.PICKED) { + return reject(google.picker.Action.CANCEL); + } + + const fileId = data.docs[0].id; + gapi.client.drive.files + .get({ + alt : 'media', + fileId, + mimeType: 'text/plain', + }) + .then(response => { + try { + const xmlDom = Blockly.Xml.textToDom(response.body); + const loadFunction = + xmlDom.hasAttribute('collection') && xmlDom.getAttribute('collection') === 'true' + ? loadBlocks + : loadWorkspace; + try { + loadFunction(xmlDom); + resolve(); + } catch (error) { + trackAndEmitError(translate('Could not load Google Drive blocks'), error); + reject(error); + } + } catch (error) { + trackAndEmitError(translate('Unrecognized file format'), error); + reject(error); + } + }) + .catch(error => { + trackAndEmitError(translate('There was an error retrieving data from Google Drive'), error); + reject(error); + }); + }; + + this.authorise().then(() => { + const mimeTypes = ['application/xml']; + const docsView = new google.picker.DocsView(); + docsView.setMimeTypes(mimeTypes.join(',')); + docsView.setIncludeFolders(true); + docsView.setOwnedByMe(true); + + const picker = new google.picker.PickerBuilder(); + picker + .setTitle(translate('Select a Binary Bot strategy')) + .setLocale(this.getPickerLanguage()) + .setAppId(this.appId) + .setOAuthToken(gapi.auth.getToken().access_token) + .addView(docsView) + .setDeveloperKey(this.apiKey) + .setCallback(userPickedFile) + .build() + .setVisible(true); + }); + }); + } + + getDefaultFolderId() { + return new Promise((resolve, reject) => { + // Avoid duplicate auth flow by checking if user is already authed + const authorisePromise = []; + if (!this.isAuthorised) { + authorisePromise.push(this.authorise); + } + Promise.all(authorisePromise).then(() => { + gapi.client.drive.files + .list({ q: 'trashed=false' }) + // eslint-disable-next-line consistent-return + .then(response => { + const botFolder = response.result.files.find( + file => + file.name === this.botFolderName && + file.mimeType === 'application/vnd.google-apps.folder' + ); + if (botFolder) { + return resolve(botFolder.id); + } + gapi.client.drive.files + .create({ + resource: { + name : this.botFolderName, + mimeType: 'application/vnd.google-apps.folder', + fields : 'id', + }, + }) + .then(createFileResponse => resolve(createFileResponse.result.id)) + .catch(error => { + trackAndEmitError( + translate('There was an error retrieving files from Google Drive'), + error + ); + reject(error); + }); + }) + .catch(error => { + trackAndEmitError(translate('There was an error listing files from Google Drive'), error); + reject(error); + }); + }); + }); + } + + saveFile(options) { + return new Promise((resolve, reject) => { + // eslint-disable-next-line consistent-return + const savePickerCallback = data => { + if (data.action !== google.picker.Action.PICKED) { + return reject(google.picker.Action.CANCEL); + } + + const folderId = data.docs[0].id; + const strategyFile = new Blob([options.content], { type: options.mimeType }); + const strategyFileMetadata = JSON.stringify({ + name : options.name, + mimeType: options.mimeType, + parents : [folderId], + }); + + const formData = new FormData(); + formData.append('metadata', new Blob([strategyFileMetadata], { type: 'application/json' })); + formData.append('file', strategyFile); + + const xhr = new XMLHttpRequest(); + xhr.responseType = 'json'; + xhr.open('POST', 'https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart'); + xhr.setRequestHeader('Authorization', `Bearer ${gapi.auth.getToken().access_token}`); + xhr.onload = () => { + if (xhr.status === 200) { + resolve(xhr.response); + } else { + trackAndEmitError(translate('There was an error processing your request'), xhr.status); + reject(); + } + }; + xhr.send(formData); + }; + + this.authorise().then(() => { + // Calling getDefaultFolderId() ensures there's at least one folder available to save to. + // FilePicker doesn't allow for folder creation, so a user without any folder in + // their drive couldn't select anything. + this.getDefaultFolderId().then(() => { + const view = new google.picker.DocsView(); + view.setIncludeFolders(true) + .setSelectFolderEnabled(true) + .setMimeTypes('application/vnd.google-apps.folder'); + + const picker = new google.picker.PickerBuilder(); + picker + .setTitle(translate('Select a folder')) + .addView(view) + .setLocale(this.getPickerLanguage()) + .setAppId(this.appId) + .setOAuthToken(gapi.auth.getToken().access_token) + .setDeveloperKey(this.apiKey) + .setCallback(savePickerCallback) + .build() + .setVisible(true); + }); + }); + }); + } +} + +const googleDrive = new GoogleDrive(); + +export default googleDrive; From d51ef853b88fa74ccd50af84b2b87181f34718b0 Mon Sep 17 00:00:00 2001 From: Aaron Imming Date: Thu, 21 Mar 2019 11:47:28 +0800 Subject: [PATCH 26/42] Reject if authorisation failed (restores button) --- src/common/integrations/GoogleDrive.js | 84 ++++++++++++++------------ 1 file changed, 44 insertions(+), 40 deletions(-) diff --git a/src/common/integrations/GoogleDrive.js b/src/common/integrations/GoogleDrive.js index dff4300d01..e4123de690 100644 --- a/src/common/integrations/GoogleDrive.js +++ b/src/common/integrations/GoogleDrive.js @@ -129,25 +129,27 @@ class GoogleDrive { }); }; - this.authorise().then(() => { - const mimeTypes = ['application/xml']; - const docsView = new google.picker.DocsView(); - docsView.setMimeTypes(mimeTypes.join(',')); - docsView.setIncludeFolders(true); - docsView.setOwnedByMe(true); + this.authorise() + .then(() => { + const mimeTypes = ['application/xml']; + const docsView = new google.picker.DocsView(); + docsView.setMimeTypes(mimeTypes.join(',')); + docsView.setIncludeFolders(true); + docsView.setOwnedByMe(true); - const picker = new google.picker.PickerBuilder(); - picker - .setTitle(translate('Select a Binary Bot strategy')) - .setLocale(this.getPickerLanguage()) - .setAppId(this.appId) - .setOAuthToken(gapi.auth.getToken().access_token) - .addView(docsView) - .setDeveloperKey(this.apiKey) - .setCallback(userPickedFile) - .build() - .setVisible(true); - }); + const picker = new google.picker.PickerBuilder(); + picker + .setTitle(translate('Select a Binary Bot strategy')) + .setLocale(this.getPickerLanguage()) + .setAppId(this.appId) + .setOAuthToken(gapi.auth.getToken().access_token) + .addView(docsView) + .setDeveloperKey(this.apiKey) + .setCallback(userPickedFile) + .build() + .setVisible(true); + }) + .catch(() => reject()); }); } @@ -231,29 +233,31 @@ class GoogleDrive { xhr.send(formData); }; - this.authorise().then(() => { - // Calling getDefaultFolderId() ensures there's at least one folder available to save to. - // FilePicker doesn't allow for folder creation, so a user without any folder in - // their drive couldn't select anything. - this.getDefaultFolderId().then(() => { - const view = new google.picker.DocsView(); - view.setIncludeFolders(true) - .setSelectFolderEnabled(true) - .setMimeTypes('application/vnd.google-apps.folder'); + this.authorise() + .then(() => { + // Calling getDefaultFolderId() ensures there's at least one folder available to save to. + // FilePicker doesn't allow for folder creation, so a user without any folder in + // their drive couldn't select anything. + this.getDefaultFolderId().then(() => { + const view = new google.picker.DocsView(); + view.setIncludeFolders(true) + .setSelectFolderEnabled(true) + .setMimeTypes('application/vnd.google-apps.folder'); - const picker = new google.picker.PickerBuilder(); - picker - .setTitle(translate('Select a folder')) - .addView(view) - .setLocale(this.getPickerLanguage()) - .setAppId(this.appId) - .setOAuthToken(gapi.auth.getToken().access_token) - .setDeveloperKey(this.apiKey) - .setCallback(savePickerCallback) - .build() - .setVisible(true); - }); - }); + const picker = new google.picker.PickerBuilder(); + picker + .setTitle(translate('Select a folder')) + .addView(view) + .setLocale(this.getPickerLanguage()) + .setAppId(this.appId) + .setOAuthToken(gapi.auth.getToken().access_token) + .setDeveloperKey(this.apiKey) + .setCallback(savePickerCallback) + .build() + .setVisible(true); + }); + }) + .catch(() => reject()); }); } } From 577b1c36f945277d623cdb7e7ffb69ecd59bb7dd Mon Sep 17 00:00:00 2001 From: Aaron Imming Date: Thu, 21 Mar 2019 14:19:06 +0800 Subject: [PATCH 27/42] Make sure button is restored on cancel action --- src/common/integrations/GoogleDrive.js | 118 +++++++++++++------------ 1 file changed, 60 insertions(+), 58 deletions(-) diff --git a/src/common/integrations/GoogleDrive.js b/src/common/integrations/GoogleDrive.js index e4123de690..a8e6257d6d 100644 --- a/src/common/integrations/GoogleDrive.js +++ b/src/common/integrations/GoogleDrive.js @@ -93,40 +93,40 @@ class GoogleDrive { return new Promise((resolve, reject) => { // eslint-disable-next-line consistent-return const userPickedFile = data => { - if (data.action !== google.picker.Action.PICKED) { - return reject(google.picker.Action.CANCEL); - } - - const fileId = data.docs[0].id; - gapi.client.drive.files - .get({ - alt : 'media', - fileId, - mimeType: 'text/plain', - }) - .then(response => { - try { - const xmlDom = Blockly.Xml.textToDom(response.body); - const loadFunction = - xmlDom.hasAttribute('collection') && xmlDom.getAttribute('collection') === 'true' - ? loadBlocks - : loadWorkspace; + if (data.action === google.picker.Action.PICKED) { + const fileId = data.docs[0].id; + gapi.client.drive.files + .get({ + alt : 'media', + fileId, + mimeType: 'text/plain', + }) + .then(response => { try { - loadFunction(xmlDom); - resolve(); + const xmlDom = Blockly.Xml.textToDom(response.body); + const loadFunction = + xmlDom.hasAttribute('collection') && xmlDom.getAttribute('collection') === 'true' + ? loadBlocks + : loadWorkspace; + try { + loadFunction(xmlDom); + resolve(); + } catch (error) { + trackAndEmitError(translate('Could not load Google Drive blocks'), error); + reject(error); + } } catch (error) { - trackAndEmitError(translate('Could not load Google Drive blocks'), error); + trackAndEmitError(translate('Unrecognized file format'), error); reject(error); } - } catch (error) { - trackAndEmitError(translate('Unrecognized file format'), error); + }) + .catch(error => { + trackAndEmitError(translate('There was an error retrieving data from Google Drive'), error); reject(error); - } - }) - .catch(error => { - trackAndEmitError(translate('There was an error retrieving data from Google Drive'), error); - reject(error); - }); + }); + } else if (data.action === google.picker.Action.CANCEL) { + reject(); + } }; this.authorise() @@ -139,6 +139,7 @@ class GoogleDrive { const picker = new google.picker.PickerBuilder(); picker + .setOrigin(`${window.location.protocol}//${window.location.host}`) .setTitle(translate('Select a Binary Bot strategy')) .setLocale(this.getPickerLanguage()) .setAppId(this.appId) @@ -149,7 +150,7 @@ class GoogleDrive { .build() .setVisible(true); }) - .catch(() => reject()); + .catch(error => reject(error)); }); } @@ -202,35 +203,35 @@ class GoogleDrive { return new Promise((resolve, reject) => { // eslint-disable-next-line consistent-return const savePickerCallback = data => { - if (data.action !== google.picker.Action.PICKED) { - return reject(google.picker.Action.CANCEL); - } - - const folderId = data.docs[0].id; - const strategyFile = new Blob([options.content], { type: options.mimeType }); - const strategyFileMetadata = JSON.stringify({ - name : options.name, - mimeType: options.mimeType, - parents : [folderId], - }); + if (data.action === google.picker.Action.PICKED) { + const folderId = data.docs[0].id; + const strategyFile = new Blob([options.content], { type: options.mimeType }); + const strategyFileMetadata = JSON.stringify({ + name : options.name, + mimeType: options.mimeType, + parents : [folderId], + }); - const formData = new FormData(); - formData.append('metadata', new Blob([strategyFileMetadata], { type: 'application/json' })); - formData.append('file', strategyFile); + const formData = new FormData(); + formData.append('metadata', new Blob([strategyFileMetadata], { type: 'application/json' })); + formData.append('file', strategyFile); - const xhr = new XMLHttpRequest(); - xhr.responseType = 'json'; - xhr.open('POST', 'https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart'); - xhr.setRequestHeader('Authorization', `Bearer ${gapi.auth.getToken().access_token}`); - xhr.onload = () => { - if (xhr.status === 200) { - resolve(xhr.response); - } else { - trackAndEmitError(translate('There was an error processing your request'), xhr.status); - reject(); - } - }; - xhr.send(formData); + const xhr = new XMLHttpRequest(); + xhr.responseType = 'json'; + xhr.open('POST', 'https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart'); + xhr.setRequestHeader('Authorization', `Bearer ${gapi.auth.getToken().access_token}`); + xhr.onload = () => { + if (xhr.status === 200) { + resolve(); + } else { + trackAndEmitError(translate('There was an error processing your request'), xhr.status); + reject(); + } + }; + xhr.send(formData); + } else if (data.action === google.picker.Action.CANCEL) { + reject(); + } }; this.authorise() @@ -246,6 +247,7 @@ class GoogleDrive { const picker = new google.picker.PickerBuilder(); picker + .setOrigin(`${window.location.protocol}//${window.location.host}`) .setTitle(translate('Select a folder')) .addView(view) .setLocale(this.getPickerLanguage()) @@ -257,7 +259,7 @@ class GoogleDrive { .setVisible(true); }); }) - .catch(() => reject()); + .catch(error => reject(error)); }); } } From 640cf5a100b4b12ea35e445581ed75fbffb06a5c Mon Sep 17 00:00:00 2001 From: Aaron Imming Date: Fri, 22 Mar 2019 18:25:19 +0800 Subject: [PATCH 28/42] Sign out user from Google Drive if access to drive was revoked --- src/common/integrations/GoogleDrive.js | 97 ++++++++++++++++---------- src/common/utils/tools.js | 2 +- 2 files changed, 63 insertions(+), 36 deletions(-) diff --git a/src/common/integrations/GoogleDrive.js b/src/common/integrations/GoogleDrive.js index a8e6257d6d..e9896cab8e 100644 --- a/src/common/integrations/GoogleDrive.js +++ b/src/common/integrations/GoogleDrive.js @@ -121,6 +121,9 @@ class GoogleDrive { } }) .catch(error => { + if (error.status && error.status === 401) { + this.signOut(); + } trackAndEmitError(translate('There was an error retrieving data from Google Drive'), error); reject(error); }); @@ -131,24 +134,37 @@ class GoogleDrive { this.authorise() .then(() => { - const mimeTypes = ['application/xml']; - const docsView = new google.picker.DocsView(); - docsView.setMimeTypes(mimeTypes.join(',')); - docsView.setIncludeFolders(true); - docsView.setOwnedByMe(true); + // FilePicker open doesn't give an unauthorised error, so check if we can list files + // first before attempting to open it (user revoked permissions through accounts.google.com) + gapi.client.drive.files + .list() + .then(() => { + const mimeTypes = ['application/xml']; + const docsView = new google.picker.DocsView(); + docsView.setMimeTypes(mimeTypes.join(',')); + docsView.setIncludeFolders(true); + docsView.setOwnedByMe(true); - const picker = new google.picker.PickerBuilder(); - picker - .setOrigin(`${window.location.protocol}//${window.location.host}`) - .setTitle(translate('Select a Binary Bot strategy')) - .setLocale(this.getPickerLanguage()) - .setAppId(this.appId) - .setOAuthToken(gapi.auth.getToken().access_token) - .addView(docsView) - .setDeveloperKey(this.apiKey) - .setCallback(userPickedFile) - .build() - .setVisible(true); + const picker = new google.picker.PickerBuilder(); + picker + .setOrigin(`${window.location.protocol}//${window.location.host}`) + .setTitle(translate('Select a Binary Bot strategy')) + .setLocale(this.getPickerLanguage()) + .setAppId(this.appId) + .setOAuthToken(gapi.auth.getToken().access_token) + .addView(docsView) + .setDeveloperKey(this.apiKey) + .setCallback(userPickedFile) + .build() + .setVisible(true); + }) + .catch(error => { + if (error.status && error.status === 401) { + this.signOut(); + } + trackAndEmitError(translate('There was an error listing files from Google Drive'), error); + reject(error); + }); }) .catch(error => reject(error)); }); @@ -184,6 +200,9 @@ class GoogleDrive { }) .then(createFileResponse => resolve(createFileResponse.result.id)) .catch(error => { + if (error.status && error.status === 401) { + this.signOut(); + } trackAndEmitError( translate('There was an error retrieving files from Google Drive'), error @@ -192,6 +211,9 @@ class GoogleDrive { }); }) .catch(error => { + if (error.status && error.status === 401) { + this.signOut(); + } trackAndEmitError(translate('There was an error listing files from Google Drive'), error); reject(error); }); @@ -224,6 +246,9 @@ class GoogleDrive { if (xhr.status === 200) { resolve(); } else { + if (xhr.status === 401) { + this.signOut(); + } trackAndEmitError(translate('There was an error processing your request'), xhr.status); reject(); } @@ -239,25 +264,27 @@ class GoogleDrive { // Calling getDefaultFolderId() ensures there's at least one folder available to save to. // FilePicker doesn't allow for folder creation, so a user without any folder in // their drive couldn't select anything. - this.getDefaultFolderId().then(() => { - const view = new google.picker.DocsView(); - view.setIncludeFolders(true) - .setSelectFolderEnabled(true) - .setMimeTypes('application/vnd.google-apps.folder'); + this.getDefaultFolderId() + .then(() => { + const view = new google.picker.DocsView(); + view.setIncludeFolders(true) + .setSelectFolderEnabled(true) + .setMimeTypes('application/vnd.google-apps.folder'); - const picker = new google.picker.PickerBuilder(); - picker - .setOrigin(`${window.location.protocol}//${window.location.host}`) - .setTitle(translate('Select a folder')) - .addView(view) - .setLocale(this.getPickerLanguage()) - .setAppId(this.appId) - .setOAuthToken(gapi.auth.getToken().access_token) - .setDeveloperKey(this.apiKey) - .setCallback(savePickerCallback) - .build() - .setVisible(true); - }); + const picker = new google.picker.PickerBuilder(); + picker + .setOrigin(`${window.location.protocol}//${window.location.host}`) + .setTitle(translate('Select a folder')) + .addView(view) + .setLocale(this.getPickerLanguage()) + .setAppId(this.appId) + .setOAuthToken(gapi.auth.getToken().access_token) + .setDeveloperKey(this.apiKey) + .setCallback(savePickerCallback) + .build() + .setVisible(true); + }) + .catch(error => reject(error)); }) .catch(error => reject(error)); }); diff --git a/src/common/utils/tools.js b/src/common/utils/tools.js index ea45df8bdc..4fddee4609 100644 --- a/src/common/utils/tools.js +++ b/src/common/utils/tools.js @@ -114,7 +114,7 @@ export const removeSpinnerInButton = ($buttonElement, initialText) => { }; export const trackAndEmitError = (message, object = {}) => { - globalObserver.emit('ui.log.warn', message); + globalObserver.emit('ui.log.error', message); if (window.trackJs) { trackJs.track(`${message} - Error: ${JSON.stringify(object)}`); } From c40d32436b718445d5ee5c65a3dc80a7f4a9b298 Mon Sep 17 00:00:00 2001 From: Aaron Imming Date: Fri, 22 Mar 2019 21:48:55 +0800 Subject: [PATCH 29/42] Show an error when user doesn't grant permission --- src/common/integrations/GoogleDrive.js | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/src/common/integrations/GoogleDrive.js b/src/common/integrations/GoogleDrive.js index e9896cab8e..409e7e243e 100644 --- a/src/common/integrations/GoogleDrive.js +++ b/src/common/integrations/GoogleDrive.js @@ -64,10 +64,26 @@ class GoogleDrive { } authorise() { - if (this.isAuthorised) { - return Promise.resolve(); - } - return this.googleAuth.signIn({ prompt: 'select_account' }); + return new Promise((resolve, reject) => { + if (this.isAuthorised) { + resolve(); + } else { + this.googleAuth + .signIn({ prompt: 'select_account' }) + .then(() => resolve()) + .catch(response => { + if (response.error === 'access_denied') { + globalObserver.emit( + 'ui.log.warn', + translate( + 'Please grant permission to view and manage Google Drive folders created with Binary Bot' + ) + ); + } + reject(response); + }); + } + }); } signOut() { From 85f79957708241c35c323cc6ba67dd2edb0a1cae Mon Sep 17 00:00:00 2001 From: Aaron Imming Date: Fri, 22 Mar 2019 23:13:52 +0800 Subject: [PATCH 30/42] Update integrations icon to Google Drive icon --- src/botPage/view/Dialogs/IntegrationsDialog.js | 13 +++++++++---- static/css/_fontello.scss | 2 +- static/image/google_drive.svg | 1 + static/image/integrations.svg | 1 - templates/bot.mustache | 2 +- 5 files changed, 12 insertions(+), 7 deletions(-) create mode 100644 static/image/google_drive.svg delete mode 100644 static/image/integrations.svg diff --git a/src/botPage/view/Dialogs/IntegrationsDialog.js b/src/botPage/view/Dialogs/IntegrationsDialog.js index 809a75af9c..0272301351 100644 --- a/src/botPage/view/Dialogs/IntegrationsDialog.js +++ b/src/botPage/view/Dialogs/IntegrationsDialog.js @@ -15,10 +15,15 @@ export default class IntegrationsDialog extends Dialog { const closeDialog = () => { this.close(); }; - super('integrations-dialog', translate('Integrations'), , { - width : 500, - height: 'auto', - }); + super( + 'integrations-dialog', + translate('Google Drive Integration'), + , + { + width : 500, + height: 'auto', + } + ); this.registerCloseOnOtherDialog(); } } diff --git a/static/css/_fontello.scss b/static/css/_fontello.scss index 055d26307e..a3fd10f779 100755 --- a/static/css/_fontello.scss +++ b/static/css/_fontello.scss @@ -93,5 +93,5 @@ &:before { content: ' '; } - background: url("../image/integrations.svg") no-repeat center; + background: url("../image/google_drive.svg") no-repeat center; } diff --git a/static/image/google_drive.svg b/static/image/google_drive.svg new file mode 100644 index 0000000000..67d50ce7c6 --- /dev/null +++ b/static/image/google_drive.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/static/image/integrations.svg b/static/image/integrations.svg deleted file mode 100644 index 7bf82226a1..0000000000 --- a/static/image/integrations.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/templates/bot.mustache b/templates/bot.mustache index 5ee66f0557..cb97e97a5e 100644 --- a/templates/bot.mustache +++ b/templates/bot.mustache @@ -113,7 +113,7 @@ - + From 504d9244a177d7e8effe2cf9116e4ca763b7b8a2 Mon Sep 17 00:00:00 2001 From: Khalid Ibrahim Date: Thu, 4 Apr 2019 13:58:08 +0800 Subject: [PATCH 31/42] index page responsive for all languages --- static/css/index.scss | 13 +++++++++++++ templates/index.mustache | 4 ++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/static/css/index.scss b/static/css/index.scss index 33789bdebc..75082d288e 100644 --- a/static/css/index.scss +++ b/static/css/index.scss @@ -82,6 +82,15 @@ ul.bullet { .show-on-load { display: none; } +#split-container { + display: flex; + flex-direction: row; + width: 100%; + .puzzle-logo { + padding-left: 20px; + } +} + /* Keep this below since css after this * will be interfering with small * screen sizes @@ -98,6 +107,10 @@ ul.bullet { } } @media only screen and (max-width: 480px) { + #split-container { + display: block; + width: auto; + } .top-image { display: block; } diff --git a/templates/index.mustache b/templates/index.mustache index 9271a2c46a..bd6ed1e43a 100644 --- a/templates/index.mustache +++ b/templates/index.mustache @@ -34,13 +34,13 @@
-
+

.

-
+
From 9e016fb24a24d76f311319206e4f3e2ab728e998 Mon Sep 17 00:00:00 2001 From: Khalid Ibrahim Date: Thu, 4 Apr 2019 14:27:35 +0800 Subject: [PATCH 32/42] deleted spaces --- static/css/_footer.scss | 2 -- 1 file changed, 2 deletions(-) diff --git a/static/css/_footer.scss b/static/css/_footer.scss index dfe1fc5eb6..65a2e31324 100644 --- a/static/css/_footer.scss +++ b/static/css/_footer.scss @@ -187,8 +187,6 @@ } @media screen and (max-width: 400px) { .regulation-logos { - - .vanuatu-icon { width: 120px; margin-left: -30px; From 674d4251bb133e18776438ff14ef45f07aeee9c8 Mon Sep 17 00:00:00 2001 From: Khalid Ibrahim Date: Fri, 5 Apr 2019 17:56:08 +0800 Subject: [PATCH 33/42] removed google+ from footer --- src/indexPage/react-components/footer.jsx | 2 -- static/image/footer/google-plus.svg | 1 - 2 files changed, 3 deletions(-) delete mode 100644 static/image/footer/google-plus.svg diff --git a/src/indexPage/react-components/footer.jsx b/src/indexPage/react-components/footer.jsx index 3fb49fa3ac..32466aeb49 100644 --- a/src/indexPage/react-components/footer.jsx +++ b/src/indexPage/react-components/footer.jsx @@ -32,7 +32,6 @@ const Footer = () => ( ( \ No newline at end of file From a9f7e807da7539781cdd5f24f5b7ba5d1d23d701 Mon Sep 17 00:00:00 2001 From: Aaron Imming Date: Thu, 21 Mar 2019 15:27:58 +0800 Subject: [PATCH 34/42] Emit bot.running event when entering position --- src/botPage/bot/TradeEngine/index.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/botPage/bot/TradeEngine/index.js b/src/botPage/bot/TradeEngine/index.js index 02ee2e475d..b018c85539 100644 --- a/src/botPage/bot/TradeEngine/index.js +++ b/src/botPage/bot/TradeEngine/index.js @@ -16,6 +16,7 @@ import Ticks from './Ticks'; import rootReducer from './state/reducers'; import * as constants from './state/constants'; import { start } from './state/actions'; +import { observer as globalObserver } from '../../../common/utils/observer'; const watchBefore = store => watchScope({ @@ -34,7 +35,7 @@ const watchDuring = store => }); /* The watchScope function is called randomly and resets the prevTick - * which leads to the same problem we try to solve. So prevTick is isolated + * which leads to the same problem we try to solve. So prevTick is isolated */ let prevTick; const watchScope = ({ store, stopScope, passScope, passFlag }) => { @@ -90,6 +91,8 @@ export default class TradeEngine extends Balance(Purchase(Sell(OpenContract(Prop throw createError('NotInitialized', translate('Bot.init is not called')); } + globalObserver.emit('bot.running'); + this.tradeOptions = expectTradeOptions(tradeOptions); this.store.dispatch(start()); From dd30bfe6e6da725c0e00895d9aa9a62529861606 Mon Sep 17 00:00:00 2001 From: Aaron Imming Date: Thu, 21 Mar 2019 15:29:47 +0800 Subject: [PATCH 35/42] Ensure correct status indicator is showing --- src/botPage/view/TradeInfoPanel/index.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/botPage/view/TradeInfoPanel/index.js b/src/botPage/view/TradeInfoPanel/index.js index 664abf034b..1cab39eaa8 100644 --- a/src/botPage/view/TradeInfoPanel/index.js +++ b/src/botPage/view/TradeInfoPanel/index.js @@ -30,6 +30,7 @@ class AnimateTrade extends Component { super(); this.indicatorMessages = { notRunning: translate('Bot is not running.'), + starting : translate('Bot is starting...'), running : translate('Bot is running...'), stopping : translate('Bot is stopping...'), stopped : translate('Bot has stopped.'), @@ -40,18 +41,24 @@ class AnimateTrade extends Component { }; } componentWillMount() { + globalObserver.register('bot.running', () => { + $('.stage-tooltip.top:eq(0)').addClass('running'); + this.setState({ indicatorMessage: this.indicatorMessages.running }); + }); globalObserver.register('bot.stop', () => { $('.stage-tooltip.top:eq(0)').removeClass('running'); this.setState({ indicatorMessage: this.indicatorMessages.stopped }); }); + $('#stopButton').click(() => { $('.stage-tooltip.top:eq(0)').removeClass('running'); this.setState({ indicatorMessage: this.state.stopMessage }); }); + $('#runButton').click(() => { resetAnimation(); $('.stage-tooltip.top:eq(0)').addClass('running'); - this.setState({ indicatorMessage: this.indicatorMessages.running }); + this.setState({ indicatorMessage: this.indicatorMessages.starting }); globalObserver.register('contract.status', contractStatus => { this.animateStage(contractStatus); }); From 742a5d63abc32147c1c5601cd14456be7411e9df Mon Sep 17 00:00:00 2001 From: Aaron Imming Date: Fri, 22 Mar 2019 14:32:03 +0800 Subject: [PATCH 36/42] Ensure correct (stop) status indicator is showing --- src/botPage/view/TradeInfoPanel/index.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/botPage/view/TradeInfoPanel/index.js b/src/botPage/view/TradeInfoPanel/index.js index 1cab39eaa8..20c70827a9 100644 --- a/src/botPage/view/TradeInfoPanel/index.js +++ b/src/botPage/view/TradeInfoPanel/index.js @@ -58,7 +58,10 @@ class AnimateTrade extends Component { $('#runButton').click(() => { resetAnimation(); $('.stage-tooltip.top:eq(0)').addClass('running'); - this.setState({ indicatorMessage: this.indicatorMessages.starting }); + this.setState({ + indicatorMessage: this.indicatorMessages.starting, + stopMessage : this.indicatorMessages.stopped, + }); globalObserver.register('contract.status', contractStatus => { this.animateStage(contractStatus); }); From 944de0f17e3bdd211916cb281b9daf93fcd50ab4 Mon Sep 17 00:00:00 2001 From: Aaron Imming Date: Wed, 10 Apr 2019 10:07:10 +0800 Subject: [PATCH 37/42] Update strings --- templates/bot.mustache | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/bot.mustache b/templates/bot.mustache index 919d316d44..3bb003d7b4 100644 --- a/templates/bot.mustache +++ b/templates/bot.mustache @@ -114,7 +114,7 @@ - + From 01d14770fb79eea44ada0b1992fa9cda43c2b4e0 Mon Sep 17 00:00:00 2001 From: Aaron Imming Date: Wed, 10 Apr 2019 10:07:44 +0800 Subject: [PATCH 38/42] Minor fixes + eslint --- src/common/integrations/GoogleDrive.js | 28 ++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/src/common/integrations/GoogleDrive.js b/src/common/integrations/GoogleDrive.js index 409e7e243e..9fbe32606e 100644 --- a/src/common/integrations/GoogleDrive.js +++ b/src/common/integrations/GoogleDrive.js @@ -3,14 +3,12 @@ import { getLanguage } from '../lang'; import { observer as globalObserver } from '../utils/observer'; import { translate, trackAndEmitError } from '../utils/tools'; import { loadWorkspace, loadBlocks } from '../../botPage/view/blockly'; +import config from '../../botPage/common/const'; class GoogleDrive { constructor() { this.botFolderName = `Binary Bot - ${translate('Strategies')}`; - this.appId = ''; - this.apiKey = ''; - this.clientId = ''; - + this.setInfo(config); this.googleAuth = null; this.isAuthorised = null; this.profile = null; @@ -43,12 +41,24 @@ class GoogleDrive { .removeClass('invisible'); }, error => { - trackAndEmitError(translate('There was an error initialising Google Drive'), error); + if (window.trackJs) { + trackJs.track( + `${translate( + 'There was an error initialising Google Drive' + )} - Error: ${JSON.stringify(error)}` + ); + } } ); }, onerror: error => { - trackAndEmitError(translate('There was an error loading Google Drive libraries'), error); + if (window.trackJs) { + trackJs.track( + `${translate('There was an error loading Google Drive libraries')} - Error: ${JSON.stringify( + error + )}` + ); + } }, }); } @@ -93,6 +103,12 @@ class GoogleDrive { return Promise.resolve(); } + setInfo(data) { + this.clientId = data.gd.cid; + this.appId = data.gd.aid; + this.apiKey = data.gd.api; + } + // eslint-disable-next-line class-methods-use-this getPickerLanguage() { const language = getLanguage(); From fb0d3335a18e030724b0bcffdd0805946060ddc5 Mon Sep 17 00:00:00 2001 From: Aaron Imming Date: Wed, 10 Apr 2019 10:08:21 +0800 Subject: [PATCH 39/42] Cleanup --- src/botPage/common/const.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/botPage/common/const.js b/src/botPage/common/const.js index dce8ceaeba..844e3fa331 100644 --- a/src/botPage/common/const.js +++ b/src/botPage/common/const.js @@ -229,6 +229,11 @@ const config = { }, bbResult : [[translate('upper'), '1'], [translate('middle'), '0'], [translate('lower'), '2']], macdFields: [[translate('Histogram'), '0'], [translate('MACD'), '1'], [translate('Signal'), '2']], + gd : { + cid: '646610722767-7ivdbunktgtnumj23en9gkecbgtf2ur7.apps.googleusercontent.com', + aid: 'binarybot-237009', + api: 'AIzaSyBieTeLip_lVQZUimIuJypU1kJyqOvQRgc', + }, }; export async function updateConfigCurrencies() { From 6f6a8ca767ac37ad0fb4f15301abda19996f2591 Mon Sep 17 00:00:00 2001 From: Aaron Imming Date: Wed, 10 Apr 2019 10:08:55 +0800 Subject: [PATCH 40/42] Update strings --- .../blocks/Scratch/Binary/Indicators/bb.js | 73 +++++++++++++++++++ .../Integrations/GoogleDriveIntegration.js | 2 +- 2 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 src/botPage/view/blockly/blocks/Scratch/Binary/Indicators/bb.js diff --git a/src/botPage/view/blockly/blocks/Scratch/Binary/Indicators/bb.js b/src/botPage/view/blockly/blocks/Scratch/Binary/Indicators/bb.js new file mode 100644 index 0000000000..9020202e0b --- /dev/null +++ b/src/botPage/view/blockly/blocks/Scratch/Binary/Indicators/bb.js @@ -0,0 +1,73 @@ +import { translate } from '../../../../../../../common/utils/tools'; +import config from '../../../../../../common/const'; + +Blockly.Blocks.bb = { + init() { + this.jsonInit({ + message0: translate('Bollinger Bands %1\n'), + message1: translate('%1 Input List %2 %3'), + message2: translate('Period %1'), + message3: translate('Std. Dev. Up Multiplier'), + message4: translate('Std. Dev. Down Multiplier'), + args0 : [ + { + type : 'field_dropdown', + name : 'BBRESULT_LIST', + options: config.bbResult, + }, + ], + args1: [ + { type: 'input_dummy' }, + { + type : 'input_value', + name : 'INPUT', + check: 'Array', + }, + { type: 'input_dummy' }, + ], + args2: [ + { + type : 'input_value', + name : 'PERIOD', + check: 'Number', + }, + ], + args3: [ + { + type : 'input_value', + name : 'UPMULTIPLIER', + check: 'Number', + }, + ], + args4: [ + { + type : 'input_value', + name : 'DOWNMULTIPLIER', + check: 'Number', + }, + ], + output : 'Number', + colour : Blockly.Colours.Binary.colour, + colourSecondary: Blockly.Colours.Binary.colourSecondary, + colourTertiary : Blockly.Colours.Binary.colourTertiary, + tooltip : translate('Calculates Bollinger Bands (BB) from a list with a period'), + }); + }, +}; + +Blockly.JavaScript.bb = block => { + const bbResult = block.getFieldValue('BBRESULT_LIST'); + const input = Blockly.JavaScript.valueToCode(block, 'INPUT') || '[]'; + const period = Blockly.JavaScript.valueToCode(block, 'PERIOD', Blockly.JavaScript.ORDER_ATOMIC) || '10'; + const stdDevUp = Blockly.JavaScript.valueToCode(block, 'UPMULTIPLIER', Blockly.JavaScript.ORDER_ATOMIC) || '5'; + const stdDevDown = Blockly.JavaScript.valueToCode(block, 'DOWNMULTIPLIER', Blockly.JavaScript.ORDER_ATOMIC) || '5'; + + const code = `Bot.bb(${input}, { + periods: ${period}, + stdDevUp: ${stdDevUp}, + stdDevDown: ${stdDevDown} + }, { + ${bbResult} + })\n`; + return [code, Blockly.JavaScript.ORDER_FUNCTION_CALL]; +}; diff --git a/src/botPage/view/react-components/Integrations/GoogleDriveIntegration.js b/src/botPage/view/react-components/Integrations/GoogleDriveIntegration.js index 38eabaca01..8d4f4c53e0 100644 --- a/src/botPage/view/react-components/Integrations/GoogleDriveIntegration.js +++ b/src/botPage/view/react-components/Integrations/GoogleDriveIntegration.js @@ -19,7 +19,7 @@ export default class GoogleDriveIntegration extends PureComponent {

Google Drive

-
{translate('Save blocks and strategies to')} Google Drive
+
{translate('Save your blocks and strategies to Google Drive')}
{googleDrive.isAuthorised && (
{`${translate('You are logged in as')} ${googleDrive.profile.getEmail()}`} From a9bc13a170ca64a782db5762c852d654a03a3be4 Mon Sep 17 00:00:00 2001 From: Aaron Imming Date: Wed, 10 Apr 2019 10:28:05 +0800 Subject: [PATCH 41/42] Catch uncaught error --- src/common/integrations/GoogleDrive.js | 86 ++++++++++++++------------ 1 file changed, 45 insertions(+), 41 deletions(-) diff --git a/src/common/integrations/GoogleDrive.js b/src/common/integrations/GoogleDrive.js index 9fbe32606e..1cdb4e61ee 100644 --- a/src/common/integrations/GoogleDrive.js +++ b/src/common/integrations/GoogleDrive.js @@ -209,47 +209,51 @@ class GoogleDrive { if (!this.isAuthorised) { authorisePromise.push(this.authorise); } - Promise.all(authorisePromise).then(() => { - gapi.client.drive.files - .list({ q: 'trashed=false' }) - // eslint-disable-next-line consistent-return - .then(response => { - const botFolder = response.result.files.find( - file => - file.name === this.botFolderName && - file.mimeType === 'application/vnd.google-apps.folder' - ); - if (botFolder) { - return resolve(botFolder.id); - } - gapi.client.drive.files - .create({ - resource: { - name : this.botFolderName, - mimeType: 'application/vnd.google-apps.folder', - fields : 'id', - }, - }) - .then(createFileResponse => resolve(createFileResponse.result.id)) - .catch(error => { - if (error.status && error.status === 401) { - this.signOut(); - } - trackAndEmitError( - translate('There was an error retrieving files from Google Drive'), - error - ); - reject(error); - }); - }) - .catch(error => { - if (error.status && error.status === 401) { - this.signOut(); - } - trackAndEmitError(translate('There was an error listing files from Google Drive'), error); - reject(error); - }); - }); + Promise.all(authorisePromise) + .then(() => { + gapi.client.drive.files + .list({ q: 'trashed=false' }) + // eslint-disable-next-line consistent-return + .then(response => { + const botFolder = response.result.files.find( + file => + file.name === this.botFolderName && + file.mimeType === 'application/vnd.google-apps.folder' + ); + if (botFolder) { + return resolve(botFolder.id); + } + gapi.client.drive.files + .create({ + resource: { + name : this.botFolderName, + mimeType: 'application/vnd.google-apps.folder', + fields : 'id', + }, + }) + .then(createFileResponse => resolve(createFileResponse.result.id)) + .catch(error => { + if (error.status && error.status === 401) { + this.signOut(); + } + trackAndEmitError( + translate('There was an error retrieving files from Google Drive'), + error + ); + reject(error); + }); + }) + .catch(error => { + if (error.status && error.status === 401) { + this.signOut(); + } + trackAndEmitError(translate('There was an error listing files from Google Drive'), error); + reject(error); + }); + }) + .catch(() => { + /* Auth error, already handled in authorise()-promise */ + }); }); } From 210747249e9cc5c6e170c2848227241e093515fe Mon Sep 17 00:00:00 2001 From: Aaron Date: Wed, 10 Apr 2019 11:08:36 +0800 Subject: [PATCH 42/42] Delete bb.js --- .../blocks/Scratch/Binary/Indicators/bb.js | 73 ------------------- 1 file changed, 73 deletions(-) delete mode 100644 src/botPage/view/blockly/blocks/Scratch/Binary/Indicators/bb.js diff --git a/src/botPage/view/blockly/blocks/Scratch/Binary/Indicators/bb.js b/src/botPage/view/blockly/blocks/Scratch/Binary/Indicators/bb.js deleted file mode 100644 index 9020202e0b..0000000000 --- a/src/botPage/view/blockly/blocks/Scratch/Binary/Indicators/bb.js +++ /dev/null @@ -1,73 +0,0 @@ -import { translate } from '../../../../../../../common/utils/tools'; -import config from '../../../../../../common/const'; - -Blockly.Blocks.bb = { - init() { - this.jsonInit({ - message0: translate('Bollinger Bands %1\n'), - message1: translate('%1 Input List %2 %3'), - message2: translate('Period %1'), - message3: translate('Std. Dev. Up Multiplier'), - message4: translate('Std. Dev. Down Multiplier'), - args0 : [ - { - type : 'field_dropdown', - name : 'BBRESULT_LIST', - options: config.bbResult, - }, - ], - args1: [ - { type: 'input_dummy' }, - { - type : 'input_value', - name : 'INPUT', - check: 'Array', - }, - { type: 'input_dummy' }, - ], - args2: [ - { - type : 'input_value', - name : 'PERIOD', - check: 'Number', - }, - ], - args3: [ - { - type : 'input_value', - name : 'UPMULTIPLIER', - check: 'Number', - }, - ], - args4: [ - { - type : 'input_value', - name : 'DOWNMULTIPLIER', - check: 'Number', - }, - ], - output : 'Number', - colour : Blockly.Colours.Binary.colour, - colourSecondary: Blockly.Colours.Binary.colourSecondary, - colourTertiary : Blockly.Colours.Binary.colourTertiary, - tooltip : translate('Calculates Bollinger Bands (BB) from a list with a period'), - }); - }, -}; - -Blockly.JavaScript.bb = block => { - const bbResult = block.getFieldValue('BBRESULT_LIST'); - const input = Blockly.JavaScript.valueToCode(block, 'INPUT') || '[]'; - const period = Blockly.JavaScript.valueToCode(block, 'PERIOD', Blockly.JavaScript.ORDER_ATOMIC) || '10'; - const stdDevUp = Blockly.JavaScript.valueToCode(block, 'UPMULTIPLIER', Blockly.JavaScript.ORDER_ATOMIC) || '5'; - const stdDevDown = Blockly.JavaScript.valueToCode(block, 'DOWNMULTIPLIER', Blockly.JavaScript.ORDER_ATOMIC) || '5'; - - const code = `Bot.bb(${input}, { - periods: ${period}, - stdDevUp: ${stdDevUp}, - stdDevDown: ${stdDevDown} - }, { - ${bbResult} - })\n`; - return [code, Blockly.JavaScript.ORDER_FUNCTION_CALL]; -};