diff --git a/.github/stale.yml b/.github/stale.yml new file mode 100644 index 0000000000..58c238a3ff --- /dev/null +++ b/.github/stale.yml @@ -0,0 +1,24 @@ +# Configuration for probot-stale - https://github.com/probot/stale + +# Limit to only `issues` +only: issues + +# Number of days of inactivity before an Issue or Pull Request is closed. +# Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale. +daysUntilClose: 14 + +# Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable +exemptLabels: + - bug + - "technical issues" + +# Comment to post when marking as stale. Set to `false` to disable +markComment: false + +# Comment to post when closing a stale Issue or Pull Request. +closeComment: > + This issue has been automatically closed since there has not been + any recent activity. Please open a new issue for related bugs. + +# Limit the number of actions per hour, from 1-30. Default is 30 +limitPerRun: 30 \ No newline at end of file diff --git a/src/botPage/bot/Interface/ToolsInterface.js b/src/botPage/bot/Interface/ToolsInterface.js index 90fb23f451..2067f0889b 100644 --- a/src/botPage/bot/Interface/ToolsInterface.js +++ b/src/botPage/bot/Interface/ToolsInterface.js @@ -1,13 +1,78 @@ import CandleInterface from './CandleInterface'; import MiscInterface from './MiscInterface'; import IndicatorsInterface from './IndicatorsInterface'; +import { translate } from '../../../common/i18n'; // prettier-ignore export default Interface => class extends IndicatorsInterface( MiscInterface(CandleInterface(Interface))) { getToolsInterface() { return { - getTime: () => parseInt(new Date().getTime() / 1000), + getTime : () => parseInt(new Date().getTime() / 1000), + toDateTime: (timestamp) => { + const getTwoDigitValue = input => { + if (input < 10) { + return `0${input}`; + } + return `${input}`; + } + const invalidTimestamp = () => `${translate('Invalid timestamp')}: ${timestamp}`; + if (typeof timestamp === 'number') { + const dateTime = new Date(timestamp * 1000); + if (dateTime.getTime()) { + const year = dateTime.getFullYear(); + const month = getTwoDigitValue(dateTime.getMonth() + 1); + const day = getTwoDigitValue(dateTime.getDate()); + const hours = getTwoDigitValue(dateTime.getHours()); + const minutes = getTwoDigitValue(dateTime.getMinutes()); + const seconds = getTwoDigitValue(dateTime.getSeconds()); + const formatGTMoffset = () => { + const GMToffsetRaw = dateTime.getTimezoneOffset(); + const sign = GMToffsetRaw > 0 ? '-' : '+'; + const GMToffset = Math.abs(GMToffsetRaw); + const h = Math.floor(GMToffset / 60); + const m = GMToffset - h * 60; + return `GMT${sign}${getTwoDigitValue(h)}${getTwoDigitValue(m)}`; + } + return `${year}-${month}-${day} ${hours}:${minutes}:${seconds} ${formatGTMoffset()}`; + } + return invalidTimestamp(); + } + return invalidTimestamp(); + }, + toTimestamp: (dateTimeString) => { + const invalidDatetime = () => `${translate('Invalid date/time')}: ${dateTimeString}`; + if (typeof dateTimeString === 'string') { + const dateTime = dateTimeString + .replace(/[^0-9.:-\s]/g, '') + .replace(/\s+/g,' ') + .trim() + .split(' '); + + const d = /^[12]\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$/; + const t = /^(0[0-9]|1[0-9]|2[0-3]):([0-5][0-9])(:([0-5][0-9])?)?$/; + + let validatedDateTime; + + if(dateTime.length >= 2) { + validatedDateTime = d.test(dateTime[0]) && t.test(dateTime[1]) ? `${dateTime[0]}T${dateTime[1]}` : null; + } else if(dateTime.length === 1) { + validatedDateTime = d.test(dateTime[0]) ? dateTime[0] : null; + } else { + validatedDateTime = null; + } + + if(validatedDateTime) { + const dateObj = new Date(validatedDateTime); + // eslint-disable-next-line no-restricted-globals + if(dateObj instanceof Date && !isNaN(dateObj)) { + return dateObj.getTime() / 1000; + } + } + return invalidDatetime(); + } + return invalidDatetime(); + }, ...this.getCandleInterface(), ...this.getMiscInterface(), ...this.getIndicatorsInterface(), diff --git a/src/botPage/bot/Interpreter.js b/src/botPage/bot/Interpreter.js index 57854d4428..1f0959e5a1 100644 --- a/src/botPage/bot/Interpreter.js +++ b/src/botPage/bot/Interpreter.js @@ -149,8 +149,10 @@ export default class Interpreter { } terminateSession() { this.$scope.api.disconnect(); - globalObserver.emit('bot.stop'); this.stopped = true; + + globalObserver.emit('bot.stop'); + globalObserver.setState({ isRunning: false }); } stop() { if (this.bot.tradeEngine.isSold === false && !this.isErrorTriggered) { diff --git a/src/botPage/bot/TradeEngine/OpenContract.js b/src/botPage/bot/TradeEngine/OpenContract.js index 424aebe810..a5d34ff6ab 100644 --- a/src/botPage/bot/TradeEngine/OpenContract.js +++ b/src/botPage/bot/TradeEngine/OpenContract.js @@ -17,8 +17,6 @@ export default Engine => this.setContractFlags(contract); - this.sellExpired(); - this.data = this.data.set('contract', contract); broadcastContract({ accountID: this.accountInfo.loginid, ...contract }); @@ -45,11 +43,7 @@ export default Engine => this.store.dispatch(openContractReceived()); if (!this.isExpired) { this.resetSubscriptionTimeout(); - return; - } - if (!this.retriedUnsuccessfullSellExpired) { - this.retriedUnsuccessfullSellExpired = true; - this.resetSubscriptionTimeout(AFTER_FINISH_TIMEOUT); + } } }); @@ -61,7 +55,6 @@ export default Engine => } subscribeToOpenContract(contractId = this.contractId) { if (this.contractId !== contractId) { - this.retriedUnsuccessfullSellExpired = false; this.resetSubscriptionTimeout(); } this.contractId = contractId; diff --git a/src/botPage/bot/TradeEngine/Sell.js b/src/botPage/bot/TradeEngine/Sell.js index 568fa6a15a..d099463fda 100644 --- a/src/botPage/bot/TradeEngine/Sell.js +++ b/src/botPage/bot/TradeEngine/Sell.js @@ -46,9 +46,4 @@ export default Engine => delayIndex++ ).then(onSuccess); } - sellExpired() { - if (this.isSellAvailable && this.isExpired) { - doUntilDone(() => this.api.sellExpiredContracts()); - } - } }; diff --git a/src/botPage/bot/TradeEngine/Total.js b/src/botPage/bot/TradeEngine/Total.js index 355c7c6446..64a567e559 100644 --- a/src/botPage/bot/TradeEngine/Total.js +++ b/src/botPage/bot/TradeEngine/Total.js @@ -1,7 +1,7 @@ import { translate } from '../../../common/i18n'; import { roundBalance } from '../../common/tools'; import { info, notify } from '../broadcast'; -import createError from '../../common/error'; +import { createError } from '../../common/error'; import { observer as globalObserver } from '../../../common/utils/observer'; const skeleton = { diff --git a/src/botPage/bot/TradeEngine/index.js b/src/botPage/bot/TradeEngine/index.js index 9af841a0b8..8ad3f62180 100644 --- a/src/botPage/bot/TradeEngine/index.js +++ b/src/botPage/bot/TradeEngine/index.js @@ -3,7 +3,7 @@ import { createStore, applyMiddleware } from 'redux'; import thunk from 'redux-thunk'; import { durationToSecond } from '../../../common/utils/tools'; import { translate } from '../../..//common/i18n'; -import createError from '../../common/error'; +import { createError } from '../../common/error'; import { doUntilDone } from '../tools'; import { expectInitArg, expectTradeOptions } from '../sanitize'; import Proposal from './Proposal'; @@ -92,6 +92,7 @@ export default class TradeEngine extends Balance(Purchase(Sell(OpenContract(Prop } globalObserver.emit('bot.running'); + globalObserver.setState({ isRunning: true }); this.tradeOptions = expectTradeOptions(tradeOptions); diff --git a/src/botPage/bot/__tests__/block-tests/After.js b/src/botPage/bot/__tests__/block-tests/After.js index 007e3c4ab0..b6c35e8c60 100644 --- a/src/botPage/bot/__tests__/block-tests/After.js +++ b/src/botPage/bot/__tests__/block-tests/After.js @@ -21,7 +21,7 @@ describe('After Purchase Blocks', () => { it('After purchase api', () => { expectResultTypes(result, [ 'boolean', // is result win - 'string', // statement + 'number', // statement ]); }); }); diff --git a/src/botPage/bot/__tests__/block-tests/tools-test/Time.js b/src/botPage/bot/__tests__/block-tests/tools-test/Time.js index 344f02c738..0c38235b8c 100644 --- a/src/botPage/bot/__tests__/block-tests/tools-test/Time.js +++ b/src/botPage/bot/__tests__/block-tests/tools-test/Time.js @@ -27,3 +27,17 @@ describe('Time in tools', () => { expect(time2 - time1).most(3); }); }); + +describe('Convert timestamp to date/time and back', () => { + const timestamp = Math.ceil(new Date().getTime() / 1000); + let result; + beforeAll(done => { + run(`(function() {return Bot.toTimestamp(Bot.toDateTime(${timestamp}));})()`).then(v => { + result = v; + done(); + }); + }); + it('converts timestamp to date/time string', () => { + expect(result).satisfy(dt => dt === timestamp); + }); +}); diff --git a/src/botPage/bot/sanitize.js b/src/botPage/bot/sanitize.js index e5e07b6525..26c5244b8c 100644 --- a/src/botPage/bot/sanitize.js +++ b/src/botPage/bot/sanitize.js @@ -1,5 +1,5 @@ import { translate } from '../../common/i18n'; -import createError from '../common/error'; +import { createError } from '../common/error'; const isPositiveNumber = num => Number.isFinite(num) && num > 0; diff --git a/src/botPage/common/error.js b/src/botPage/common/error.js index 50ba49bf20..69458cfdb2 100644 --- a/src/botPage/common/error.js +++ b/src/botPage/common/error.js @@ -1,7 +1,13 @@ -const createError = (name, message) => { +import { observer as globalObserver } from '../../common/utils/observer'; +import { translate } from '../../common/i18n'; + +export const createError = (name, message) => { const e = new Error(message); e.name = name; return e; }; -export default createError; +export const createErrorAndEmit = (name, message) => { + globalObserver.emit('ui.log.warn', `${translate(message)}`); + return createError(name, message); +}; diff --git a/src/botPage/view/View.js b/src/botPage/view/View.js index 9dd37c1b1e..3000071b5f 100644 --- a/src/botPage/view/View.js +++ b/src/botPage/view/View.js @@ -511,9 +511,19 @@ export default class View { }); const startBot = limitations => { - $('#stopButton, #summaryStopButton').show(); - $('#runButton, #summaryRunButton').hide(); - $('#runButton, #summaryRunButton').prop('disabled', true); + const elRunButtons = document.querySelectorAll('#runButton, #summaryRunButton'); + const elStopButtons = document.querySelectorAll('#stopButton, #summaryStopButton'); + + elRunButtons.forEach(el => { + const elRunButton = el; + elRunButton.style.display = 'none'; + elRunButton.setAttributeNode(document.createAttribute('disabled')); + }); + elStopButtons.forEach(el => { + const elStopButton = el; + elStopButton.style.display = 'inline-block'; + }); + globalObserver.emit('summary.disable_clear'); showSummary(); this.blockly.run(limitations); @@ -625,6 +635,9 @@ export default class View { this.blockly.stop(); } addEventHandlers() { + const getRunButtonElements = () => document.querySelectorAll('#runButton, #summaryRunButton'); + const getStopButtonElements = () => document.querySelectorAll('#stopButton, #summaryStopButton'); + window.addEventListener('storage', e => { window.onbeforeunload = null; if (e.key === 'activeToken' && !e.newValue) window.location.reload(); @@ -632,7 +645,11 @@ export default class View { }); globalObserver.register('Error', error => { - $('#runButton, #summaryRunButton').prop('disabled', false); + getRunButtonElements().forEach(el => { + const elRunButton = el; + elRunButton.removeAttribute('disabled'); + }); + if (error.error && error.error.error.code === 'InvalidToken') { removeAllTokens(); updateTokenList(); @@ -640,8 +657,32 @@ export default class View { } }); + globalObserver.register('bot.running', () => { + getRunButtonElements().forEach(el => { + const elRunButton = el; + elRunButton.style.display = 'none'; + elRunButton.setAttributeNode(document.createAttribute('disabled')); + }); + getStopButtonElements().forEach(el => { + const elStopButton = el; + elStopButton.style.display = 'inline-block'; + elStopButton.removeAttribute('disabled'); + }); + }); + globalObserver.register('bot.stop', () => { - $('#runButton, #summaryRunButton').prop('disabled', false); + // Enable run button, this event is emitted after the interpreter + // killed the API connection. + getStopButtonElements().forEach(el => { + const elStopButton = el; + elStopButton.style.display = null; + elStopButton.removeAttribute('disabled'); + }); + getRunButtonElements().forEach(el => { + const elRunButton = el; + elRunButton.style.display = null; + elRunButton.removeAttribute('disabled'); + }); }); globalObserver.register('bot.info', info => { diff --git a/src/botPage/view/blockly/blocks/shared.js b/src/botPage/view/blockly/blocks/shared.js index 8170874564..7e5ed841fe 100644 --- a/src/botPage/view/blockly/blocks/shared.js +++ b/src/botPage/view/blockly/blocks/shared.js @@ -409,3 +409,22 @@ export const getPredictionForContracts = (contracts, selectedContractType) => { } return predictionRange; }; + +export const disableRunButton = shouldDisable => { + const elRunButtons = document.querySelectorAll('#runButton, #summaryRunButton'); + const isRunning = globalObserver.getState('isRunning'); + + elRunButtons.forEach(elRunButton => { + if (isRunning) { + if (shouldDisable) { + elRunButton.setAttributeNode(document.createAttribute('disabled')); + } else { + // Do not enable. The bot is running. + } + } else if (shouldDisable) { + elRunButton.setAttributeNode(document.createAttribute('disabled')); + } else { + elRunButton.removeAttribute('disabled'); + } + }); +}; diff --git a/src/botPage/view/blockly/blocks/tools/time/index.js b/src/botPage/view/blockly/blocks/tools/time/index.js index 2c41b91b5b..6dc16d8fa4 100644 --- a/src/botPage/view/blockly/blocks/tools/time/index.js +++ b/src/botPage/view/blockly/blocks/tools/time/index.js @@ -1,3 +1,5 @@ import './epoch'; import './timeout'; import './interval'; +import './todatetime'; +import './totimestamp'; diff --git a/src/botPage/view/blockly/blocks/tools/time/todatetime.js b/src/botPage/view/blockly/blocks/tools/time/todatetime.js new file mode 100644 index 0000000000..df1e981408 --- /dev/null +++ b/src/botPage/view/blockly/blocks/tools/time/todatetime.js @@ -0,0 +1,30 @@ +import { translate } from '../../../../../../common/i18n'; + +Blockly.Blocks.todatetime = { + init: function init() { + this.appendDummyInput(); + this.appendValueInput('TIMESTAMP').appendField(translate('To Date/Time')); + this.setInputsInline(true); + this.setOutput(true, 'String'); + this.setColour('#dedede'); + this.setTooltip( + translate( + 'Converts a number of seconds since Epoch into a string representing date and time. Example: 1546347825 will be converted to 2019-01-01 21:03:45.' + ) + ); + }, +}; + +Blockly.JavaScript.todatetime = block => { + const timestamp = Blockly.JavaScript.valueToCode(block, 'TIMESTAMP', Blockly.JavaScript.ORDER_ATOMIC); + // eslint-disable-next-line no-underscore-dangle + const functionName = Blockly.JavaScript.provideFunction_('timestampToDateString', [ + // eslint-disable-next-line no-underscore-dangle + `function ${Blockly.JavaScript.FUNCTION_NAME_PLACEHOLDER_}(timestamp) { + return Bot.toDateTime(timestamp); + }`, + ]); + + const code = `${functionName}(${timestamp})`; + return [code, Blockly.JavaScript.ORDER_FUNCTION_CALL]; +}; diff --git a/src/botPage/view/blockly/blocks/tools/time/totimestamp.js b/src/botPage/view/blockly/blocks/tools/time/totimestamp.js new file mode 100644 index 0000000000..8fdc7330c6 --- /dev/null +++ b/src/botPage/view/blockly/blocks/tools/time/totimestamp.js @@ -0,0 +1,30 @@ +import { translate } from '../../../../../../common/i18n'; + +Blockly.Blocks.totimestamp = { + init: function init() { + this.appendDummyInput(); + this.appendValueInput('DATETIME').appendField(translate('To Timestamp')); + this.setInputsInline(true); + this.setOutput(true, 'Number'); + this.setColour('#dedede'); + this.setTooltip( + translate( + 'Converts a string representing a date/time string into seconds since Epoch. Example: 2019-01-01 21:03:45 GMT+0800 will be converted to 1546347825. Time and time zone offset are optional.' + ) + ); + }, +}; + +Blockly.JavaScript.totimestamp = block => { + const dateString = Blockly.JavaScript.valueToCode(block, 'DATETIME', Blockly.JavaScript.ORDER_ATOMIC); + // eslint-disable-next-line no-underscore-dangle + const functionName = Blockly.JavaScript.provideFunction_('dateTimeStringToTimestamp', [ + // eslint-disable-next-line no-underscore-dangle + `function ${Blockly.JavaScript.FUNCTION_NAME_PLACEHOLDER_}(dateTimeString) { + return Bot.toTimestamp(dateTimeString); + }`, + ]); + + const code = `${functionName}(${dateString})`; + return [code, Blockly.JavaScript.ORDER_FUNCTION_CALL]; +}; diff --git a/src/botPage/view/blockly/blocks/trade/tradeOptions.js b/src/botPage/view/blockly/blocks/trade/tradeOptions.js index f3266c543f..83581524c3 100644 --- a/src/botPage/view/blockly/blocks/trade/tradeOptions.js +++ b/src/botPage/view/blockly/blocks/trade/tradeOptions.js @@ -5,6 +5,7 @@ import { getDurationsForContracts, getBarriersForContracts, getPredictionForContracts, + disableRunButton, } from '../shared'; import { insideTrade } from '../../relationChecker'; import { findTopParentBlock, hideInteractionsFromBlockly, getBlocksByType } from '../../utils'; @@ -102,8 +103,15 @@ export default () => { } }, pollForContracts(symbol) { + disableRunButton(true); return new Promise(resolve => { const contractsForSymbol = haveContractsForSymbol(symbol); + + const resolveContracts = resolveObj => { + disableRunButton(false); + resolve(resolveObj); + }; + if (!contractsForSymbol) { // Register an event and use as a lock to avoid spamming API const event = `contractsLoaded.${symbol}`; @@ -111,7 +119,7 @@ export default () => { globalObserver.register(event, () => {}); getContractsAvailableForSymbol(symbol).then(contracts => { globalObserver.unregisterAll(event); // Release the lock - resolve(contracts); + resolveContracts(contracts); }); } else { // Request in progress, start polling localStorage until contracts are available. @@ -119,16 +127,16 @@ export default () => { const contracts = haveContractsForSymbol(symbol); if (contracts) { clearInterval(pollingFn); - resolve(contracts.available); + resolveContracts(contracts.available); } }, 100); setTimeout(() => { clearInterval(pollingFn); - resolve([]); + resolveContracts([]); }, 10000); } } else { - resolve(contractsForSymbol.available); + resolveContracts(contractsForSymbol.available); } }); }, diff --git a/src/botPage/view/blockly/customBlockly.js b/src/botPage/view/blockly/customBlockly.js index 01f238e1ea..b4f75f1cec 100644 --- a/src/botPage/view/blockly/customBlockly.js +++ b/src/botPage/view/blockly/customBlockly.js @@ -1,6 +1,7 @@ import GTM from '../../../common/gtm'; import { translate, translateLangToLang } from '../../../common/i18n'; import { getLanguage } from '../../../common/lang'; +import { save } from './utils'; /* eslint-disable */ Blockly.WorkspaceAudio.prototype.preload = function() {}; @@ -376,3 +377,29 @@ Blockly.WorkspaceAudio.prototype.preload = function() { } } }; + +// https://groups.google.com/forum/#!msg/blockly/eS1V49pI9c8/VEh5UuUcBAAJ +const addDownloadOption = (callback, options, block) => { + options.push({ + text: translate('Download'), + enabled: true, + callback: () => { + const xml = Blockly.Xml.textToDom(''); + xml.appendChild(Blockly.Xml.blockToDom(block)); + save('binary-bot-block', true, xml); + }, + }); + callback(options); +}; + +const originalCustomContextVarFn = + Blockly.Constants.Variables.CUSTOM_CONTEXT_MENU_VARIABLE_GETTER_SETTER_MIXIN.customContextMenu; +Blockly.Constants.Variables.CUSTOM_CONTEXT_MENU_VARIABLE_GETTER_SETTER_MIXIN.customContextMenu = function(options) { + addDownloadOption(originalCustomContextVarFn.bind(this), options, this); +}; + +const originalCustomContextLoopFn = + Blockly.Constants.Loops.CUSTOM_CONTEXT_MENU_CREATE_VARIABLES_GET_MIXIN.customContextMenu; +Blockly.Constants.Loops.CUSTOM_CONTEXT_MENU_CREATE_VARIABLES_GET_MIXIN.customContextMenu = function(options) { + addDownloadOption(originalCustomContextLoopFn.bind(this), options, this); +}; diff --git a/src/botPage/view/blockly/index.js b/src/botPage/view/blockly/index.js index 0636724381..cf92dd5e2c 100644 --- a/src/botPage/view/blockly/index.js +++ b/src/botPage/view/blockly/index.js @@ -16,7 +16,7 @@ import { cleanBeforeExport, } from './utils'; import Interpreter from '../../bot/Interpreter'; -import createError from '../../common/error'; +import { createErrorAndEmit } from '../../common/error'; import { translate, xml as translateXml } from '../../../common/i18n'; import { getLanguage } from '../../../common/lang'; import { observer as globalObserver } from '../../../common/utils/observer'; @@ -315,14 +315,45 @@ export default class _Blockly { } /* eslint-disable class-methods-use-this */ load(blockStr = '', dropEvent = {}) { - let xml; + const unrecognisedMsg = () => translate('Unrecognized file format'); + try { + const xmlDoc = new DOMParser().parseFromString(blockStr, 'application/xml'); + + if (xmlDoc.getElementsByTagName('parsererror').length) { + throw new Error(); + } + } catch (err) { + throw createErrorAndEmit('FileLoad', unrecognisedMsg()); + } + + let xml; try { xml = Blockly.Xml.textToDom(blockStr); } catch (e) { - throw createError('FileLoad', translate('Unrecognized file format')); + throw createErrorAndEmit('FileLoad', unrecognisedMsg()); + } + + const blocklyXml = xml.querySelectorAll('block'); + + if (!blocklyXml.length) { + throw createErrorAndEmit( + 'FileLoad', + 'XML file contains unsupported elements. Please check or modify file.' + ); } + blocklyXml.forEach(block => { + const blockType = block.getAttribute('type'); + + if (!Object.keys(Blockly.Blocks).includes(blockType)) { + throw createErrorAndEmit( + 'FileLoad', + 'XML file contains unsupported elements. Please check or modify file' + ); + } + }); + try { if (xml.hasAttribute('collection') && xml.getAttribute('collection') === 'true') { loadBlocks(xml, dropEvent); @@ -330,7 +361,7 @@ export default class _Blockly { loadWorkspace(xml); } } catch (e) { - throw createError('FileLoad', translate('Unable to load the block file')); + throw createErrorAndEmit('FileLoad', translate('Unable to load the block file')); } } /* eslint-disable class-methods-use-this */ @@ -409,12 +440,17 @@ while(true) { } stop(stopBeforeStart) { if (!stopBeforeStart) { - const $runButtons = $('#runButton, #summaryRunButton'); - const $stopButtons = $('#stopButton, #summaryStopButton'); - if ($runButtons.is(':visible') || $stopButtons.is(':visible')) { - $runButtons.show(); - $stopButtons.hide(); - } + const elRunButtons = document.querySelectorAll('#runButton, #summaryRunButton'); + const elStopButtons = document.querySelectorAll('#stopButton, #summaryStopButton'); + + elRunButtons.forEach(el => { + const elRunButton = el; + elRunButton.style.display = 'initial'; + }); + elStopButtons.forEach(el => { + const elStopButton = el; + elStopButton.style.display = null; + }); } if (this.interpreter) { this.interpreter.stop(); diff --git a/src/common/utils/observer.js b/src/common/utils/observer.js index 9fded6642a..d0f06d51f6 100644 --- a/src/common/utils/observer.js +++ b/src/common/utils/observer.js @@ -3,6 +3,7 @@ import { Map, List } from 'immutable'; export default class Observer { constructor() { this.eam = new Map(); // event action map + this.state = {}; } register(event, _action, once, unregisterIfError, unregisterAllBefore) { if (unregisterAllBefore) { @@ -53,6 +54,12 @@ export default class Observer { this.eam.get(event).forEach(action => action.action(data)); } } + setState(state = {}) { + this.state = Object.assign({}, this.state, state); + } + getState(key) { + return this.state[key]; + } } export const observer = new Observer(); diff --git a/src/indexPage/endpoint.js b/src/indexPage/endpoint.js index 7032919acd..872d037d90 100644 --- a/src/indexPage/endpoint.js +++ b/src/indexPage/endpoint.js @@ -1,5 +1,6 @@ import { get as getStorage, set as setStorage } from '../common/utils/storageManager'; import { generateWebSocketURL, getDefaultEndpoint, generateTestLiveApiInstance } from '../common/appId'; +import { translate } from '../common/utils/tools'; if (document.location.href.endsWith('/endpoint')) { window.location.replace(`${document.location.href}.html`); @@ -64,6 +65,15 @@ function addEndpoint(e) { setStorage('config.server_url', serverUrl); setStorage('config.app_id', appId); + const urlReg = /^(?:http(s)?:\/\/)?[\w.-]+(?:.[\w.-]+)+[\w-._~:\/?#[\]@!$&'()*+,;=.]+$/; + + if (!urlReg.test(serverUrl)) { + $('#error') + .html(translate('Please enter a valid server URL')) + .show(); + return; + } + checkConnection(appId, serverUrl); } diff --git a/static/css/_blockly-toolbox.scss b/static/css/_blockly-toolbox.scss index a3f8870445..3ec6e5818c 100644 --- a/static/css/_blockly-toolbox.scss +++ b/static/css/_blockly-toolbox.scss @@ -59,7 +59,7 @@ border-width: thin; color: $brand-dark-gray; border-right: 0.063em solid; - width: 11em; + min-width: 11em; } .blocklyIconShape { diff --git a/static/css/_toolbox.scss b/static/css/_toolbox.scss index 79daa64c74..866d3a7146 100644 --- a/static/css/_toolbox.scss +++ b/static/css/_toolbox.scss @@ -27,7 +27,7 @@ z-index: 0; overflow: auto; - #runButton[disabled], #runButton[disabled]:hover { + #runButton[disabled], #runButton[disabled]:hover, #stopButton[disabled], #stopButton[disabled] { @include toolbox-runButton-disabled; } diff --git a/static/css/bot.scss b/static/css/bot.scss index a4a2063f26..76111ace27 100644 --- a/static/css/bot.scss +++ b/static/css/bot.scss @@ -169,7 +169,7 @@ body { background: black; } -#stopButton { +#stopButton, #summaryStopButton { display: none; } diff --git a/static/xml/toolbox.xml b/static/xml/toolbox.xml index a5aff41962..16c6fe0a51 100644 --- a/static/xml/toolbox.xml +++ b/static/xml/toolbox.xml @@ -408,6 +408,20 @@ + + + + yyyy-mm-dd hh:mm:ss + + + + + + + 0 + + +