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
+
+
+