Merge branch 'master' of https://github.com/ethereum/remix-project into indexdbwfcircle
commit
c62fb20e61
@ -0,0 +1,44 @@ |
||||
{ |
||||
"root": true, |
||||
"ignorePatterns": ["**/*"], |
||||
"plugins": ["@nrwl/nx"], |
||||
"overrides": [ |
||||
{ |
||||
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"], |
||||
"rules": { |
||||
"@nrwl/nx/enforce-module-boundaries": [ |
||||
"error", |
||||
{ |
||||
"enforceBuildableLibDependency": true, |
||||
"allow": [], |
||||
"depConstraints": [ |
||||
{ |
||||
"sourceTag": "*", |
||||
"onlyDependOnLibsWithTags": ["*"] |
||||
} |
||||
] |
||||
} |
||||
] |
||||
} |
||||
}, |
||||
{ |
||||
"files": ["*.ts", "*.tsx"], |
||||
"extends": ["plugin:@nrwl/nx/typescript"], |
||||
"rules": { |
||||
"@typescript-eslint/ban-ts-comment": "off", |
||||
"@typescript-eslint/no-this-alias": "off", |
||||
"@typescript-eslint/no-empty-function": "off", |
||||
"eslint-disable-next-line no-empty": "off", |
||||
"no-empty": "off" |
||||
} |
||||
}, |
||||
{ |
||||
"files": ["*.js", "*.jsx"], |
||||
"extends": ["plugin:@nrwl/nx/javascript"], |
||||
"rules": {} |
||||
} |
||||
], |
||||
"globals": { |
||||
"JSX": true |
||||
} |
||||
} |
@ -0,0 +1,15 @@ |
||||
import { NightwatchBrowser } from 'nightwatch' |
||||
import EventEmitter from 'events' |
||||
|
||||
class CurrentSelectedFileIs extends EventEmitter { |
||||
command (this: NightwatchBrowser, value: string): NightwatchBrowser { |
||||
this.api |
||||
.waitForElementContainsText('*[data-id="tabs-component"] *[data-id="tab-active"]', value) |
||||
.perform(() => { |
||||
this.emit('complete') |
||||
}) |
||||
return this |
||||
} |
||||
} |
||||
|
||||
module.exports = CurrentSelectedFileIs |
@ -1,235 +0,0 @@ |
||||
'use strict' |
||||
|
||||
import { NightwatchBrowser } from 'nightwatch' |
||||
import init from '../helpers/init' |
||||
|
||||
module.exports = { |
||||
|
||||
before: function (browser: NightwatchBrowser, done: VoidFunction) { |
||||
init(browser, done) |
||||
}, |
||||
|
||||
'Should zoom in editor ': function (browser: NightwatchBrowser) { |
||||
browser.waitForElementVisible('div[data-id="mainPanelPluginsContainer"]') |
||||
.clickLaunchIcon('filePanel') |
||||
.waitForElementVisible('div[data-id="filePanelFileExplorerTree"]') |
||||
.openFile('contracts') |
||||
.openFile('contracts/1_Storage.sol') |
||||
.waitForElementVisible('#editorView') |
||||
.checkElementStyle('.view-lines', 'font-size', '14px') |
||||
.click('*[data-id="tabProxyZoomIn"]') |
||||
.click('*[data-id="tabProxyZoomIn"]') |
||||
.checkElementStyle('.view-lines', 'font-size', '16px') |
||||
}, |
||||
|
||||
'Should zoom out editor ': function (browser: NightwatchBrowser) { |
||||
browser.waitForElementVisible('#editorView') |
||||
.checkElementStyle('.view-lines', 'font-size', '16px') |
||||
.click('*[data-id="tabProxyZoomOut"]') |
||||
.click('*[data-id="tabProxyZoomOut"]') |
||||
.checkElementStyle('.view-lines', 'font-size', '14px') |
||||
}, |
||||
|
||||
'Should display compile error in editor ': function (browser: NightwatchBrowser) { |
||||
browser.waitForElementVisible('#editorView') |
||||
.setEditorValue(storageContractWithError + 'error') |
||||
.pause(2000) |
||||
.waitForElementVisible('.margin-view-overlays .fa-exclamation-square', 120000) |
||||
.checkAnnotations('fa-exclamation-square', 29) // error
|
||||
.clickLaunchIcon('udapp') |
||||
.checkAnnotationsNotPresent('fa-exclamation-square') // error
|
||||
.clickLaunchIcon('solidity') |
||||
.checkAnnotations('fa-exclamation-square', 29) // error
|
||||
}, |
||||
|
||||
'Should minimize and maximize codeblock in editor ': '' + function (browser: NightwatchBrowser) { |
||||
browser.waitForElementVisible('#editorView') |
||||
.waitForElementVisible('.ace_open') |
||||
.click('.ace_start:nth-of-type(1)') |
||||
.waitForElementVisible('.ace_closed') |
||||
.click('.ace_start:nth-of-type(1)') |
||||
.waitForElementVisible('.ace_open') |
||||
}, |
||||
|
||||
'Should add breakpoint to editor ': function (browser: NightwatchBrowser) { |
||||
browser.waitForElementVisible('#editorView') |
||||
.waitForElementNotPresent('.margin-view-overlays .fa-circle') |
||||
.execute(() => { |
||||
(window as any).addRemixBreakpoint(1) |
||||
}, [], () => {}) |
||||
.waitForElementVisible('.margin-view-overlays .fa-circle') |
||||
}, |
||||
|
||||
'Should load syntax highlighter for ace light theme': '' + function (browser: NightwatchBrowser) { |
||||
browser.waitForElementVisible('#editorView') |
||||
.checkElementStyle('.ace_keyword', 'color', aceThemes.light.keyword) |
||||
.checkElementStyle('.ace_comment.ace_doc', 'color', aceThemes.light.comment) |
||||
.checkElementStyle('.ace_function', 'color', aceThemes.light.function) |
||||
.checkElementStyle('.ace_variable', 'color', aceThemes.light.variable) |
||||
}, |
||||
|
||||
'Should load syntax highlighter for ace dark theme': '' + function (browser: NightwatchBrowser) { |
||||
browser.waitForElementVisible('*[data-id="verticalIconsKindsettings"]') |
||||
.click('*[data-id="verticalIconsKindsettings"]') |
||||
.waitForElementVisible('*[data-id="settingsTabThemeLabelDark"]') |
||||
.click('*[data-id="settingsTabThemeLabelDark"]') |
||||
.pause(2000) |
||||
.waitForElementVisible('#editorView') |
||||
/* @todo(#2863) ch for class and not colors |
||||
.checkElementStyle('.ace_keyword', 'color', aceThemes.dark.keyword) |
||||
.checkElementStyle('.ace_comment.ace_doc', 'color', aceThemes.dark.comment) |
||||
.checkElementStyle('.ace_function', 'color', aceThemes.dark.function) |
||||
.checkElementStyle('.ace_variable', 'color', aceThemes.dark.variable) |
||||
*/ |
||||
}, |
||||
|
||||
'Should highlight source code ': function (browser: NightwatchBrowser) { |
||||
// include all files here because switching between plugins in side-panel removes highlight
|
||||
browser |
||||
.addFile('sourcehighlight.js', sourcehighlightScript) |
||||
.addFile('removeAllSourcehighlightScript.js', removeAllSourcehighlightScript) |
||||
.openFile('sourcehighlight.js') |
||||
.executeScript('remix.exeCurrent()') |
||||
.scrollToLine(32) |
||||
.waitForElementPresent('.highlightLine33', 60000) |
||||
.checkElementStyle('.highlightLine33', 'background-color', 'rgb(52, 152, 219)') |
||||
.scrollToLine(40) |
||||
.waitForElementPresent('.highlightLine41', 60000) |
||||
.checkElementStyle('.highlightLine41', 'background-color', 'rgb(52, 152, 219)') |
||||
.scrollToLine(50) |
||||
.waitForElementPresent('.highlightLine51', 60000) |
||||
.checkElementStyle('.highlightLine51', 'background-color', 'rgb(52, 152, 219)') |
||||
}, |
||||
|
||||
'Should remove 1 highlight from source code': '' + function (browser: NightwatchBrowser) { |
||||
browser.waitForElementVisible('li[data-id="treeViewLitreeViewItemremoveSourcehighlightScript.js"]') |
||||
.click('li[data-id="treeViewLitreeViewItemremoveSourcehighlightScript.js"]') |
||||
.pause(2000) |
||||
.executeScript('remix.exeCurrent()') |
||||
.waitForElementVisible('li[data-id="treeViewLitreeViewItemcontracts"]') |
||||
.click('li[data-id="treeViewLitreeViewItemcontracts"]') |
||||
.waitForElementVisible('li[data-id="treeViewLitreeViewItemcontracts/3_Ballot.sol"]') |
||||
.click('li[data-id="treeViewLitreeViewItemcontracts/3_Ballot.sol"]') |
||||
.waitForElementNotPresent('.highlightLine33', 60000) |
||||
.checkElementStyle('.highlightLine41', 'background-color', 'rgb(52, 152, 219)') |
||||
.checkElementStyle('.highlightLine51', 'background-color', 'rgb(52, 152, 219)') |
||||
}, |
||||
|
||||
'Should remove all highlights from source code ': function (browser: NightwatchBrowser) { |
||||
browser.waitForElementVisible('li[data-id="treeViewLitreeViewItemremoveAllSourcehighlightScript.js"]') |
||||
.click('li[data-id="treeViewLitreeViewItemremoveAllSourcehighlightScript.js"]') |
||||
.pause(2000) |
||||
.executeScript('remix.exeCurrent()') |
||||
.waitForElementVisible('li[data-id="treeViewLitreeViewItemcontracts/3_Ballot.sol"]') |
||||
.click('li[data-id="treeViewLitreeViewItemcontracts/3_Ballot.sol"]') |
||||
.pause(2000) |
||||
.waitForElementNotPresent('.highlightLine33', 60000) |
||||
.waitForElementNotPresent('.highlightLine41', 60000) |
||||
.waitForElementNotPresent('.highlightLine51', 60000) |
||||
.end() |
||||
} |
||||
} |
||||
|
||||
const aceThemes = { |
||||
light: { |
||||
keyword: 'rgb(147, 15, 128)', |
||||
comment: 'rgb(35, 110, 36)', |
||||
function: 'rgb(0, 0, 162)', |
||||
variable: 'rgb(253, 151, 31)' |
||||
}, |
||||
dark: { |
||||
keyword: 'rgb(0, 105, 143)', |
||||
comment: 'rgb(85, 85, 85)', |
||||
function: 'rgb(0, 174, 239)', |
||||
variable: 'rgb(153, 119, 68)' |
||||
} |
||||
} |
||||
|
||||
const sourcehighlightScript = { |
||||
content: ` |
||||
(async () => { |
||||
try { |
||||
await remix.call('fileManager', 'open', 'contracts/3_Ballot.sol') |
||||
const pos = { |
||||
start: { |
||||
line: 32, |
||||
column: 3 |
||||
}, |
||||
end: { |
||||
line: 32, |
||||
column: 20 |
||||
} |
||||
} |
||||
await remix.call('editor', 'highlight', pos, 'contracts/3_Ballot.sol') |
||||
|
||||
const pos2 = { |
||||
start: { |
||||
line: 40, |
||||
column: 3 |
||||
}, |
||||
end: { |
||||
line: 40, |
||||
column: 20 |
||||
} |
||||
} |
||||
await remix.call('editor', 'highlight', pos2, 'contracts/3_Ballot.sol') |
||||
|
||||
const pos3 = { |
||||
start: { |
||||
line: 50, |
||||
column: 3 |
||||
}, |
||||
end: { |
||||
line: 50, |
||||
column: 20 |
||||
} |
||||
} |
||||
await remix.call('editor', 'highlight', pos3, 'contracts/3_Ballot.sol') |
||||
} catch (e) { |
||||
console.log(e.message) |
||||
} |
||||
})() |
||||
` |
||||
} |
||||
|
||||
const removeAllSourcehighlightScript = { |
||||
content: ` |
||||
(async () => { |
||||
try { |
||||
await remix.call('editor', 'discardHighlight')
|
||||
} catch (e) { |
||||
console.log(e.message) |
||||
} |
||||
})() |
||||
` |
||||
} |
||||
|
||||
const storageContractWithError = ` |
||||
// SPDX-License-Identifier: GPL-3.0
|
||||
|
||||
pragma solidity >=0.7.0 <0.9.0; |
||||
|
||||
/** |
||||
* @title Storage |
||||
* @dev Store & retrieve value in a variable |
||||
*/ |
||||
contract Storage { |
||||
|
||||
uint256 number; |
||||
|
||||
/** |
||||
* @dev Store value in variable |
||||
* @param num value to store |
||||
*/ |
||||
function store(uint256 num) public { |
||||
number = num; |
||||
} |
||||
|
||||
/** |
||||
* @dev Return value
|
||||
* @return value of 'number' |
||||
*/ |
||||
function retrieve() public view returns (uint256){ |
||||
return number; |
||||
} |
||||
}` |
@ -0,0 +1,494 @@ |
||||
'use strict' |
||||
|
||||
import { NightwatchBrowser } from 'nightwatch' |
||||
import init from '../helpers/init' |
||||
|
||||
module.exports = { |
||||
|
||||
before: function (browser: NightwatchBrowser, done: VoidFunction) { |
||||
init(browser, done, 'http://127.0.0.1:8080', true) |
||||
}, |
||||
|
||||
'Should zoom in editor #group1': function (browser: NightwatchBrowser) { |
||||
browser.waitForElementVisible('div[data-id="mainPanelPluginsContainer"]') |
||||
.clickLaunchIcon('filePanel') |
||||
.waitForElementVisible('div[data-id="filePanelFileExplorerTree"]') |
||||
.openFile('contracts') |
||||
.openFile('contracts/1_Storage.sol') |
||||
.waitForElementVisible('#editorView') |
||||
.checkElementStyle('.view-lines', 'font-size', '14px') |
||||
.click('*[data-id="tabProxyZoomIn"]') |
||||
.click('*[data-id="tabProxyZoomIn"]') |
||||
.checkElementStyle('.view-lines', 'font-size', '16px') |
||||
}, |
||||
|
||||
'Should zoom out editor #group1': function (browser: NightwatchBrowser) { |
||||
browser.waitForElementVisible('#editorView') |
||||
.checkElementStyle('.view-lines', 'font-size', '16px') |
||||
.click('*[data-id="tabProxyZoomOut"]') |
||||
.click('*[data-id="tabProxyZoomOut"]') |
||||
.checkElementStyle('.view-lines', 'font-size', '14px') |
||||
}, |
||||
|
||||
'Should display compile error in editor #group1': function (browser: NightwatchBrowser) { |
||||
browser.waitForElementVisible('#editorView') |
||||
.setEditorValue(storageContractWithError + 'error') |
||||
.pause(2000) |
||||
.waitForElementVisible('.margin-view-overlays .fa-exclamation-square', 120000) |
||||
.checkAnnotations('fa-exclamation-square', 29) // error
|
||||
.clickLaunchIcon('udapp') |
||||
.checkAnnotationsNotPresent('fa-exclamation-square') // error
|
||||
.clickLaunchIcon('solidity') |
||||
.checkAnnotations('fa-exclamation-square', 29) // error
|
||||
}, |
||||
|
||||
'Should minimize and maximize codeblock in editor #group1': '' + function (browser: NightwatchBrowser) { |
||||
browser.waitForElementVisible('#editorView') |
||||
.waitForElementVisible('.ace_open') |
||||
.click('.ace_start:nth-of-type(1)') |
||||
.waitForElementVisible('.ace_closed') |
||||
.click('.ace_start:nth-of-type(1)') |
||||
.waitForElementVisible('.ace_open') |
||||
}, |
||||
|
||||
'Should add breakpoint to editor #group1': function (browser: NightwatchBrowser) { |
||||
browser.waitForElementVisible('#editorView') |
||||
.waitForElementNotPresent('.margin-view-overlays .fa-circle') |
||||
.execute(() => { |
||||
(window as any).addRemixBreakpoint(1) |
||||
}, [], () => {}) |
||||
.waitForElementVisible('.margin-view-overlays .fa-circle') |
||||
}, |
||||
|
||||
'Should load syntax highlighter for ace light theme #group1': '' + function (browser: NightwatchBrowser) { |
||||
browser.waitForElementVisible('#editorView') |
||||
.checkElementStyle('.ace_keyword', 'color', aceThemes.light.keyword) |
||||
.checkElementStyle('.ace_comment.ace_doc', 'color', aceThemes.light.comment) |
||||
.checkElementStyle('.ace_function', 'color', aceThemes.light.function) |
||||
.checkElementStyle('.ace_variable', 'color', aceThemes.light.variable) |
||||
}, |
||||
|
||||
'Should load syntax highlighter for ace dark theme #group1': '' + function (browser: NightwatchBrowser) { |
||||
browser.waitForElementVisible('*[data-id="verticalIconsKindsettings"]') |
||||
.click('*[data-id="verticalIconsKindsettings"]') |
||||
.waitForElementVisible('*[data-id="settingsTabThemeLabelDark"]') |
||||
.click('*[data-id="settingsTabThemeLabelDark"]') |
||||
.pause(2000) |
||||
.waitForElementVisible('#editorView') |
||||
/* @todo(#2863) ch for class and not colors |
||||
.checkElementStyle('.ace_keyword', 'color', aceThemes.dark.keyword) |
||||
.checkElementStyle('.ace_comment.ace_doc', 'color', aceThemes.dark.comment) |
||||
.checkElementStyle('.ace_function', 'color', aceThemes.dark.function) |
||||
.checkElementStyle('.ace_variable', 'color', aceThemes.dark.variable) |
||||
*/ |
||||
}, |
||||
|
||||
'Should highlight source code #group1': function (browser: NightwatchBrowser) { |
||||
// include all files here because switching between plugins in side-panel removes highlight
|
||||
browser |
||||
.addFile('sourcehighlight.js', sourcehighlightScript) |
||||
.addFile('removeAllSourcehighlightScript.js', removeAllSourcehighlightScript) |
||||
.openFile('sourcehighlight.js') |
||||
.executeScript('remix.exeCurrent()') |
||||
.scrollToLine(32) |
||||
.waitForElementPresent('.highlightLine33', 60000) |
||||
.checkElementStyle('.highlightLine33', 'background-color', 'rgb(52, 152, 219)') |
||||
.scrollToLine(40) |
||||
.waitForElementPresent('.highlightLine41', 60000) |
||||
.checkElementStyle('.highlightLine41', 'background-color', 'rgb(52, 152, 219)') |
||||
.scrollToLine(50) |
||||
.waitForElementPresent('.highlightLine51', 60000) |
||||
.checkElementStyle('.highlightLine51', 'background-color', 'rgb(52, 152, 219)') |
||||
}, |
||||
|
||||
'Should remove 1 highlight from source code #group1': '' + function (browser: NightwatchBrowser) { |
||||
browser.waitForElementVisible('li[data-id="treeViewLitreeViewItemremoveSourcehighlightScript.js"]') |
||||
.click('li[data-id="treeViewLitreeViewItemremoveSourcehighlightScript.js"]') |
||||
.pause(2000) |
||||
.executeScript('remix.exeCurrent()') |
||||
.waitForElementVisible('li[data-id="treeViewLitreeViewItemcontracts"]') |
||||
.click('li[data-id="treeViewLitreeViewItemcontracts"]') |
||||
.waitForElementVisible('li[data-id="treeViewLitreeViewItemcontracts/3_Ballot.sol"]') |
||||
.click('li[data-id="treeViewLitreeViewItemcontracts/3_Ballot.sol"]') |
||||
.waitForElementNotPresent('.highlightLine33', 60000) |
||||
.checkElementStyle('.highlightLine41', 'background-color', 'rgb(52, 152, 219)') |
||||
.checkElementStyle('.highlightLine51', 'background-color', 'rgb(52, 152, 219)') |
||||
}, |
||||
|
||||
'Should remove all highlights from source code #group1': function (browser: NightwatchBrowser) { |
||||
browser.waitForElementVisible('li[data-id="treeViewLitreeViewItemremoveAllSourcehighlightScript.js"]') |
||||
.click('li[data-id="treeViewLitreeViewItemremoveAllSourcehighlightScript.js"]') |
||||
.pause(2000) |
||||
.executeScript('remix.exeCurrent()') |
||||
.waitForElementVisible('li[data-id="treeViewLitreeViewItemcontracts/3_Ballot.sol"]') |
||||
.click('li[data-id="treeViewLitreeViewItemcontracts/3_Ballot.sol"]') |
||||
.pause(2000) |
||||
.waitForElementNotPresent('.highlightLine33', 60000) |
||||
.waitForElementNotPresent('.highlightLine41', 60000) |
||||
.waitForElementNotPresent('.highlightLine51', 60000) |
||||
}, |
||||
|
||||
'Should display the context view #group2': function (browser: NightwatchBrowser) { |
||||
browser |
||||
.openFile('contracts') |
||||
.openFile('contracts/1_Storage.sol') |
||||
.waitForElementVisible('#editorView') |
||||
.setEditorValue(storageContractWithError) |
||||
.pause(2000) |
||||
.execute(() => { |
||||
(document.getElementById('editorView') as any).gotoLine(17, 16) |
||||
}, [], () => {}) |
||||
.waitForElementVisible('.contextview') |
||||
.waitForElementContainsText('.contextview .type', 'FunctionDefinition') |
||||
.waitForElementContainsText('.contextview .name', 'store') |
||||
.execute(() => { |
||||
(document.getElementById('editorView') as any).gotoLine(18, 12) |
||||
}, [], () => {}) |
||||
.waitForElementContainsText('.contextview .type', 'uint256') |
||||
.waitForElementContainsText('.contextview .name', 'number') |
||||
.click('.contextview [data-action="previous"]') // declaration
|
||||
.pause(1000) |
||||
.execute(() => { |
||||
return (document.getElementById('editorView') as any).getCursorPosition() |
||||
}, [], (result) => { |
||||
console.log('result', result) |
||||
browser.assert.equal(result.value, '180') |
||||
}) |
||||
.click('.contextview [data-action="next"]') // back to the initial state
|
||||
.pause(1000) |
||||
.execute(() => { |
||||
return (document.getElementById('editorView') as any).getCursorPosition() |
||||
}, [], (result) => { |
||||
console.log('result', result) |
||||
browser.assert.equal(result.value, '323') |
||||
}) |
||||
.click('.contextview [data-action="next"]') // next reference
|
||||
.pause(1000) |
||||
.execute(() => { |
||||
return (document.getElementById('editorView') as any).getCursorPosition() |
||||
}, [], (result) => { |
||||
console.log('result', result) |
||||
browser.assert.equal(result.value, '489') |
||||
}) |
||||
.click('.contextview [data-action="gotoref"]') // back to the declaration
|
||||
.pause(1000) |
||||
.execute(() => { |
||||
return (document.getElementById('editorView') as any).getCursorPosition() |
||||
}, [], (result) => { |
||||
console.log('result', result) |
||||
browser.assert.equal(result.value, '180') |
||||
}) |
||||
}, |
||||
|
||||
'Should display the context view, loop over "Owner" by switching file #group2': function (browser: NightwatchBrowser) { |
||||
browser |
||||
.clickLaunchIcon('solidity') |
||||
.click('[for="autoCompile"]') // disable auto compile
|
||||
.openFile('contracts') |
||||
.openFile('contracts/3_Ballot.sol') |
||||
.waitForElementVisible('#editorView') |
||||
.setEditorValue(BallotWithARefToOwner) |
||||
.clickLaunchIcon('solidity') |
||||
.click('*[data-id="compilerContainerCompileBtn"]') // compile
|
||||
.pause(2000) |
||||
.execute(() => { |
||||
(document.getElementById('editorView') as any).gotoLine(14, 6) |
||||
}, [], () => {}) |
||||
.waitForElementVisible('.contextview') |
||||
.waitForElementContainsText('.contextview .type', 'ContractDefinition') |
||||
.waitForElementContainsText('.contextview .name', 'Owner') |
||||
.click('.contextview [data-action="next"]') |
||||
.pause(1000) |
||||
.execute(() => { |
||||
return (document.getElementById('editorView') as any).getCursorPosition() |
||||
}, [], (result) => { |
||||
console.log('result', result) |
||||
browser.assert.equal(result.value, '1061') |
||||
}) |
||||
.click('.contextview [data-action="next"]') |
||||
.pause(1000) |
||||
.execute(() => { |
||||
return (document.getElementById('editorView') as any).getCursorPosition() |
||||
}, [], (result) => { |
||||
console.log('result', result) |
||||
browser.assert.equal(result.value, '122') |
||||
}) |
||||
.currentSelectedFileIs('2_Owner.sol') // make sure the current file has been properly changed
|
||||
.click('.contextview [data-action="next"]') |
||||
.pause(1000) |
||||
.execute(() => { |
||||
return (document.getElementById('editorView') as any).getCursorPosition() |
||||
}, [], (result) => { |
||||
console.log('result', result) |
||||
browser.assert.equal(result.value, '211') |
||||
}) |
||||
.click('.contextview [data-action="next"]') |
||||
.currentSelectedFileIs('3_Ballot.sol') |
||||
.pause(1000) |
||||
.execute(() => { |
||||
return (document.getElementById('editorView') as any).getCursorPosition() |
||||
}, [], (result) => { |
||||
console.log('result', result) |
||||
browser.assert.equal(result.value, '1061') |
||||
}) |
||||
.click('.contextview [data-action="gotoref"]') // go to the declaration
|
||||
.pause(1000) |
||||
.execute(() => { |
||||
return (document.getElementById('editorView') as any).getCursorPosition() |
||||
}, [], (result) => { |
||||
console.log('result', result) |
||||
browser.assert.equal(result.value, '122') |
||||
}) |
||||
.end() |
||||
} |
||||
} |
||||
|
||||
const aceThemes = { |
||||
light: { |
||||
keyword: 'rgb(147, 15, 128)', |
||||
comment: 'rgb(35, 110, 36)', |
||||
function: 'rgb(0, 0, 162)', |
||||
variable: 'rgb(253, 151, 31)' |
||||
}, |
||||
dark: { |
||||
keyword: 'rgb(0, 105, 143)', |
||||
comment: 'rgb(85, 85, 85)', |
||||
function: 'rgb(0, 174, 239)', |
||||
variable: 'rgb(153, 119, 68)' |
||||
} |
||||
} |
||||
|
||||
const sourcehighlightScript = { |
||||
content: ` |
||||
(async () => { |
||||
try { |
||||
await remix.call('fileManager', 'open', 'contracts/3_Ballot.sol') |
||||
const pos = { |
||||
start: { |
||||
line: 32, |
||||
column: 3 |
||||
}, |
||||
end: { |
||||
line: 32, |
||||
column: 20 |
||||
} |
||||
} |
||||
await remix.call('editor', 'highlight', pos, 'contracts/3_Ballot.sol') |
||||
|
||||
const pos2 = { |
||||
start: { |
||||
line: 40, |
||||
column: 3 |
||||
}, |
||||
end: { |
||||
line: 40, |
||||
column: 20 |
||||
} |
||||
} |
||||
await remix.call('editor', 'highlight', pos2, 'contracts/3_Ballot.sol') |
||||
|
||||
const pos3 = { |
||||
start: { |
||||
line: 50, |
||||
column: 3 |
||||
}, |
||||
end: { |
||||
line: 50, |
||||
column: 20 |
||||
} |
||||
} |
||||
await remix.call('editor', 'highlight', pos3, 'contracts/3_Ballot.sol') |
||||
} catch (e) { |
||||
console.log(e.message) |
||||
} |
||||
})() |
||||
` |
||||
} |
||||
|
||||
const removeAllSourcehighlightScript = { |
||||
content: ` |
||||
(async () => { |
||||
try { |
||||
await remix.call('editor', 'discardHighlight')
|
||||
} catch (e) { |
||||
console.log(e.message) |
||||
} |
||||
})() |
||||
` |
||||
} |
||||
|
||||
const storageContractWithError = ` |
||||
// SPDX-License-Identifier: GPL-3.0
|
||||
|
||||
pragma solidity >=0.7.0 <0.9.0; |
||||
|
||||
/** |
||||
* @title Storage |
||||
* @dev Store & retrieve value in a variable |
||||
*/ |
||||
contract Storage { |
||||
|
||||
uint256 number; |
||||
|
||||
/** |
||||
* @dev Store value in variable |
||||
* @param num value to store |
||||
*/ |
||||
function store(uint256 num) public { |
||||
number = num; |
||||
} |
||||
|
||||
/** |
||||
* @dev Return value
|
||||
* @return value of 'number' |
||||
*/ |
||||
function retrieve() public view returns (uint256){ |
||||
return number; |
||||
} |
||||
}` |
||||
|
||||
const BallotWithARefToOwner = ` |
||||
|
||||
|
||||
// SPDX-License-Identifier: GPL-3.0
|
||||
|
||||
pragma solidity >=0.7.0 <0.9.0; |
||||
|
||||
import "./2_Owner.sol"; |
||||
|
||||
/** |
||||
* @title Ballot |
||||
* @dev Implements voting process along with vote delegation |
||||
*/ |
||||
contract Ballot { |
||||
Owner c; |
||||
struct Voter { |
||||
uint weight; // weight is accumulated by delegation
|
||||
bool voted; // if true, that person already voted
|
||||
address delegate; // person delegated to
|
||||
uint vote; // index of the voted proposal
|
||||
} |
||||
|
||||
struct Proposal { |
||||
// If you can limit the length to a certain number of bytes,
|
||||
// always use one of bytes1 to bytes32 because they are much cheaper
|
||||
bytes32 name; // short name (up to 32 bytes)
|
||||
uint voteCount; // number of accumulated votes
|
||||
} |
||||
|
||||
address public chairperson; |
||||
|
||||
mapping(address => Voter) public voters; |
||||
|
||||
Proposal[] public proposals; |
||||
|
||||
/** |
||||
* @dev Create a new ballot to choose one of 'proposalNames'. |
||||
* @param proposalNames names of proposals |
||||
*/ |
||||
constructor(bytes32[] memory proposalNames) { |
||||
c = new Owner(); |
||||
chairperson = msg.sender; |
||||
voters[chairperson].weight = 1; |
||||
|
||||
for (uint i = 0; i < proposalNames.length; i++) { |
||||
// 'Proposal({...})' creates a temporary
|
||||
// Proposal object and 'proposals.push(...)'
|
||||
// appends it to the end of 'proposals'.
|
||||
proposals.push(Proposal({ |
||||
name: proposalNames[i], |
||||
voteCount: 0 |
||||
})); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* @dev Give 'voter' the right to vote on this ballot. May only be called by 'chairperson'. |
||||
* @param voter address of voter |
||||
*/ |
||||
function giveRightToVote(address voter) public { |
||||
require( |
||||
msg.sender == chairperson, |
||||
"Only chairperson can give right to vote." |
||||
); |
||||
require( |
||||
!voters[voter].voted, |
||||
"The voter already voted." |
||||
); |
||||
require(voters[voter].weight == 0); |
||||
voters[voter].weight = 1; |
||||
} |
||||
|
||||
/** |
||||
* @dev Delegate your vote to the voter 'to'. |
||||
* @param to address to which vote is delegated |
||||
*/ |
||||
function delegate(address to) public { |
||||
Voter storage sender = voters[msg.sender]; |
||||
require(!sender.voted, "You already voted."); |
||||
require(to != msg.sender, "Self-delegation is disallowed."); |
||||
|
||||
while (voters[to].delegate != address(0)) { |
||||
to = voters[to].delegate; |
||||
|
||||
// We found a loop in the delegation, not allowed.
|
||||
require(to != msg.sender, "Found loop in delegation."); |
||||
} |
||||
sender.voted = true; |
||||
sender.delegate = to; |
||||
Voter storage delegate_ = voters[to]; |
||||
if (delegate_.voted) { |
||||
// If the delegate already voted,
|
||||
// directly add to the number of votes
|
||||
proposals[delegate_.vote].voteCount += sender.weight; |
||||
} else { |
||||
// If the delegate did not vote yet,
|
||||
// add to her weight.
|
||||
delegate_.weight += sender.weight; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* @dev Give your vote (including votes delegated to you) to proposal 'proposals[proposal].name'. |
||||
* @param proposal index of proposal in the proposals array |
||||
*/ |
||||
function vote(uint proposal) public { |
||||
Voter storage sender = voters[msg.sender]; |
||||
require(sender.weight != 0, "Has no right to vote"); |
||||
require(!sender.voted, "Already voted."); |
||||
sender.voted = true; |
||||
sender.vote = proposal; |
||||
|
||||
// If 'proposal' is out of the range of the array,
|
||||
// this will throw automatically and revert all
|
||||
// changes.
|
||||
proposals[proposal].voteCount += sender.weight; |
||||
} |
||||
|
||||
/** |
||||
* @dev Computes the winning proposal taking all previous votes into account. |
||||
* @return winningProposal_ index of winning proposal in the proposals array |
||||
*/ |
||||
function winningProposal() public view |
||||
returns (uint winningProposal_) |
||||
{ |
||||
uint winningVoteCount = 0; |
||||
for (uint p = 0; p < proposals.length; p++) { |
||||
if (proposals[p].voteCount > winningVoteCount) { |
||||
winningVoteCount = proposals[p].voteCount; |
||||
winningProposal_ = p; |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* @dev Calls winningProposal() function to get the index of the winner contained in the proposals array and then |
||||
* @return winnerName_ the name of the winner |
||||
*/ |
||||
function winnerName() public view |
||||
returns (bytes32 winnerName_) |
||||
{ |
||||
winnerName_ = proposals[winningProposal()].name; |
||||
} |
||||
} |
||||
` |
@ -0,0 +1,62 @@ |
||||
'use strict' |
||||
import { NightwatchBrowser } from 'nightwatch' |
||||
import init from '../helpers/init' |
||||
|
||||
const testData = { |
||||
validURL: 'https://github.com/OpenZeppelin/openzeppelin-solidity/blob/67bca857eedf99bf44a4b6a0fc5b5ed553135316/contracts/access/Roles.sol', |
||||
invalidURL: 'https://github.com/Oppelin/Roles.sol' |
||||
} |
||||
|
||||
module.exports = { |
||||
before: function (browser: NightwatchBrowser, done: VoidFunction) { |
||||
init(browser, done) |
||||
}, |
||||
|
||||
'Import from GitHub Modal': function (browser: NightwatchBrowser) { |
||||
browser.clickLaunchIcon('home') |
||||
.waitForElementVisible('*[data-id="remixIdeIconPanel"]', 10000) |
||||
.clickLaunchIcon('filePanel') |
||||
.click('div[title="home"]') |
||||
.waitForElementVisible('button[data-id="landingPageImportFromGitHubButton"]') |
||||
.pause(1000) |
||||
.scrollAndClick('button[data-id="landingPageImportFromGitHubButton"]') |
||||
.waitForElementVisible('*[data-id="homeTabModalDialogModalTitle-react"]') |
||||
.assert.containsText('*[data-id="homeTabModalDialogModalTitle-react"]', 'Import from Github') |
||||
.waitForElementVisible('*[data-id="homeTabModalDialogModalBody-react"]') |
||||
.assert.containsText('*[data-id="homeTabModalDialogModalBody-react"]', 'Enter the github URL you would like to load.') |
||||
.waitForElementVisible('*[data-id="homeTabModalDialogCustomPromptText"]') |
||||
.refresh() |
||||
}, |
||||
|
||||
'Display Error Message For Invalid GitHub URL Modal': function (browser: NightwatchBrowser) { |
||||
browser |
||||
.waitForElementVisible('*[data-id="remixIdeIconPanel"]', 10000) |
||||
.scrollAndClick('*[data-id="landingPageImportFromGitHubButton"]') |
||||
.waitForElementVisible('input[data-id="homeTabModalDialogCustomPromptText"]') |
||||
.execute(() => { |
||||
(document.querySelector('input[data-id="homeTabModalDialogCustomPromptText"]') as any).focus() |
||||
}, [], () => {}) |
||||
.setValue('input[data-id="homeTabModalDialogCustomPromptText"]', testData.invalidURL) |
||||
.waitForElementVisible('*[data-id="homeTab-modal-footer-ok-react"]') |
||||
.scrollAndClick('[data-id="homeTab-modal-footer-ok-react"]') // submitted
|
||||
.waitForElementVisible('*[data-shared="tooltipPopup"]') |
||||
.assert.containsText('*[data-shared="tooltipPopup"] span', 'not found ' + testData.invalidURL) |
||||
}, |
||||
|
||||
'Import From Github For Valid URL': function (browser: NightwatchBrowser) { |
||||
browser |
||||
.waitForElementVisible('*[data-id="remixIdeIconPanel"]', 10000) |
||||
.scrollAndClick('*[data-id="landingPageImportFromGitHubButton"]') |
||||
.waitForElementVisible('*[data-id="homeTabModalDialogCustomPromptText"]') |
||||
.clearValue('*[data-id="homeTabModalDialogCustomPromptText"]') |
||||
.execute(() => { |
||||
(document.querySelector('input[data-id="homeTabModalDialogCustomPromptText"]') as any).focus() |
||||
}, [], () => {}) |
||||
.setValue('input[data-id="homeTabModalDialogCustomPromptText"]', testData.validURL) |
||||
.waitForElementVisible('*[data-id="homeTab-modal-footer-ok-react"]') |
||||
.scrollAndClick('[data-id="homeTab-modal-footer-ok-react"]') |
||||
.openFile('github/OpenZeppelin/openzeppelin-solidity/contracts/access/Roles.sol') |
||||
.waitForElementVisible("div[title='default_workspace/github/OpenZeppelin/openzeppelin-solidity/contracts/access/Roles.sol'") |
||||
.end() |
||||
} |
||||
} |
@ -1,6 +0,0 @@ |
||||
'use strict' |
||||
import * as test from './solidityImport.test' |
||||
import buildGroupTest from '../helpers/buildgrouptest' |
||||
const group = 'group1' |
||||
|
||||
module.exports = buildGroupTest(group, test) |
@ -1,31 +0,0 @@ |
||||
import { AbstractPanel } from './panel' |
||||
import * as packageJson from '../../../../../package.json' |
||||
const csjs = require('csjs-inject') |
||||
const yo = require('yo-yo') |
||||
|
||||
const css = csjs` |
||||
.pluginsContainer { |
||||
display: none; |
||||
} |
||||
` |
||||
|
||||
const profile = { |
||||
name: 'hiddenPanel', |
||||
displayName: 'Hidden Panel', |
||||
description: '', |
||||
version: packageJson.version, |
||||
methods: ['addView', 'removeView'] |
||||
} |
||||
|
||||
export class HiddenPanel extends AbstractPanel { |
||||
constructor () { |
||||
super(profile) |
||||
} |
||||
|
||||
render () { |
||||
return yo` |
||||
<div class=${css.pluginsContainer}> |
||||
${this.view} |
||||
</div>` |
||||
} |
||||
} |
@ -0,0 +1,37 @@ |
||||
// eslint-disable-next-line no-use-before-define
|
||||
import React from 'react' |
||||
import ReactDOM from 'react-dom' // eslint-disable-line
|
||||
import { AbstractPanel } from './panel' |
||||
import * as packageJson from '../../../../../package.json' |
||||
import { RemixPluginPanel } from '@remix-ui/panel' |
||||
|
||||
const profile = { |
||||
name: 'hiddenPanel', |
||||
displayName: 'Hidden Panel', |
||||
description: '', |
||||
version: packageJson.version, |
||||
methods: ['addView', 'removeView'] |
||||
} |
||||
|
||||
export class HiddenPanel extends AbstractPanel { |
||||
el: HTMLElement |
||||
constructor () { |
||||
super(profile) |
||||
this.el = document.createElement('div') |
||||
this.el.setAttribute('class', 'pluginsContainer') |
||||
} |
||||
|
||||
addView (profile: any, view: any): void { |
||||
super.removeView(profile) |
||||
super.addView(profile, view) |
||||
this.renderComponent() |
||||
} |
||||
|
||||
render () { |
||||
return this.el |
||||
} |
||||
|
||||
renderComponent () { |
||||
ReactDOM.render(<RemixPluginPanel header={<></>} plugins={this.plugins}/>, this.el) |
||||
} |
||||
} |
@ -1,38 +0,0 @@ |
||||
import { AbstractPanel } from './panel' |
||||
import * as packageJson from '../../../../../package.json' |
||||
const yo = require('yo-yo') |
||||
const csjs = require('csjs-inject') |
||||
|
||||
const css = csjs` |
||||
.pluginsContainer { |
||||
height: 100%; |
||||
display: flex; |
||||
overflow-y: hidden; |
||||
} |
||||
` |
||||
|
||||
const profile = { |
||||
name: 'mainPanel', |
||||
displayName: 'Main Panel', |
||||
description: '', |
||||
version: packageJson.version, |
||||
methods: ['addView', 'removeView'] |
||||
} |
||||
|
||||
export class MainPanel extends AbstractPanel { |
||||
constructor () { |
||||
super(profile) |
||||
} |
||||
|
||||
focus (name) { |
||||
this.emit('focusChanged', name) |
||||
super.focus(name) |
||||
} |
||||
|
||||
render () { |
||||
return yo` |
||||
<div class=${css.pluginsContainer} data-id="mainPanelPluginsContainer" id='mainPanelPluginsContainer-id'> |
||||
${this.view} |
||||
</div>` |
||||
} |
||||
} |
@ -0,0 +1,57 @@ |
||||
import React from 'react' // eslint-disable-line
|
||||
import { AbstractPanel } from './panel' |
||||
import ReactDOM from 'react-dom' // eslint-disable-line
|
||||
import { RemixPluginPanel } from '@remix-ui/panel' |
||||
import packageJson from '../../../../../package.json' |
||||
|
||||
const profile = { |
||||
name: 'mainPanel', |
||||
displayName: 'Main Panel', |
||||
description: '', |
||||
version: packageJson.version, |
||||
methods: ['addView', 'removeView', 'showContent'] |
||||
} |
||||
|
||||
export class MainPanel extends AbstractPanel { |
||||
element: HTMLDivElement |
||||
constructor (config) { |
||||
super(profile) |
||||
this.element = document.createElement('div') |
||||
this.element.setAttribute('data-id', 'mainPanelPluginsContainer') |
||||
this.element.setAttribute('style', 'height: 100%; width: 100%;') |
||||
// this.config = config
|
||||
} |
||||
|
||||
onActivation () { |
||||
this.renderComponent() |
||||
} |
||||
|
||||
focus (name) { |
||||
this.emit('focusChanged', name) |
||||
super.focus(name) |
||||
this.renderComponent() |
||||
} |
||||
|
||||
addView (profile, view) { |
||||
super.addView(profile, view) |
||||
this.renderComponent() |
||||
} |
||||
|
||||
removeView (profile) { |
||||
super.removeView(profile) |
||||
this.renderComponent() |
||||
} |
||||
|
||||
async showContent (name) { |
||||
super.showContent(name) |
||||
this.renderComponent() |
||||
} |
||||
|
||||
render () { |
||||
return this.element |
||||
} |
||||
|
||||
renderComponent () { |
||||
ReactDOM.render(<RemixPluginPanel header={<></>} plugins={this.plugins}/>, this.element) |
||||
} |
||||
} |
@ -1,111 +0,0 @@ |
||||
import { EventEmitter } from 'events' |
||||
import { HostPlugin } from '@remixproject/engine-web' |
||||
const csjs = require('csjs-inject') |
||||
const yo = require('yo-yo') |
||||
|
||||
const css = csjs` |
||||
.plugins { |
||||
height: 100%; |
||||
} |
||||
.plugItIn { |
||||
display : none; |
||||
height : 100%; |
||||
} |
||||
.plugItIn > div { |
||||
overflow-y : auto; |
||||
overflow-x : hidden; |
||||
height : 100%; |
||||
width : 100%; |
||||
} |
||||
.plugItIn.active { |
||||
display : block; |
||||
} |
||||
.pluginsContainer { |
||||
height : 100%; |
||||
overflow-y : hidden; |
||||
} |
||||
` |
||||
|
||||
/** Abstract class used for hosting the view of a plugin */ |
||||
export class AbstractPanel extends HostPlugin { |
||||
constructor (profile) { |
||||
super(profile) |
||||
this.events = new EventEmitter() |
||||
this.contents = {} |
||||
this.active = undefined |
||||
|
||||
// View where the plugin HTMLElement leaves
|
||||
this.view = yo`<div id="plugins" class="${css.plugins}"></div>` |
||||
} |
||||
|
||||
/** |
||||
* Add the plugin to the panel |
||||
* @param {String} name the name of the plugin |
||||
* @param {HTMLElement} content the HTMLContent of the plugin |
||||
*/ |
||||
add (view, name) { |
||||
if (this.contents[name]) throw new Error(`Plugin ${name} already rendered`) |
||||
view.style.height = '100%' |
||||
view.style.width = '100%' |
||||
view.style.border = '0' |
||||
|
||||
const isIframe = view.tagName === 'IFRAME' |
||||
view.style.display = isIframe ? 'none' : 'block' |
||||
const loading = isIframe ? yo` |
||||
<div class="d-flex justify-content-center align-items-center"> |
||||
<div class="spinner-border" role="status"> |
||||
<span class="sr-only">Loading...</span> |
||||
</div> |
||||
</div> |
||||
` : ''
|
||||
this.contents[name] = yo`<div class="${css.plugItIn}" >${view}${loading}</div>` |
||||
|
||||
if (view.tagName === 'IFRAME') { |
||||
view.addEventListener('load', () => { |
||||
if (this.contents[name].contains(loading)) { |
||||
this.contents[name].removeChild(loading) |
||||
} |
||||
view.style.display = 'block' |
||||
}) |
||||
} |
||||
this.contents[name].style.display = 'none' |
||||
this.view.appendChild(this.contents[name]) |
||||
} |
||||
|
||||
addView (profile, view) { |
||||
this.add(view, profile.name) |
||||
} |
||||
|
||||
removeView (profile) { |
||||
this.remove(profile.name) |
||||
} |
||||
|
||||
/** |
||||
* Remove a plugin from the panel |
||||
* @param {String} name The name of the plugin to remove |
||||
*/ |
||||
remove (name) { |
||||
const el = this.contents[name] |
||||
delete this.contents[name] |
||||
if (el) el.parentElement.removeChild(el) |
||||
if (name === this.active) this.active = undefined |
||||
} |
||||
|
||||
/** |
||||
* Display the content of this specific plugin |
||||
* @param {String} name The name of the plugin to display the content |
||||
*/ |
||||
showContent (name) { |
||||
if (!this.contents[name]) throw new Error(`Plugin ${name} is not yet activated`) |
||||
// hiding the current view and display the `moduleName`
|
||||
if (this.active) { |
||||
this.contents[this.active].style.display = 'none' |
||||
} |
||||
this.contents[name].style.display = 'flex' |
||||
this.active = name |
||||
} |
||||
|
||||
focus (name) { |
||||
this.showContent(name) |
||||
} |
||||
} |
@ -0,0 +1,62 @@ |
||||
import React from 'react' // eslint-disable-line
|
||||
import { EventEmitter } from 'events' |
||||
import { HostPlugin } from '@remixproject/engine-web' // eslint-disable-line
|
||||
import { PluginRecord } from 'libs/remix-ui/panel/src/lib/types' |
||||
const EventManager = require('../../lib/events') |
||||
|
||||
export class AbstractPanel extends HostPlugin { |
||||
events: EventEmitter |
||||
event: any |
||||
public plugins: Record<string, PluginRecord> = {} |
||||
constructor (profile) { |
||||
super(profile) |
||||
this.events = new EventEmitter() |
||||
this.event = new EventManager() |
||||
} |
||||
|
||||
currentFocus (): string { |
||||
return Object.values(this.plugins).find(plugin => { |
||||
return plugin.active |
||||
}).profile.name |
||||
} |
||||
|
||||
addView (profile, view) { |
||||
if (this.plugins[profile.name]) throw new Error(`Plugin ${profile.name} already rendered`) |
||||
this.plugins[profile.name] = { |
||||
profile: profile, |
||||
view: view, |
||||
active: false, |
||||
class: 'plugItIn active' |
||||
} |
||||
} |
||||
|
||||
removeView (profile) { |
||||
this.emit('pluginDisabled', profile.name) |
||||
this.call('menuicons', 'unlinkContent', profile) |
||||
this.remove(profile.name) |
||||
} |
||||
|
||||
/** |
||||
* Remove a plugin from the panel |
||||
* @param {String} name The name of the plugin to remove |
||||
*/ |
||||
remove (name) { |
||||
delete this.plugins[name] |
||||
} |
||||
|
||||
/** |
||||
* Display the content of this specific plugin |
||||
* @param {String} name The name of the plugin to display the content |
||||
*/ |
||||
showContent (name) { |
||||
if (!this.plugins[name]) throw new Error(`Plugin ${name} is not yet activated`)
|
||||
Object.values(this.plugins).forEach(plugin => { |
||||
plugin.active = false |
||||
}) |
||||
this.plugins[name].active = true |
||||
} |
||||
|
||||
focus (name) { |
||||
this.showContent(name) |
||||
} |
||||
} |
@ -1,147 +0,0 @@ |
||||
const yo = require('yo-yo') |
||||
const csjs = require('csjs-inject') |
||||
const modalDialog = require('../ui/modaldialog') |
||||
|
||||
const css = csjs` |
||||
.remixui_permissions { |
||||
position: sticky; |
||||
bottom: 0; |
||||
display: flex; |
||||
justify-content: flex-end; |
||||
align-items: center; |
||||
padding: 5px 20px; |
||||
} |
||||
.permissions button { |
||||
padding: 2px 5px; |
||||
cursor: pointer; |
||||
} |
||||
.permissionForm h4 { |
||||
font-size: 1.3rem; |
||||
text-align: center; |
||||
} |
||||
.permissionForm h6 { |
||||
font-size: 1.1rem; |
||||
} |
||||
.permissionForm hr { |
||||
width: 80%; |
||||
} |
||||
.permissionKey { |
||||
display: flex; |
||||
justify-content: space-between; |
||||
align-items: center; |
||||
} |
||||
.permissionKey i { |
||||
cursor: pointer; |
||||
} |
||||
.checkbox { |
||||
display: flex; |
||||
align-items: center; |
||||
} |
||||
.checkbox label { |
||||
margin: 0; |
||||
font-size: 1rem; |
||||
} |
||||
` |
||||
|
||||
export class PluginManagerSettings { |
||||
constructor () { |
||||
const fromLocal = window.localStorage.getItem('plugins/permissions') |
||||
this.permissions = JSON.parse(fromLocal || '{}') |
||||
} |
||||
|
||||
openDialog () { |
||||
this.currentSetting = this.settings() |
||||
modalDialog('Plugin Manager Permissions', this.currentSetting, |
||||
{ fn: () => this.onValidation() } |
||||
) |
||||
} |
||||
|
||||
onValidation () { |
||||
const permissions = JSON.stringify(this.permissions) |
||||
window.localStorage.setItem('plugins/permissions', permissions) |
||||
} |
||||
|
||||
/** Clear one permission from a plugin */ |
||||
clearPersmission (from, to, method) { |
||||
// eslint-disable-next-line no-debugger
|
||||
debugger |
||||
if (this.permissions[to] && this.permissions[to][method]) { |
||||
delete this.permissions[to][method][from] |
||||
if (Object.keys(this.permissions[to][method]).length === 0) { |
||||
delete this.permissions[to][method] |
||||
} |
||||
if (Object.keys(this.permissions[to]).length === 0) { |
||||
delete this.permissions[to] |
||||
} |
||||
yo.update(this.currentSetting, this.settings()) |
||||
} |
||||
} |
||||
|
||||
/** Clear all persmissions from a plugin */ |
||||
clearAllPersmission (to) { |
||||
// eslint-disable-next-line no-debugger
|
||||
debugger |
||||
if (!this.permissions[to]) return |
||||
delete this.permissions[to] |
||||
yo.update(this.currentSetting, this.settings()) |
||||
} |
||||
|
||||
settings () { |
||||
const permissionByToPlugin = (toPlugin, funcObj) => { |
||||
const permissionByMethod = (methodName, fromPlugins) => { |
||||
const togglePermission = (fromPlugin) => { |
||||
this.permissions[toPlugin][methodName][fromPlugin].allow = !this.permissions[toPlugin][methodName][fromPlugin].allow |
||||
} |
||||
return Object.keys(fromPlugins).map(fromName => { |
||||
const fromPluginPermission = fromPlugins[fromName] |
||||
const checkbox = fromPluginPermission.allow |
||||
? yo`<input onchange=${() => togglePermission(fromName)} class="mr-2" type="checkbox" checked id="permission-checkbox-${toPlugin}-${methodName}-${toPlugin}" aria-describedby="module ${fromPluginPermission} asks permission for ${methodName}" />` |
||||
: yo`<input onchange=${() => togglePermission(fromName)} class="mr-2" type="checkbox" id="permission-checkbox-${toPlugin}-${methodName}-${toPlugin}" aria-describedby="module ${fromPluginPermission} asks permission for ${methodName}" />` |
||||
return yo` |
||||
<div class="form-group ${css.permissionKey}"> |
||||
<div class="${css.checkbox}"> |
||||
${checkbox} |
||||
<label for="permission-checkbox-${toPlugin}-${methodName}-${toPlugin}" data-id="permission-label-${toPlugin}-${methodName}-${toPlugin}">Allow <u>${fromName}</u> to call <u>${methodName}</u></label> |
||||
</div> |
||||
<i onclick="${() => this.clearPersmission(fromName, toPlugin, methodName)}" class="fa fa-trash-alt" data-id="pluginManagerSettingsRemovePermission-${toPlugin}-${methodName}-${toPlugin}"></i> |
||||
</div> |
||||
` |
||||
}) |
||||
} |
||||
|
||||
const permissionsByFunctions = Object |
||||
.keys(funcObj) |
||||
.map(methodName => permissionByMethod(methodName, funcObj[methodName])) |
||||
|
||||
return yo` |
||||
<div border p-2> |
||||
<div class="pb-2 ${css.permissionKey}"> |
||||
<h3>${toPlugin} permissions:</h3> |
||||
<i onclick="${() => this.clearAllPersmission(toPlugin)}" class="far fa-trash-alt" data-id="pluginManagerSettingsClearAllPermission-${toPlugin}"></i> |
||||
</div> |
||||
${permissionsByFunctions} |
||||
</div>` |
||||
} |
||||
|
||||
const byToPlugin = Object |
||||
.keys(this.permissions) |
||||
.map(toPlugin => permissionByToPlugin(toPlugin, this.permissions[toPlugin])) |
||||
|
||||
const title = byToPlugin.length === 0 |
||||
? yo`<h4>No Permission requested yet.</h4>` |
||||
: yo`<h4>Current Permission settings</h4>` |
||||
|
||||
return yo`<form class="${css.permissionForm}" data-id="pluginManagerSettingsPermissionForm">
|
||||
${title} |
||||
<hr/> |
||||
${byToPlugin} |
||||
</form>` |
||||
} |
||||
|
||||
render () { |
||||
return yo` |
||||
<footer class="bg-light ${css.permissions} remix-bg-opacity"> |
||||
<button onclick="${() => this.openDialog()}" class="btn btn-primary settings-button" data-id="pluginManagerPermissionsButton">Permissions</button> |
||||
</footer>` |
||||
} |
||||
} |
@ -1,156 +0,0 @@ |
||||
import { AbstractPanel } from './panel' |
||||
import * as packageJson from '../../../../../package.json' |
||||
const csjs = require('csjs-inject') |
||||
const yo = require('yo-yo') |
||||
|
||||
const css = csjs` |
||||
.panel { |
||||
width: 100%; |
||||
height: 100%; |
||||
display: flex; |
||||
flex-direction: column; |
||||
flex: auto; |
||||
} |
||||
.swapitTitle { |
||||
margin: 0; |
||||
text-transform: uppercase; |
||||
white-space: nowrap; |
||||
overflow: hidden; |
||||
text-overflow: ellipsis; |
||||
} |
||||
.swapitTitle i{ |
||||
padding-left: 6px; |
||||
font-size: 14px; |
||||
} |
||||
.swapitHeader { |
||||
display: flex; |
||||
align-items: center; |
||||
padding: 16px 24px 15px; |
||||
justify-content: space-between; |
||||
} |
||||
.icons i { |
||||
height: 80%; |
||||
cursor: pointer; |
||||
} |
||||
.pluginsContainer { |
||||
height: 100%; |
||||
overflow-y: auto; |
||||
} |
||||
.titleInfo { |
||||
padding-left: 10px; |
||||
} |
||||
.versionBadge { |
||||
background-color: var(--light); |
||||
padding: 0 7px; |
||||
font-weight: bolder; |
||||
margin-left: 5px; |
||||
text-transform: lowercase; |
||||
cursor: default; |
||||
} |
||||
` |
||||
|
||||
const sidePanel = { |
||||
name: 'sidePanel', |
||||
displayName: 'Side Panel', |
||||
description: '', |
||||
version: packageJson.version, |
||||
methods: ['addView', 'removeView'] |
||||
} |
||||
|
||||
// TODO merge with vertical-icons.js
|
||||
export class SidePanel extends AbstractPanel { |
||||
constructor (appManager, verticalIcons) { |
||||
super(sidePanel) |
||||
this.appManager = appManager |
||||
this.header = yo`<header></header>` |
||||
this.renderHeader() |
||||
this.verticalIcons = verticalIcons |
||||
|
||||
// Toggle content
|
||||
verticalIcons.events.on('toggleContent', (name) => { |
||||
if (!this.contents[name]) return |
||||
if (this.active === name) { |
||||
// TODO: Only keep `this.emit` (issue#2210)
|
||||
this.emit('toggle', name) |
||||
this.events.emit('toggle', name) |
||||
return |
||||
} |
||||
this.showContent(name) |
||||
// TODO: Only keep `this.emit` (issue#2210)
|
||||
this.emit('showing', name) |
||||
this.events.emit('showing', name) |
||||
}) |
||||
// Force opening
|
||||
verticalIcons.events.on('showContent', (name) => { |
||||
if (!this.contents[name]) return |
||||
this.showContent(name) |
||||
// TODO: Only keep `this.emit` (issue#2210)
|
||||
this.emit('showing', name) |
||||
this.events.emit('showing', name) |
||||
}) |
||||
} |
||||
|
||||
focus (name) { |
||||
this.emit('focusChanged', name) |
||||
super.focus(name) |
||||
} |
||||
|
||||
removeView (profile) { |
||||
super.removeView(profile) |
||||
this.emit('pluginDisabled', profile.name) |
||||
this.verticalIcons.unlinkContent(profile) |
||||
} |
||||
|
||||
addView (profile, view) { |
||||
super.addView(profile, view) |
||||
this.verticalIcons.linkContent(profile) |
||||
} |
||||
|
||||
/** |
||||
* Display content and update the header |
||||
* @param {String} name The name of the plugin to display |
||||
*/ |
||||
async showContent (name) { |
||||
super.showContent(name) |
||||
this.renderHeader() |
||||
this.emit('focusChanged', name) |
||||
} |
||||
|
||||
/** The header of the side panel */ |
||||
async renderHeader () { |
||||
let name = ' - ' |
||||
let docLink = '' |
||||
let versionWarning |
||||
if (this.active) { |
||||
const profile = await this.appManager.getProfile(this.active) |
||||
name = profile.displayName ? profile.displayName : profile.name |
||||
docLink = profile.documentation ? yo`<a href="${profile.documentation}" class="${css.titleInfo}" title="link to documentation" target="_blank"><i aria-hidden="true" class="fas fa-book"></i></a>` : '' |
||||
if (profile.version && profile.version.match(/\b(\w*alpha\w*)\b/g)) { |
||||
versionWarning = yo`<small title="Version Alpha" class="badge-light ${css.versionBadge}">alpha</small>` |
||||
} |
||||
// Beta
|
||||
if (profile.version && profile.version.match(/\b(\w*beta\w*)\b/g)) { |
||||
versionWarning = yo`<small title="Version Beta" class="badge-light ${css.versionBadge}">beta</small>` |
||||
} |
||||
} |
||||
|
||||
const header = yo` |
||||
<header class="${css.swapitHeader}"> |
||||
<h6 class="${css.swapitTitle}" data-id="sidePanelSwapitTitle">${name}</h6> |
||||
${docLink} |
||||
${versionWarning} |
||||
</header> |
||||
` |
||||
yo.update(this.header, header) |
||||
} |
||||
|
||||
render () { |
||||
return yo` |
||||
<section class="${css.panel} plugin-manager"> |
||||
${this.header} |
||||
<div class="${css.pluginsContainer}"> |
||||
${this.view} |
||||
</div> |
||||
</section>` |
||||
} |
||||
} |
@ -0,0 +1,88 @@ |
||||
// eslint-disable-next-line no-use-before-define
|
||||
import React from 'react' |
||||
import ReactDOM from 'react-dom' |
||||
import { AbstractPanel } from './panel' |
||||
import { RemixPluginPanel } from '@remix-ui/panel' |
||||
import packageJson from '../../../../../package.json' |
||||
import RemixUIPanelHeader from 'libs/remix-ui/panel/src/lib/plugins/panel-header' |
||||
// const csjs = require('csjs-inject')
|
||||
|
||||
const sidePanel = { |
||||
name: 'sidePanel', |
||||
displayName: 'Side Panel', |
||||
description: '', |
||||
version: packageJson.version, |
||||
methods: ['addView', 'removeView'] |
||||
} |
||||
|
||||
export class SidePanel extends AbstractPanel { |
||||
sideelement: any |
||||
constructor() { |
||||
super(sidePanel) |
||||
this.sideelement = document.createElement('section') |
||||
this.sideelement.setAttribute('class', 'panel plugin-manager') |
||||
} |
||||
|
||||
onActivation() { |
||||
this.renderComponent() |
||||
// Toggle content
|
||||
this.on('menuicons', 'toggleContent', (name) => { |
||||
if (!this.plugins[name]) return |
||||
if (this.plugins[name].active) { |
||||
// TODO: Only keep `this.emit` (issue#2210)
|
||||
this.emit('toggle', name) |
||||
this.events.emit('toggle', name) |
||||
return |
||||
} |
||||
this.showContent(name) |
||||
// TODO: Only keep `this.emit` (issue#2210)
|
||||
this.emit('showing', name) |
||||
this.events.emit('showing', name) |
||||
}) |
||||
// Force opening
|
||||
this.on('menuicons', 'showContent', (name) => { |
||||
if (!this.plugins[name]) return |
||||
this.showContent(name) |
||||
// TODO: Only keep `this.emit` (issue#2210)
|
||||
this.emit('showing', name) |
||||
this.events.emit('showing', name) |
||||
}) |
||||
} |
||||
|
||||
focus(name) { |
||||
this.emit('focusChanged', name) |
||||
super.focus(name) |
||||
} |
||||
|
||||
removeView(profile) { |
||||
if (this.plugins[profile.name].active) this.call('menuicons', 'select', 'filePanel') |
||||
super.removeView(profile) |
||||
this.emit('pluginDisabled', profile.name) |
||||
this.call('menuicons', 'unlinkContent', profile) |
||||
this.renderComponent() |
||||
} |
||||
|
||||
addView(profile, view) { |
||||
super.addView(profile, view) |
||||
this.call('menuicons', 'linkContent', profile) |
||||
this.renderComponent() |
||||
} |
||||
|
||||
/** |
||||
* Display content and update the header |
||||
* @param {String} name The name of the plugin to display |
||||
*/ |
||||
async showContent(name) { |
||||
super.showContent(name) |
||||
this.emit('focusChanged', name) |
||||
this.renderComponent() |
||||
} |
||||
|
||||
render() { |
||||
return this.sideelement |
||||
} |
||||
|
||||
renderComponent() { |
||||
ReactDOM.render(<RemixPluginPanel header={<RemixUIPanelHeader plugins={this.plugins}></RemixUIPanelHeader>} plugins={this.plugins} />, this.sideelement) |
||||
} |
||||
} |
@ -1,355 +0,0 @@ |
||||
import * as packageJson from '../../../../../package.json' |
||||
import { basicLogo } from '../ui/svgLogo' |
||||
var yo = require('yo-yo') |
||||
var csjs = require('csjs-inject') |
||||
var helper = require('../../lib/helper') |
||||
const globalRegistry = require('../../global/registry') |
||||
const contextMenu = require('../ui/contextMenu') |
||||
const { Plugin } = require('@remixproject/engine') |
||||
const EventEmitter = require('events') |
||||
let VERTICALMENU_HANDLE |
||||
|
||||
const profile = { |
||||
name: 'menuicons', |
||||
displayName: 'Vertical Icons', |
||||
description: '', |
||||
version: packageJson.version, |
||||
methods: ['select'] |
||||
} |
||||
|
||||
// TODO merge with side-panel.js. VerticalIcons should not be a plugin
|
||||
export class VerticalIcons extends Plugin { |
||||
constructor (appManager) { |
||||
super(profile) |
||||
this.events = new EventEmitter() |
||||
this.appManager = appManager |
||||
this.icons = {} |
||||
this.iconKind = {} |
||||
this.iconStatus = {} |
||||
|
||||
const themeModule = globalRegistry.get('themeModule').api |
||||
themeModule.events.on('themeChanged', (theme) => { |
||||
this.onThemeChanged(theme.quality) |
||||
}) |
||||
} |
||||
|
||||
linkContent (profile) { |
||||
if (!profile.icon) return |
||||
this.addIcon(profile) |
||||
this.listenOnStatus(profile) |
||||
} |
||||
|
||||
unlinkContent (profile) { |
||||
this.removeIcon(profile) |
||||
} |
||||
|
||||
listenOnStatus (profile) { |
||||
// the list of supported keys. 'none' will remove the status
|
||||
const keys = ['edited', 'succeed', 'none', 'loading', 'failed'] |
||||
const types = ['error', 'warning', 'success', 'info', ''] |
||||
const fn = (status) => { |
||||
if (!types.includes(status.type) && status.type) throw new Error(`type should be ${keys.join()}`) |
||||
if (status.key === undefined) throw new Error('status key should be defined') |
||||
|
||||
if (typeof status.key === 'string' && (!keys.includes(status.key))) { |
||||
throw new Error('key should contain either number or ' + keys.join()) |
||||
} |
||||
this.setIconStatus(profile.name, status) |
||||
} |
||||
this.iconStatus[profile.name] = fn |
||||
this.on(profile.name, 'statusChanged', this.iconStatus[profile.name]) |
||||
} |
||||
|
||||
/** |
||||
* Add an icon to the map |
||||
* @param {ModuleProfile} profile The profile of the module |
||||
*/ |
||||
addIcon ({ kind, name, icon, displayName, tooltip, documentation }) { |
||||
let title = (tooltip || displayName || name) |
||||
title = title.replace(/^\w/, c => c.toUpperCase()) |
||||
this.icons[name] = yo` |
||||
<div |
||||
class="${css.icon} m-2" |
||||
onclick="${() => { this.toggle(name) }}" |
||||
plugin="${name}" |
||||
title="${title}" |
||||
oncontextmenu="${(e) => this.itemContextMenu(e, name, documentation)}" |
||||
data-id="verticalIconsKind${name}" |
||||
id="verticalIconsKind${name}" |
||||
> |
||||
<img class="image" src="${icon}" alt="${name}" /> |
||||
</div>` |
||||
this.iconKind[kind || 'none'].appendChild(this.icons[name]) |
||||
} |
||||
|
||||
/** |
||||
* resolve a classes list for @arg key |
||||
* @param {Object} key |
||||
* @param {Object} type |
||||
*/ |
||||
resolveClasses (key, type) { |
||||
let classes = css.status |
||||
switch (key) { |
||||
case 'succeed': |
||||
classes += ' fas fa-check-circle text-' + type + ' ' + css.statusCheck |
||||
break |
||||
case 'edited': |
||||
classes += ' fas fa-sync text-' + type + ' ' + css.statusCheck |
||||
break |
||||
case 'loading': |
||||
classes += ' fas fa-spinner text-' + type + ' ' + css.statusCheck |
||||
break |
||||
case 'failed': |
||||
classes += ' fas fa-exclamation-triangle text-' + type + ' ' + css.statusCheck |
||||
break |
||||
default: { |
||||
classes += ' badge badge-pill badge-' + type |
||||
} |
||||
} |
||||
return classes |
||||
} |
||||
|
||||
/** |
||||
* Set a new status for the @arg name |
||||
* @param {String} name |
||||
* @param {Object} status |
||||
*/ |
||||
setIconStatus (name, status) { |
||||
const el = this.icons[name] |
||||
if (!el) return |
||||
const statusEl = el.querySelector('i') |
||||
if (statusEl) { |
||||
el.removeChild(statusEl) |
||||
} |
||||
if (status.key === 'none') return // remove status
|
||||
|
||||
let text = '' |
||||
let key = '' |
||||
if (typeof status.key === 'number') { |
||||
key = status.key.toString() |
||||
text = key |
||||
} else key = helper.checkSpecialChars(status.key) ? '' : status.key |
||||
|
||||
let type = '' |
||||
if (status.type === 'error') { |
||||
type = 'danger' // to use with bootstrap
|
||||
} else type = helper.checkSpecialChars(status.type) ? '' : status.type |
||||
const title = helper.checkSpecialChars(status.title) ? '' : status.title |
||||
|
||||
el.appendChild(yo`<i
|
||||
title="${title}" |
||||
class="${this.resolveClasses(key, type)}" |
||||
aria-hidden="true" |
||||
> |
||||
${text} |
||||
</i>`) |
||||
|
||||
el.classList.add(`${css.icon}`) |
||||
} |
||||
|
||||
/** |
||||
* Remove an icon from the map |
||||
* @param {ModuleProfile} profile The profile of the module |
||||
*/ |
||||
removeIcon ({ kind, name }) { |
||||
if (this.icons[name]) this.iconKind[kind || 'none'].removeChild(this.icons[name]) |
||||
} |
||||
|
||||
/** |
||||
* Remove active for the current activated icons |
||||
*/ |
||||
removeActive () { |
||||
// reset filters
|
||||
const images = this.view.querySelectorAll('.image') |
||||
images.forEach(function (im) { |
||||
im.style.setProperty('filter', 'invert(0.5)') |
||||
}) |
||||
|
||||
// remove active
|
||||
const currentActive = this.view.querySelector('.active') |
||||
if (currentActive) { |
||||
currentActive.classList.remove('active') |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Add active for the new activated icon |
||||
* @param {string} name Name of profile of the module to activate |
||||
*/ |
||||
addActive (name) { |
||||
if (name === 'home') return |
||||
const themeType = globalRegistry.get('themeModule').api.currentTheme().quality |
||||
const invert = themeType === 'dark' ? 1 : 0 |
||||
const brightness = themeType === 'dark' ? '150' : '0' // should be >100 for icons with color
|
||||
const nextActive = this.view.querySelector(`[plugin="${name}"]`) |
||||
if (nextActive) { |
||||
const image = nextActive.querySelector('.image') |
||||
nextActive.classList.add('active') |
||||
image.style.setProperty('filter', `invert(${invert}) grayscale(1) brightness(${brightness}%)`) |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Set an icon as active |
||||
* @param {string} name Name of profile of the module to activate |
||||
*/ |
||||
select (name) { |
||||
this.updateActivations(name) |
||||
// TODO: Only keep `this.emit` (issue#2210)
|
||||
this.emit('showContent', name) |
||||
this.events.emit('showContent', name) |
||||
} |
||||
|
||||
/** |
||||
* Toggles the side panel for plugin |
||||
* @param {string} name Name of profile of the module to activate |
||||
*/ |
||||
toggle (name) { |
||||
this.updateActivations(name) |
||||
// TODO: Only keep `this.emit` (issue#2210)
|
||||
this.emit('toggleContent', name) |
||||
this.events.emit('toggleContent', name) |
||||
} |
||||
|
||||
updateActivations (name) { |
||||
this.removeActive() |
||||
this.addActive(name) |
||||
} |
||||
|
||||
onThemeChanged (themeType) { |
||||
const invert = themeType === 'dark' ? 1 : 0 |
||||
const active = this.view.querySelector('.active') |
||||
if (active) { |
||||
const image = active.querySelector('.image') |
||||
image.style.setProperty('filter', `invert(${invert})`) |
||||
} |
||||
} |
||||
|
||||
async itemContextMenu (e, name, documentation) { |
||||
const actions = {} |
||||
if (await this.appManager.canDeactivatePlugin(profile, { name })) { |
||||
actions.Deactivate = () => { |
||||
// this.call('manager', 'deactivatePlugin', name)
|
||||
this.appManager.deactivatePlugin(name) |
||||
if (e.target.parentElement.classList.contains('active')) { |
||||
this.select('filePanel') |
||||
} |
||||
} |
||||
} |
||||
const links = {} |
||||
if (documentation) { |
||||
links.Documentation = documentation |
||||
} |
||||
if (Object.keys(actions).length || Object.keys(links).length) { |
||||
VERTICALMENU_HANDLE && VERTICALMENU_HANDLE.hide(null, true) |
||||
VERTICALMENU_HANDLE = contextMenu(e, actions, links) |
||||
} |
||||
e.preventDefault() |
||||
e.stopPropagation() |
||||
} |
||||
|
||||
render () { |
||||
const home = yo` |
||||
<div |
||||
class="m-1 mt-2 ${css.homeIcon}" |
||||
onclick="${async () => { |
||||
await this.appManager.activatePlugin('home') |
||||
this.call('tabs', 'focus', 'home') |
||||
}}" |
||||
plugin="home" title="Home" |
||||
data-id="verticalIconsHomeIcon" |
||||
id="verticalIconsHomeIcon" |
||||
> |
||||
${basicLogo()} |
||||
</div> |
||||
` |
||||
this.iconKind.fileexplorer = yo`<div id='fileExplorerIcons' data-id="verticalIconsFileExplorerIcons"></div>` |
||||
this.iconKind.compiler = yo`<div id='compileIcons'></div>` |
||||
this.iconKind.udapp = yo`<div id='runIcons'></div>` |
||||
this.iconKind.testing = yo`<div id='testingIcons'></div>` |
||||
this.iconKind.analysis = yo`<div id='analysisIcons'></div>` |
||||
this.iconKind.debugging = yo`<div id='debuggingIcons' data-id="verticalIconsDebuggingIcons"></div>` |
||||
this.iconKind.none = yo`<div id='otherIcons'></div>` |
||||
this.iconKind.settings = yo`<div id='settingsIcons' data-id="verticalIconsSettingsIcons"></div>` |
||||
|
||||
this.view = yo` |
||||
<div class="h-100"> |
||||
<div class=${css.icons}> |
||||
${home} |
||||
${this.iconKind.fileexplorer} |
||||
${this.iconKind.compiler} |
||||
${this.iconKind.udapp} |
||||
${this.iconKind.testing} |
||||
${this.iconKind.analysis} |
||||
${this.iconKind.debugging} |
||||
${this.iconKind.none} |
||||
${this.iconKind.settings} |
||||
</div> |
||||
</div> |
||||
` |
||||
return this.view |
||||
} |
||||
} |
||||
|
||||
const css = csjs` |
||||
.homeIcon { |
||||
display: block; |
||||
width: 42px; |
||||
height: 42px; |
||||
margin-bottom: 20px; |
||||
cursor: pointer; |
||||
} |
||||
.homeIcon svg path { |
||||
fill: var(--primary); |
||||
} |
||||
.homeIcon svg polygon { |
||||
fill: var(--primary); |
||||
} |
||||
.icons { |
||||
} |
||||
.icon { |
||||
cursor: pointer; |
||||
margin-bottom: 12px; |
||||
width: 36px; |
||||
height: 36px; |
||||
padding: 3px; |
||||
position: relative; |
||||
border-radius: 8px; |
||||
} |
||||
.icon img { |
||||
width: 28px; |
||||
height: 28px; |
||||
padding: 4px; |
||||
filter: invert(0.5); |
||||
} |
||||
.image { |
||||
} |
||||
.icon svg { |
||||
width: 28px; |
||||
height: 28px; |
||||
padding: 4px; |
||||
} |
||||
.icon[title='Settings'] { |
||||
position: absolute; |
||||
bottom: 0; |
||||
} |
||||
.status { |
||||
position: absolute; |
||||
bottom: 0; |
||||
right: 0; |
||||
} |
||||
.statusCheck { |
||||
font-size: 1.2em; |
||||
} |
||||
.statusWithBG |
||||
border-radius: 8px; |
||||
background-color: var(--danger); |
||||
color: var(--light); |
||||
font-size: 12px; |
||||
height: 15px; |
||||
text-align: center; |
||||
font-weight: bold; |
||||
padding-left: 5px; |
||||
padding-right: 5px; |
||||
} |
||||
` |
@ -0,0 +1,116 @@ |
||||
// eslint-disable-next-line no-use-before-define
|
||||
import React from 'react' |
||||
import ReactDOM from 'react-dom' |
||||
import Registry from '../state/registry' |
||||
import packageJson from '../../../../../package.json' |
||||
import { Plugin } from '@remixproject/engine' |
||||
import { EventEmitter } from 'events' |
||||
import { IconRecord, RemixUiVerticalIconsPanel } from '@remix-ui/vertical-icons-panel' |
||||
import { Profile } from '@remixproject/plugin-utils' |
||||
import { timeStamp } from 'console' |
||||
|
||||
const profile = { |
||||
name: 'menuicons', |
||||
displayName: 'Vertical Icons', |
||||
description: '', |
||||
version: packageJson.version, |
||||
methods: ['select', 'unlinkContent', 'linkContent'], |
||||
events: ['toggleContent', 'showContent'] |
||||
} |
||||
|
||||
export class VerticalIcons extends Plugin { |
||||
events: EventEmitter |
||||
htmlElement: HTMLDivElement |
||||
icons: Record<string, IconRecord> = {} |
||||
constructor () { |
||||
super(profile) |
||||
this.events = new EventEmitter() |
||||
this.htmlElement = document.createElement('div') |
||||
this.htmlElement.setAttribute('id', 'icon-panel') |
||||
} |
||||
|
||||
renderComponent () { |
||||
const fixedOrder = ['filePanel', 'solidity','udapp', 'debugger', 'solidityStaticAnalysis', 'solidityUnitTesting', 'pluginManager'] |
||||
|
||||
const divived = Object.values(this.icons).map((value) => { return { |
||||
...value, |
||||
isRequired: fixedOrder.indexOf(value.profile.name) > -1 |
||||
}}).sort((a,b) => { |
||||
return a.timestamp - b.timestamp |
||||
}) |
||||
|
||||
const required = divived.filter((value) => value.isRequired).sort((a,b) => { |
||||
return fixedOrder.indexOf(a.profile.name) - fixedOrder.indexOf(b.profile.name) |
||||
}) |
||||
|
||||
const sorted: IconRecord[] = [ |
||||
...required, |
||||
...divived.filter((value) => { return !value.isRequired }) |
||||
] |
||||
|
||||
ReactDOM.render( |
||||
<RemixUiVerticalIconsPanel |
||||
verticalIconsPlugin={this} |
||||
icons={sorted} |
||||
/>, |
||||
this.htmlElement) |
||||
} |
||||
|
||||
onActivation () { |
||||
this.renderComponent() |
||||
this.on('sidePanel', 'focusChanged', (name: string) => { |
||||
Object.keys(this.icons).map((o) => { |
||||
this.icons[o].active = false |
||||
}) |
||||
this.icons[name].active = true |
||||
this.renderComponent() |
||||
}) |
||||
} |
||||
|
||||
async linkContent (profile: Profile) { |
||||
if (!profile.icon) return |
||||
if (!profile.kind) profile.kind = 'none' |
||||
this.icons[profile.name] = { |
||||
profile: profile, |
||||
active: false, |
||||
canbeDeactivated: await this.call('manager', 'canDeactivate', this.profile, profile), |
||||
timestamp: Date.now() |
||||
} |
||||
this.renderComponent() |
||||
} |
||||
|
||||
unlinkContent (profile: Profile) { |
||||
delete this.icons[profile.name] |
||||
this.renderComponent() |
||||
} |
||||
|
||||
async activateHome() { |
||||
await this.call('manager', 'activatePlugin', 'home') |
||||
await this.call('tabs', 'focus', 'home') |
||||
} |
||||
|
||||
/** |
||||
* Set an icon as active |
||||
* @param {string} name Name of profile of the module to activate |
||||
*/ |
||||
select (name: string) { |
||||
// TODO: Only keep `this.emit` (issue#2210)
|
||||
console.log(name, this) |
||||
this.emit('showContent', name) |
||||
this.events.emit('showContent', name) |
||||
} |
||||
|
||||
/** |
||||
* Toggles the side panel for plugin |
||||
* @param {string} name Name of profile of the module to activate |
||||
*/ |
||||
toggle (name: string) { |
||||
// TODO: Only keep `this.emit` (issue#2210)
|
||||
this.emit('toggleContent', name) |
||||
this.events.emit('toggleContent', name) |
||||
} |
||||
|
||||
render () { |
||||
return this.htmlElement |
||||
} |
||||
} |
@ -1,194 +0,0 @@ |
||||
'use strict' |
||||
import { sourceMappingDecoder } from '@remix-project/remix-debug' |
||||
const yo = require('yo-yo') |
||||
const globalRegistry = require('../../global/registry') |
||||
|
||||
const css = require('./styles/contextView-styles') |
||||
|
||||
/* |
||||
Display information about the current focused code: |
||||
- if it's a reference, display information about the declaration |
||||
- jump to the declaration |
||||
- number of references |
||||
- rename declaration/references |
||||
*/ |
||||
class ContextView { |
||||
constructor (opts, localRegistry) { |
||||
this._components = {} |
||||
this._components.registry = localRegistry || globalRegistry |
||||
this.contextualListener = opts.contextualListener |
||||
this.editor = opts.editor |
||||
this._deps = { |
||||
compilersArtefacts: this._components.registry.get('compilersartefacts').api, |
||||
offsetToLineColumnConverter: this._components.registry.get('offsettolinecolumnconverter').api, |
||||
config: this._components.registry.get('config').api, |
||||
fileManager: this._components.registry.get('filemanager').api |
||||
} |
||||
this._view = null |
||||
this._nodes = null |
||||
this._current = null |
||||
this.sourceMappingDecoder = sourceMappingDecoder |
||||
this.previousElement = null |
||||
this.contextualListener.event.register('contextChanged', nodes => { |
||||
this.show() |
||||
this._nodes = nodes |
||||
this.update() |
||||
}) |
||||
this.contextualListener.event.register('stopHighlighting', () => { |
||||
}) |
||||
} |
||||
|
||||
render () { |
||||
const view = yo` |
||||
<div class="${css.contextview} ${css.contextviewcontainer} bg-light text-dark border-0"> |
||||
<div class=${css.container}> |
||||
${this._renderTarget()} |
||||
</div> |
||||
</div>` |
||||
if (!this._view) { |
||||
this._view = view |
||||
} |
||||
return view |
||||
} |
||||
|
||||
hide () { |
||||
if (this._view) { |
||||
this._view.style.display = 'none' |
||||
} |
||||
} |
||||
|
||||
show () { |
||||
if (this._view) { |
||||
this._view.style.display = 'block' |
||||
} |
||||
} |
||||
|
||||
update () { |
||||
if (this._view) { |
||||
yo.update(this._view, this.render()) |
||||
} |
||||
} |
||||
|
||||
_renderTarget () { |
||||
let last |
||||
const previous = this._current |
||||
if (this._nodes && this._nodes.length) { |
||||
last = this._nodes[this._nodes.length - 1] |
||||
if (isDefinition(last)) { |
||||
this._current = last |
||||
} else { |
||||
const target = this.contextualListener.declarationOf(last) |
||||
if (target) { |
||||
this._current = target |
||||
} else { |
||||
this._current = null |
||||
} |
||||
} |
||||
} |
||||
if (!this._current || !previous || previous.id !== this._current.id || (this.previousElement && !this.previousElement.children.length)) { |
||||
this.previousElement = this._render(this._current, last) |
||||
} |
||||
return this.previousElement |
||||
} |
||||
|
||||
_jumpToInternal (position) { |
||||
const jumpToLine = (lineColumn) => { |
||||
if (lineColumn.start && lineColumn.start.line && lineColumn.start.column) { |
||||
this.editor.gotoLine(lineColumn.start.line, lineColumn.end.column + 1) |
||||
} |
||||
} |
||||
const lastCompilationResult = this._deps.compilersArtefacts.__last |
||||
if (lastCompilationResult && lastCompilationResult.languageversion.indexOf('soljson') === 0 && lastCompilationResult.data) { |
||||
const lineColumn = this._deps.offsetToLineColumnConverter.offsetToLineColumn( |
||||
position, |
||||
position.file, |
||||
lastCompilationResult.getSourceCode().sources, |
||||
lastCompilationResult.getAsts()) |
||||
const filename = lastCompilationResult.getSourceName(position.file) |
||||
// TODO: refactor with rendererAPI.errorClick
|
||||
if (filename !== this._deps.config.get('currentFile')) { |
||||
const provider = this._deps.fileManager.fileProviderOf(filename) |
||||
if (provider) { |
||||
provider.exists(filename).then(exist => { |
||||
this._deps.fileManager.open(filename) |
||||
jumpToLine(lineColumn) |
||||
}).catch(error => { |
||||
if (error) return console.log(error) |
||||
}) |
||||
} |
||||
} else { |
||||
jumpToLine(lineColumn) |
||||
} |
||||
} |
||||
} |
||||
|
||||
_render (node, nodeAtCursorPosition) { |
||||
if (!node) return yo`<div></div>` |
||||
let references = this.contextualListener.referencesOf(node) |
||||
const type = node.typeDescriptions && node.typeDescriptions.typeString ? node.typeDescriptions.typeString : node.nodeType |
||||
references = `${references ? references.length : '0'} reference(s)` |
||||
|
||||
let ref = 0 |
||||
const nodes = this.contextualListener.getActiveHighlights() |
||||
for (const k in nodes) { |
||||
if (nodeAtCursorPosition.id === nodes[k].nodeId) { |
||||
ref = k |
||||
break |
||||
} |
||||
} |
||||
|
||||
// JUMP BETWEEN REFERENCES
|
||||
const jump = (e) => { |
||||
e.target.dataset.action === 'next' ? ref++ : ref-- |
||||
if (ref < 0) ref = nodes.length - 1 |
||||
if (ref >= nodes.length) ref = 0 |
||||
this._jumpToInternal(nodes[ref].position) |
||||
} |
||||
|
||||
const jumpTo = () => { |
||||
if (node && node.src) { |
||||
const position = this.sourceMappingDecoder.decode(node.src) |
||||
if (position) { |
||||
this._jumpToInternal(position) |
||||
} |
||||
} |
||||
} |
||||
|
||||
const showGasEstimation = () => { |
||||
if (node.nodeType === 'FunctionDefinition') { |
||||
const result = this.contextualListener.gasEstimation(node) |
||||
const executionCost = ' Execution cost: ' + result.executionCost + ' gas' |
||||
const codeDepositCost = 'Code deposit cost: ' + result.codeDepositCost + ' gas' |
||||
const estimatedGas = result.codeDepositCost ? `${codeDepositCost}, ${executionCost}` : `${executionCost}` |
||||
return yo` |
||||
<div class=${css.gasEstimation}> |
||||
<i class="fas fa-gas-pump ${css.gasStationIcon}" title='Gas estimation'></i> |
||||
<span>${estimatedGas}</span> |
||||
</div> |
||||
` |
||||
} |
||||
} |
||||
|
||||
return yo` |
||||
<div class=${css.line}>${showGasEstimation()} |
||||
<div title=${type} class=${css.type}>${type}</div> |
||||
<div title=${node.name} class=${css.name}>${node.name}</div> |
||||
<i class="fas fa-share ${css.jump}" aria-hidden="true" onclick=${jumpTo}></i> |
||||
<span class=${css.referencesnb}>${references}</span> |
||||
<i data-action='previous' class="fas fa-chevron-up ${css.jump}" aria-hidden="true" onclick=${jump}></i> |
||||
<i data-action='next' class="fas fa-chevron-down ${css.jump}" aria-hidden="true" onclick=${jump}></i> |
||||
</div> |
||||
` |
||||
} |
||||
} |
||||
|
||||
function isDefinition (node) { |
||||
return node.nodeType === 'ContractDefinition' || |
||||
node.nodeType === 'FunctionDefinition' || |
||||
node.nodeType === 'ModifierDefinition' || |
||||
node.nodeType === 'VariableDeclaration' || |
||||
node.nodeType === 'StructDefinition' || |
||||
node.nodeType === 'EventDefinition' |
||||
} |
||||
|
||||
module.exports = ContextView |
@ -0,0 +1,95 @@ |
||||
import { Plugin } from '@remixproject/engine' |
||||
import { Profile } from '@remixproject/plugin-utils' |
||||
import { EventEmitter } from 'events' |
||||
import QueryParams from '../../lib/query-params' |
||||
|
||||
const profile: Profile = { |
||||
name: 'layout', |
||||
description: 'layout', |
||||
methods: ['minimize'] |
||||
} |
||||
|
||||
interface panelState { |
||||
active: boolean |
||||
plugin: Plugin |
||||
minimized: boolean |
||||
} |
||||
interface panels { |
||||
tabs: panelState |
||||
editor: panelState |
||||
main: panelState |
||||
terminal: panelState |
||||
} |
||||
|
||||
export class Layout extends Plugin { |
||||
event: any |
||||
panels: panels |
||||
constructor () { |
||||
super(profile) |
||||
this.event = new EventEmitter() |
||||
} |
||||
|
||||
async onActivation (): Promise<void> { |
||||
this.on('fileManager', 'currentFileChanged', () => { |
||||
this.panels.editor.active = true |
||||
this.panels.main.active = false |
||||
this.event.emit('change', null) |
||||
}) |
||||
this.on('tabs', 'openFile', () => { |
||||
this.panels.editor.active = true |
||||
this.panels.main.active = false |
||||
this.event.emit('change', null) |
||||
}) |
||||
this.on('tabs', 'switchApp', (name: string) => { |
||||
this.call('mainPanel', 'showContent', name) |
||||
this.panels.editor.active = false |
||||
this.panels.main.active = true |
||||
this.event.emit('change', null) |
||||
}) |
||||
this.on('tabs', 'closeApp', (name: string) => { |
||||
this.panels.editor.active = true |
||||
this.panels.main.active = false |
||||
this.event.emit('change', null) |
||||
}) |
||||
this.on('tabs', 'tabCountChanged', async count => { |
||||
if (!count) await this.call('manager', 'activatePlugin', 'home') |
||||
}) |
||||
this.on('manager', 'activate', (profile: Profile) => { |
||||
switch (profile.name) { |
||||
case 'filePanel': |
||||
this.call('menuicons', 'select', 'filePanel') |
||||
break |
||||
} |
||||
}) |
||||
document.addEventListener('keypress', e => { |
||||
if (e.shiftKey && e.ctrlKey) { |
||||
if (e.code === 'KeyF') { |
||||
// Ctrl+Shift+F
|
||||
this.call('menuicons', 'select', 'filePanel') |
||||
} else if (e.code === 'KeyA') { |
||||
// Ctrl+Shift+A
|
||||
this.call('menuicons', 'select', 'pluginManager') |
||||
} else if (e.code === 'KeyS') { |
||||
// Ctrl+Shift+S
|
||||
this.call('menuicons', 'select', 'settings') |
||||
} |
||||
e.preventDefault() |
||||
} |
||||
}) |
||||
const queryParams = new QueryParams() |
||||
const params = queryParams.get() |
||||
if (params.minimizeterminal || params.embed) { |
||||
this.panels.terminal.minimized = true |
||||
this.event.emit('change', this.panels) |
||||
this.emit('change', this.panels) |
||||
} |
||||
if (params.minimizesidepanel || params.embed) { |
||||
this.event.emit('minimizesidepanel') |
||||
} |
||||
} |
||||
|
||||
minimize (name: string, minimized:boolean): void { |
||||
this.panels[name].minimized = minimized |
||||
this.event.emit('change', null) |
||||
} |
||||
} |
@ -1,211 +0,0 @@ |
||||
var yo = require('yo-yo') |
||||
var EventManager = require('../../lib/events') |
||||
|
||||
var globalRegistry = require('../../global/registry') |
||||
var { TabProxy } = require('./tab-proxy.js') |
||||
|
||||
var ContextView = require('../editor/contextView') |
||||
|
||||
var csjs = require('csjs-inject') |
||||
|
||||
var css = csjs` |
||||
.mainview { |
||||
display : flex; |
||||
flex-direction : column; |
||||
height : 100%; |
||||
width : 100%; |
||||
} |
||||
` |
||||
|
||||
// @todo(#650) Extract this into two classes: MainPanel (TabsProxy + Iframe/Editor) & BottomPanel (Terminal)
|
||||
export class MainView { |
||||
constructor (contextualListener, editor, mainPanel, fileManager, appManager, terminal) { |
||||
var self = this |
||||
self.event = new EventManager() |
||||
self._view = {} |
||||
self._components = {} |
||||
self._components.registry = globalRegistry |
||||
self.editor = editor |
||||
self.fileManager = fileManager |
||||
self.mainPanel = mainPanel |
||||
self.txListener = globalRegistry.get('txlistener').api |
||||
self._components.terminal = terminal |
||||
self._components.contextualListener = contextualListener |
||||
this.appManager = appManager |
||||
this.init() |
||||
} |
||||
|
||||
async showApp (name) { |
||||
await this.fileManager.unselectCurrentFile() |
||||
this.mainPanel.showContent(name) |
||||
this._view.editor.style.display = 'none' |
||||
this._components.contextView.hide() |
||||
this._view.mainPanel.style.display = 'block' |
||||
} |
||||
|
||||
getAppPanel () { |
||||
return this.mainPanel |
||||
} |
||||
|
||||
init () { |
||||
var self = this |
||||
self._deps = { |
||||
config: self._components.registry.get('config').api, |
||||
fileManager: self._components.registry.get('filemanager').api |
||||
} |
||||
|
||||
self.tabProxy = new TabProxy(self.fileManager, self.editor) |
||||
/* |
||||
We listen here on event from the tab component to display / hide the editor and mainpanel |
||||
depending on the content that should be displayed |
||||
*/ |
||||
self.fileManager.events.on('currentFileChanged', (file) => { |
||||
// we check upstream for "fileChanged"
|
||||
self._view.editor.style.display = 'block' |
||||
self._view.mainPanel.style.display = 'none' |
||||
self._components.contextView.show() |
||||
}) |
||||
self.tabProxy.event.on('openFile', (file) => { |
||||
self._view.editor.style.display = 'block' |
||||
self._view.mainPanel.style.display = 'none' |
||||
self._components.contextView.show() |
||||
}) |
||||
self.tabProxy.event.on('closeFile', (file) => { |
||||
}) |
||||
self.tabProxy.event.on('switchApp', self.showApp.bind(self)) |
||||
self.tabProxy.event.on('closeApp', (name) => { |
||||
self._view.editor.style.display = 'block' |
||||
self._components.contextView.show() |
||||
self._view.mainPanel.style.display = 'none' |
||||
}) |
||||
self.tabProxy.event.on('tabCountChanged', (count) => { |
||||
if (!count) this.editor.displayEmptyReadOnlySession() |
||||
}) |
||||
self.data = { |
||||
_layout: { |
||||
top: { |
||||
offset: self._terminalTopOffset(), |
||||
show: true |
||||
} |
||||
} |
||||
} |
||||
|
||||
const contextView = new ContextView({ contextualListener: self._components.contextualListener, editor: self.editor }) |
||||
|
||||
self._components.contextView = contextView |
||||
|
||||
self._components.terminal.event.register('resize', delta => self._adjustLayout('top', delta)) |
||||
if (self.txListener) { |
||||
self._components.terminal.event.register('listenOnNetWork', (listenOnNetWork) => { |
||||
self.txListener.setListenOnNetwork(listenOnNetWork) |
||||
}) |
||||
} |
||||
} |
||||
|
||||
_terminalTopOffset () { |
||||
return this._deps.config.get('terminal-top-offset') || 150 |
||||
} |
||||
|
||||
_adjustLayout (direction, delta) { |
||||
var limitUp = 0 |
||||
var limitDown = 32 |
||||
var containerHeight = window.innerHeight - limitUp // - menu bar containerHeight
|
||||
var self = this |
||||
var layout = self.data._layout[direction] |
||||
if (layout) { |
||||
if (delta === undefined) { |
||||
layout.show = !layout.show |
||||
if (layout.show) delta = layout.offset |
||||
else delta = 0 |
||||
} else { |
||||
layout.show = true |
||||
self._deps.config.set(`terminal-${direction}-offset`, delta) |
||||
layout.offset = delta |
||||
} |
||||
} |
||||
var tmp = delta - limitDown |
||||
delta = tmp > 0 ? tmp : 0 |
||||
if (direction === 'top') { |
||||
var mainPanelHeight = containerHeight - delta |
||||
mainPanelHeight = mainPanelHeight < 0 ? 0 : mainPanelHeight |
||||
self._view.editor.style.height = `${mainPanelHeight}px` |
||||
self._view.mainPanel.style.height = `${mainPanelHeight}px` |
||||
self._view.terminal.style.height = `${delta}px` // - menu bar height
|
||||
self.editor.resize((document.querySelector('#editorWrap') || {}).checked) |
||||
self._components.terminal.scroll2bottom() |
||||
} |
||||
} |
||||
|
||||
minimizeTerminal () { |
||||
this._adjustLayout('top') |
||||
} |
||||
|
||||
showTerminal (offset) { |
||||
this._adjustLayout('top', offset || this._terminalTopOffset()) |
||||
} |
||||
|
||||
getTerminal () { |
||||
return this._components.terminal |
||||
} |
||||
|
||||
getEditor () { |
||||
var self = this |
||||
return self.editor |
||||
} |
||||
|
||||
refresh () { |
||||
var self = this |
||||
self._view.tabs.onmouseenter() |
||||
} |
||||
|
||||
log (data = {}) { |
||||
var self = this |
||||
var command = self._components.terminal.commands[data.type] |
||||
if (typeof command === 'function') command(data.value) |
||||
} |
||||
|
||||
logMessage (msg) { |
||||
var self = this |
||||
self.log({ type: 'log', value: msg }) |
||||
} |
||||
|
||||
logHtmlMessage (msg) { |
||||
var self = this |
||||
self.log({ type: 'html', value: msg }) |
||||
} |
||||
|
||||
render () { |
||||
var self = this |
||||
if (self._view.mainview) return self._view.mainview |
||||
self._view.editor = self.editor.render() |
||||
self._view.editor.style.display = 'none' |
||||
self._view.mainPanel = self.mainPanel.render() |
||||
self._view.terminal = self._components.terminal.render() |
||||
self._view.mainview = yo` |
||||
<div class=${css.mainview}> |
||||
${self.tabProxy.renderTabsbar()} |
||||
${self._view.editor} |
||||
${self._view.mainPanel} |
||||
${self._components.contextView.render()} |
||||
${self._view.terminal} |
||||
</div> |
||||
` |
||||
// INIT
|
||||
self._adjustLayout('top', self.data._layout.top.offset) |
||||
|
||||
document.addEventListener('keydown', (e) => { |
||||
if (e.altKey && e.keyCode === 84) self.tabProxy.switchNextTab() // alt + t
|
||||
}) |
||||
|
||||
return self._view.mainview |
||||
} |
||||
|
||||
registerCommand (name, command, opts) { |
||||
var self = this |
||||
return self._components.terminal.registerCommand(name, command, opts) |
||||
} |
||||
|
||||
updateTerminalFilter (filter) { |
||||
this._components.terminal.updateJournal(filter) |
||||
} |
||||
} |
@ -0,0 +1,31 @@ |
||||
import { Plugin } from '@remixproject/engine' |
||||
import QueryParams from '../../lib/query-params' |
||||
import Registry from '../state/registry' |
||||
|
||||
const profile = { |
||||
name: 'config', |
||||
displayName: 'Config', |
||||
description: 'Config', |
||||
methods: ['getAppParameter', 'setAppParameter'] |
||||
} |
||||
|
||||
export class ConfigPlugin extends Plugin { |
||||
constructor () { |
||||
super(profile) |
||||
} |
||||
|
||||
getAppParameter (name: string) { |
||||
const queryParams = new QueryParams() |
||||
const params = queryParams.get() |
||||
const config = Registry.getInstance().get('config').api |
||||
const param = params[name] ? params[name] : config.get(name) |
||||
if (param === 'true') return true |
||||
if (param === 'false') return false |
||||
return param |
||||
} |
||||
|
||||
setAppParameter (name: string, value: any) { |
||||
const config = Registry.getInstance().get('config').api |
||||
config.set(name, value) |
||||
} |
||||
} |
@ -0,0 +1,44 @@ |
||||
import { Plugin } from '@remixproject/engine' |
||||
import { LibraryProfile, MethodApi, StatusEvents } from '@remixproject/plugin-utils' |
||||
import { AppModal } from '@remix-ui/app' |
||||
import { AlertModal } from 'libs/remix-ui/app/src/lib/remix-app/interface' |
||||
import { dispatchModalInterface } from 'libs/remix-ui/app/src/lib/remix-app/context/context' |
||||
|
||||
interface INotificationApi { |
||||
events: StatusEvents, |
||||
methods: { |
||||
modal: (args: AppModal) => void |
||||
alert: (args: AlertModal) => void |
||||
toast: (message: string) => void |
||||
} |
||||
} |
||||
|
||||
const profile:LibraryProfile<INotificationApi> = { |
||||
name: 'notification', |
||||
displayName: 'Notification', |
||||
description: 'Displays notifications', |
||||
methods: ['modal', 'alert', 'toast'] |
||||
} |
||||
|
||||
export class NotificationPlugin extends Plugin implements MethodApi<INotificationApi> { |
||||
dispatcher: dispatchModalInterface |
||||
constructor () { |
||||
super(profile) |
||||
} |
||||
|
||||
setDispatcher (dispatcher: dispatchModalInterface) { |
||||
this.dispatcher = dispatcher |
||||
} |
||||
|
||||
async modal (args: AppModal) { |
||||
return this.dispatcher.modal(args) |
||||
} |
||||
|
||||
async alert (args: AlertModal) { |
||||
return this.dispatcher.alert(args) |
||||
} |
||||
|
||||
async toast (message: string | JSX.Element) { |
||||
this.dispatcher.toast(message) |
||||
} |
||||
} |
@ -0,0 +1,126 @@ |
||||
import React from 'react' // eslint-disable-line
|
||||
import { Plugin } from '@remixproject/engine' |
||||
import { AppModal } from 'libs/remix-ui/app/src' |
||||
import { PermissionHandlerDialog, PermissionHandlerValue } from 'libs/remix-ui/permission-handler/src' |
||||
import { Profile } from '@remixproject/plugin-utils' |
||||
|
||||
const profile = { |
||||
name: 'permissionhandler', |
||||
displayName: 'permissionhandler', |
||||
description: 'permissionhandler', |
||||
methods: ['askPermission'] |
||||
} |
||||
|
||||
export class PermissionHandlerPlugin extends Plugin { |
||||
permissions: any |
||||
currentVersion: number |
||||
constructor() { |
||||
super(profile) |
||||
this.permissions = this._getFromLocal() |
||||
this.currentVersion = 1 |
||||
// here we remove the old permissions saved before adding 'permissionVersion'
|
||||
// since with v1 the structure has been changed because of new engine ^0.2.0-alpha.6 changes
|
||||
if (!localStorage.getItem('permissionVersion')) { |
||||
localStorage.setItem('plugins/permissions', '') |
||||
localStorage.setItem('permissionVersion', this.currentVersion.toString()) |
||||
} |
||||
} |
||||
|
||||
_getFromLocal() { |
||||
const permission = localStorage.getItem('plugins/permissions') |
||||
return permission ? JSON.parse(permission) : {} |
||||
} |
||||
|
||||
persistPermissions() { |
||||
const permissions = JSON.stringify(this.permissions) |
||||
localStorage.setItem('plugins/permissions', permissions) |
||||
} |
||||
|
||||
switchMode (from: Profile, to: Profile, method: string, set: boolean) { |
||||
set |
||||
? this.permissions[to.name][method][from.name] = {} |
||||
: delete this.permissions[to.name][method][from.name] |
||||
} |
||||
|
||||
clear() { |
||||
localStorage.removeItem('plugins/permissions') |
||||
} |
||||
|
||||
notAllowWarning(from: Profile, to: Profile, method: string) { |
||||
return `${from.displayName || from.name} is not allowed to call ${method} method of ${to.displayName || to.name}.` |
||||
} |
||||
|
||||
async getTheme() { |
||||
return (await this.call('theme', 'currentTheme')).quality |
||||
} |
||||
|
||||
/** |
||||
* Check if a plugin has the permission to call another plugin and askPermission if needed |
||||
* @param {PluginProfile} from the profile of the plugin that make the call |
||||
* @param {ModuleProfile} to The profile of the module that receive the call |
||||
* @param {string} method The name of the function to be called |
||||
* @param {string} message from the caller plugin to add more details if needed |
||||
* @returns {Promise<boolean>} |
||||
*/ |
||||
async askPermission(from: Profile, to: Profile, method: string, message: string) { |
||||
try { |
||||
this.permissions = this._getFromLocal() |
||||
if (!this.permissions[to.name]) this.permissions[to.name] = {} |
||||
if (!this.permissions[to.name][method]) this.permissions[to.name][method] = {} |
||||
if (!this.permissions[to.name][method][from.name]) return this.openPermission(from, to, method, message) |
||||
|
||||
const { allow, hash } = this.permissions[to.name][method][from.name] |
||||
if (!allow) { |
||||
const warning = this.notAllowWarning(from, to, method) |
||||
this.call('notification', 'toast', warning) |
||||
return false |
||||
} |
||||
return hash === from.hash |
||||
? true // Allow
|
||||
: await this.openPermission(from, to, method, message) |
||||
} catch (err) { |
||||
throw new Error(err) |
||||
} |
||||
} |
||||
|
||||
async openPermission(from: Profile, to: Profile, method: string, message: string) { |
||||
const remember = this.permissions[to.name][method][from.name] |
||||
const value: PermissionHandlerValue = { |
||||
from, |
||||
to, |
||||
method, |
||||
message, |
||||
remember |
||||
} |
||||
const modal: AppModal = { |
||||
id: 'PermissionHandler', |
||||
title: `Permission needed for ${to.displayName || to.name}`, |
||||
message: <PermissionHandlerDialog plugin={this} theme={await this.getTheme()} value={value}></PermissionHandlerDialog>, |
||||
okLabel: 'Accept', |
||||
cancelLabel: 'Decline' |
||||
} |
||||
|
||||
const result = await this.call('notification', 'modal', modal) |
||||
return new Promise((resolve, reject) => { |
||||
if (result) { |
||||
if (this.permissions[to.name][method][from.name]) { |
||||
this.permissions[to.name][method][from.name] = { |
||||
allow: true, |
||||
hash: from.hash |
||||
} |
||||
this.persistPermissions() |
||||
} |
||||
resolve(true) |
||||
} else { |
||||
if (this.permissions[to.name][method][from.name]) { |
||||
this.permissions[to.name][method][from.name] = { |
||||
allow: false, |
||||
hash: from.hash |
||||
} |
||||
this.persistPermissions() |
||||
} |
||||
reject(this.notAllowWarning(from, to, method)) |
||||
} |
||||
}) |
||||
} |
||||
} |
@ -0,0 +1,38 @@ |
||||
type registryEntry = { |
||||
api: any, |
||||
name: string |
||||
} |
||||
|
||||
export default class Registry { |
||||
private static instance: Registry; |
||||
private state: any |
||||
|
||||
private constructor () { |
||||
this.state = {} |
||||
} |
||||
|
||||
public static getInstance (): Registry { |
||||
if (!Registry.instance) { |
||||
Registry.instance = new Registry() |
||||
} |
||||
|
||||
return Registry.instance |
||||
} |
||||
|
||||
public put (entry: registryEntry) { |
||||
if (this.state[entry.name]) return this.state[entry.name] |
||||
const server = { |
||||
// uid: serveruid,
|
||||
api: entry.api |
||||
} |
||||
this.state[entry.name] = { server } |
||||
return server |
||||
} |
||||
|
||||
public get (name: string) { |
||||
const state = this.state[name] |
||||
if (!state) return |
||||
const server = state.server |
||||
return server |
||||
} |
||||
} |
@ -1,82 +0,0 @@ |
||||
import * as packageJson from '../../../../../package.json' |
||||
import { Plugin } from '@remixproject/engine' |
||||
import Web3 from 'web3' |
||||
const yo = require('yo-yo') |
||||
const modalDialogCustom = require('../ui/modal-dialog-custom') |
||||
|
||||
const profile = { |
||||
name: 'hardhat-provider', |
||||
displayName: 'Hardhat Provider', |
||||
kind: 'provider', |
||||
description: 'Hardhat provider', |
||||
methods: ['sendAsync'], |
||||
version: packageJson.version |
||||
} |
||||
|
||||
export default class HardhatProvider extends Plugin { |
||||
constructor (blockchain) { |
||||
super(profile) |
||||
this.provider = null |
||||
this.blocked = false // used to block any call when trying to recover after a failed connection.
|
||||
this.blockchain = blockchain |
||||
} |
||||
|
||||
onDeactivation () { |
||||
this.provider = null |
||||
this.blocked = false |
||||
} |
||||
|
||||
hardhatProviderDialogBody () { |
||||
return yo` |
||||
<div class=""> |
||||
Note: To run Hardhat network node on your system, go to hardhat project folder and run command: |
||||
<div class="border p-1">npx hardhat node</div> |
||||
<br> |
||||
For more info, visit: <a href="https://hardhat.org/getting-started/#connecting-a-wallet-or-dapp-to-hardhat-network" target="_blank">Hardhat Documentation</a> |
||||
<br><br> |
||||
Hardhat JSON-RPC Endpoint |
||||
</div> |
||||
` |
||||
} |
||||
|
||||
sendAsync (data) { |
||||
return new Promise((resolve, reject) => { |
||||
if (this.blocked) return reject(new Error('provider unable to connect')) |
||||
// If provider is not set, allow to open modal only when provider is trying to connect
|
||||
if (!this.provider) { |
||||
modalDialogCustom.prompt('Hardhat node request', this.hardhatProviderDialogBody(), 'http://127.0.0.1:8545', (target) => { |
||||
this.provider = new Web3.providers.HttpProvider(target) |
||||
this.sendAsyncInternal(data, resolve, reject) |
||||
}, () => { |
||||
this.sendAsyncInternal(data, resolve, reject) |
||||
}) |
||||
} else { |
||||
this.sendAsyncInternal(data, resolve, reject) |
||||
} |
||||
}) |
||||
} |
||||
|
||||
sendAsyncInternal (data, resolve, reject) { |
||||
if (this.provider) { |
||||
// Check the case where current environment is VM on UI and it still sends RPC requests
|
||||
// This will be displayed on UI tooltip as 'cannot get account list: Environment Updated !!'
|
||||
if (this.blockchain.getProvider() !== 'Hardhat Provider' && data.method !== 'net_listening') return reject(new Error('Environment Updated !!')) |
||||
this.provider[this.provider.sendAsync ? 'sendAsync' : 'send'](data, async (error, message) => { |
||||
if (error) { |
||||
this.blocked = true |
||||
modalDialogCustom.alert('Hardhat Provider', `Error while connecting to the hardhat provider: ${error.message}`) |
||||
await this.call('udapp', 'setEnvironmentMode', { context: 'vm', fork: 'london' }) |
||||
this.provider = null |
||||
setTimeout(_ => { this.blocked = false }, 1000) // we wait 1 second for letting remix to switch to vm
|
||||
return reject(error) |
||||
} |
||||
resolve(message) |
||||
}) |
||||
} else { |
||||
const result = data.method === 'net_listening' ? 'canceled' : [] |
||||
resolve({ jsonrpc: '2.0', result: result, id: data.id }) |
||||
} |
||||
} |
||||
} |
||||
|
||||
module.exports = HardhatProvider |
@ -0,0 +1,128 @@ |
||||
import * as packageJson from '../../../../../package.json' |
||||
import { Plugin } from '@remixproject/engine' |
||||
import { AppModal, AlertModal, ModalTypes } from '@remix-ui/app' |
||||
import React from 'react' // eslint-disable-line
|
||||
import { Blockchain } from '../../blockchain/blockchain' |
||||
import { ethers } from 'ethers' |
||||
|
||||
const profile = { |
||||
name: 'hardhat-provider', |
||||
displayName: 'Hardhat Provider', |
||||
kind: 'provider', |
||||
description: 'Hardhat provider', |
||||
methods: ['sendAsync'], |
||||
version: packageJson.version |
||||
} |
||||
|
||||
type JsonDataRequest = { |
||||
id: number, |
||||
jsonrpc: string // version
|
||||
method: string, |
||||
params: Array<any>, |
||||
} |
||||
|
||||
type JsonDataResult = { |
||||
id: number, |
||||
jsonrpc: string // version
|
||||
result: any |
||||
} |
||||
|
||||
type RejectRequest = (error: Error) => void |
||||
type SuccessRequest = (data: JsonDataResult) => void |
||||
|
||||
export class HardhatProvider extends Plugin { |
||||
provider: ethers.providers.JsonRpcProvider |
||||
blocked: boolean |
||||
blockchain: Blockchain |
||||
target: String |
||||
|
||||
constructor (blockchain) { |
||||
super(profile) |
||||
this.provider = null |
||||
this.blocked = false // used to block any call when trying to recover after a failed connection.
|
||||
this.blockchain = blockchain |
||||
} |
||||
|
||||
onDeactivation () { |
||||
this.provider = null |
||||
this.blocked = false |
||||
} |
||||
|
||||
hardhatProviderDialogBody (): JSX.Element { |
||||
return (<div> Note: To run Hardhat network node on your system, go to hardhat project folder and run command: |
||||
<div className="border p-1">npx hardhat node</div>
|
||||
For more info, visit: <a href="https://hardhat.org/getting-started/#connecting-a-wallet-or-dapp-to-hardhat-network" target="_blank">Hardhat Documentation</a>
|
||||
Hardhat JSON-RPC Endpoint |
||||
</div>) |
||||
} |
||||
|
||||
sendAsync (data: JsonDataRequest): Promise<any> { |
||||
return new Promise(async (resolve, reject) => { |
||||
if (this.blocked) return reject(new Error('provider unable to connect')) |
||||
// If provider is not set, allow to open modal only when provider is trying to connect
|
||||
if (!this.provider) { |
||||
let value: string |
||||
try { |
||||
value = await ((): Promise<string> => { |
||||
return new Promise((resolve, reject) => { |
||||
const modalContent: AppModal = { |
||||
id: 'hardhatprovider', |
||||
title: 'Hardhat node request', |
||||
message: this.hardhatProviderDialogBody(), |
||||
modalType: ModalTypes.prompt, |
||||
okLabel: 'OK', |
||||
cancelLabel: 'Cancel', |
||||
okFn: (value: string) => { |
||||
setTimeout(() => resolve(value), 0) |
||||
}, |
||||
cancelFn: () => { |
||||
setTimeout(() => reject(new Error('Canceled')), 0) |
||||
}, |
||||
hideFn: () => { |
||||
setTimeout(() => reject(new Error('Hide')), 0) |
||||
}, |
||||
defaultValue: 'http://127.0.0.1:8545' |
||||
} |
||||
this.call('notification', 'modal', modalContent) |
||||
}) |
||||
})() |
||||
} catch (e) { |
||||
// the modal has been canceled/hide
|
||||
return |
||||
}
|
||||
this.provider = new ethers.providers.JsonRpcProvider(value) |
||||
this.sendAsyncInternal(data, resolve, reject)
|
||||
} else { |
||||
this.sendAsyncInternal(data, resolve, reject) |
||||
} |
||||
}) |
||||
} |
||||
|
||||
private async sendAsyncInternal (data: JsonDataRequest, resolve: SuccessRequest, reject: RejectRequest): Promise<void> { |
||||
if (this.provider) { |
||||
// Check the case where current environment is VM on UI and it still sends RPC requests
|
||||
// This will be displayed on UI tooltip as 'cannot get account list: Environment Updated !!'
|
||||
if (this.blockchain.getProvider() !== 'Hardhat Provider' && data.method !== 'net_listening') return reject(new Error('Environment Updated !!')) |
||||
|
||||
try { |
||||
const result = await this.provider.send(data.method, data.params) |
||||
resolve({ jsonrpc: '2.0', result, id: data.id }) |
||||
} catch (error) { |
||||
this.blocked = true |
||||
const modalContent: AlertModal = { |
||||
id: 'hardhatprovider', |
||||
title: 'Hardhat Provider', |
||||
message: `Error while connecting to the hardhat provider: ${error.message}`, |
||||
} |
||||
this.call('notification', 'alert', modalContent) |
||||
await this.call('udapp', 'setEnvironmentMode', { context: 'vm', fork: 'london' }) |
||||
this.provider = null |
||||
setTimeout(_ => { this.blocked = false }, 1000) // we wait 1 second for letting remix to switch to vm
|
||||
reject(error) |
||||
} |
||||
} else { |
||||
const result = data.method === 'net_listening' ? 'canceled' : [] |
||||
resolve({ jsonrpc: '2.0', result: result, id: data.id }) |
||||
} |
||||
} |
||||
} |
@ -1,22 +0,0 @@ |
||||
var yo = require('yo-yo') |
||||
var css = require('./styles/plugin-tab-styles') |
||||
|
||||
class PluginTab { |
||||
constructor (json) { |
||||
this.el = null |
||||
this.data = { json } |
||||
} |
||||
|
||||
render () { |
||||
if (this.el) return this.el |
||||
|
||||
this.el = yo` |
||||
<div class="${css.pluginTabView}" id="pluginView"> |
||||
<iframe class="${css.iframe}" src="${this.data.json.url}/index.html"></iframe> |
||||
</div>` |
||||
|
||||
return this.el |
||||
} |
||||
} |
||||
|
||||
module.exports = PluginTab |
@ -1,423 +0,0 @@ |
||||
import publishToStorage from '../../../publishToStorage' |
||||
const yo = require('yo-yo') |
||||
const ethJSUtil = require('ethereumjs-util') |
||||
const css = require('../styles/run-tab-styles') |
||||
const modalDialogCustom = require('../../ui/modal-dialog-custom') |
||||
const remixLib = require('@remix-project/remix-lib') |
||||
const EventManager = remixLib.EventManager |
||||
const confirmDialog = require('../../ui/confirmDialog') |
||||
const modalDialog = require('../../ui/modaldialog') |
||||
const MultiParamManager = require('../../ui/multiParamManager') |
||||
const helper = require('../../../lib/helper') |
||||
const addTooltip = require('../../ui/tooltip') |
||||
const _paq = window._paq = window._paq || [] |
||||
|
||||
class ContractDropdownUI { |
||||
constructor (blockchain, dropdownLogic, logCallback, runView) { |
||||
this.blockchain = blockchain |
||||
this.dropdownLogic = dropdownLogic |
||||
this.logCallback = logCallback |
||||
this.runView = runView |
||||
this.event = new EventManager() |
||||
|
||||
this.listenToEvents() |
||||
this.ipfsCheckedState = false |
||||
this.exEnvironment = blockchain.getProvider() |
||||
this.listenToContextChange() |
||||
this.loadType = 'other' |
||||
} |
||||
|
||||
listenToEvents () { |
||||
this.dropdownLogic.event.register('newlyCompiled', (success, data, source, compiler, compilerFullName, file) => { |
||||
if (!this.selectContractNames) return |
||||
this.selectContractNames.innerHTML = '' |
||||
if (success) { |
||||
this.dropdownLogic.getCompiledContracts(compiler, compilerFullName).forEach((contract) => { |
||||
this.selectContractNames.appendChild(yo`<option value="${contract.name}" compiler="${compilerFullName}">${contract.name} - ${contract.file}</option>`) |
||||
}) |
||||
} |
||||
this.enableAtAddress(success) |
||||
this.enableContractNames(success) |
||||
this.setInputParamsPlaceHolder() |
||||
|
||||
if (success) { |
||||
this.compFails.style.display = 'none' |
||||
} else { |
||||
this.compFails.style.display = 'block' |
||||
} |
||||
}) |
||||
} |
||||
|
||||
listenToContextChange () { |
||||
this.blockchain.event.register('networkStatus', ({ error, network }) => { |
||||
if (error) { |
||||
console.log('can\'t detect network') |
||||
return |
||||
} |
||||
this.exEnvironment = this.blockchain.getProvider() |
||||
this.networkName = network.name |
||||
this.networkId = network.id |
||||
|
||||
const savedConfig = window.localStorage.getItem(`ipfs/${this.exEnvironment}/${this.networkName}`) |
||||
|
||||
// check if an already selected option exist else use default workflow
|
||||
if (savedConfig !== null) { |
||||
this.setCheckedState(savedConfig) |
||||
} else { |
||||
this.setCheckedState(this.networkName === 'Main') |
||||
} |
||||
}) |
||||
} |
||||
|
||||
setCheckedState (value) { |
||||
value = value === 'true' ? true : value === 'false' ? false : value |
||||
this.ipfsCheckedState = value |
||||
if (this.ipfsCheckbox) this.ipfsCheckbox.checked = value |
||||
} |
||||
|
||||
toggleCheckedState () { |
||||
if (this.exEnvironment === 'vm') this.networkName = 'VM' |
||||
this.ipfsCheckedState = !this.ipfsCheckedState |
||||
window.localStorage.setItem(`ipfs/${this.exEnvironment}/${this.networkName}`, this.ipfsCheckedState) |
||||
} |
||||
|
||||
enableContractNames (enable) { |
||||
if (enable) { |
||||
if (this.selectContractNames.value === '') return |
||||
this.selectContractNames.removeAttribute('disabled') |
||||
this.selectContractNames.setAttribute('title', 'Select contract for Deploy or At Address.') |
||||
} else { |
||||
this.selectContractNames.setAttribute('disabled', true) |
||||
if (this.loadType === 'sol') { |
||||
this.selectContractNames.setAttribute('title', '⚠ Select and compile *.sol file to deploy or access a contract.') |
||||
} else { |
||||
this.selectContractNames.setAttribute('title', '⚠ Selected *.abi file allows accessing contracts, select and compile *.sol file to deploy and access one.') |
||||
} |
||||
} |
||||
} |
||||
|
||||
enableAtAddress (enable) { |
||||
if (enable) { |
||||
const address = this.atAddressButtonInput.value |
||||
if (!address || !ethJSUtil.isValidAddress(address)) { |
||||
this.enableAtAddress(false) |
||||
return |
||||
} |
||||
this.atAddress.removeAttribute('disabled') |
||||
this.atAddress.setAttribute('title', 'Interact with the given contract.') |
||||
} else { |
||||
this.atAddress.setAttribute('disabled', true) |
||||
if (this.atAddressButtonInput.value === '') { |
||||
this.atAddress.setAttribute('title', '⚠ Compile *.sol file or select *.abi file & then enter the address of deployed contract.') |
||||
} else { |
||||
this.atAddress.setAttribute('title', '⚠ Compile *.sol file or select *.abi file.') |
||||
} |
||||
} |
||||
} |
||||
|
||||
render () { |
||||
this.compFails = yo`<i title="No contract compiled yet or compilation failed. Please check the compile tab for more information." class="m-2 ml-3 fas fa-times-circle ${css.errorIcon}" ></i>` |
||||
this.atAddress = yo`<button class="${css.atAddress} btn btn-sm btn-info" id="runAndDeployAtAdressButton" onclick=${this.loadFromAddress.bind(this)}>At Address</button>` |
||||
this.atAddressButtonInput = yo`<input class="${css.input} ${css.ataddressinput} ataddressinput form-control" placeholder="Load contract from Address" title="address of contract" oninput=${this.atAddressChanged.bind(this)} />` |
||||
this.selectContractNames = yo`<select class="${css.contractNames} custom-select" disabled title="Please compile *.sol file to deploy or access a contract"></select>` |
||||
this.abiLabel = yo`<span class="py-1">ABI file selected</span>` |
||||
if (this.exEnvironment === 'vm') this.networkName = 'VM' |
||||
this.enableAtAddress(false) |
||||
this.abiLabel.style.display = 'none' |
||||
|
||||
const savedConfig = window.localStorage.getItem(`ipfs/${this.exEnvironment}/${this.networkName}`) |
||||
this.ipfsCheckedState = savedConfig === 'true' ? true : false // eslint-disable-line
|
||||
|
||||
this.ipfsCheckbox = yo` |
||||
<input |
||||
id="deployAndRunPublishToIPFS" |
||||
data-id="contractDropdownIpfsCheckbox" |
||||
class="form-check-input custom-control-input" |
||||
type="checkbox" |
||||
onchange=${() => this.toggleCheckedState()} |
||||
> |
||||
` |
||||
if (this.ipfsCheckedState) this.ipfsCheckbox.checked = true |
||||
|
||||
this.deployCheckBox = yo` |
||||
<div class="d-flex py-1 align-items-center custom-control custom-checkbox"> |
||||
${this.ipfsCheckbox} |
||||
<label |
||||
for="deployAndRunPublishToIPFS" |
||||
data-id="contractDropdownIpfsCheckboxLabel" |
||||
class="m-0 form-check-label custom-control-label ${css.checkboxAlign}" |
||||
title="Publishing the source code and metadata to IPFS facilitates source code verification using Sourcify and will greatly foster contract adoption (auditing, debugging, calling it, etc...)" |
||||
> |
||||
Publish to IPFS |
||||
</label> |
||||
</div> |
||||
` |
||||
this.createPanel = yo`<div class="${css.deployDropdown}"></div>` |
||||
this.orLabel = yo`<div class="${css.orLabel} mt-2">or</div>` |
||||
|
||||
const contractNamesContainer = yo` |
||||
<div class="${css.container}" data-id="contractDropdownContainer"> |
||||
<label class="${css.settingsLabel}">Contract</label> |
||||
<div class="${css.subcontainer}"> |
||||
${this.selectContractNames} ${this.compFails} |
||||
${this.abiLabel} |
||||
</div> |
||||
<div> |
||||
${this.createPanel} |
||||
${this.orLabel} |
||||
<div class="${css.button} ${css.atAddressSect}"> |
||||
${this.atAddress} |
||||
${this.atAddressButtonInput} |
||||
</div> |
||||
</div> |
||||
</div> |
||||
` |
||||
this.selectContractNames.addEventListener('change', this.setInputParamsPlaceHolder.bind(this)) |
||||
this.setInputParamsPlaceHolder() |
||||
if (!this.contractNamesContainer) { |
||||
this.contractNamesContainer = contractNamesContainer |
||||
} |
||||
return contractNamesContainer |
||||
} |
||||
|
||||
atAddressChanged (event) { |
||||
if (!this.atAddressButtonInput.value) { |
||||
this.enableAtAddress(false) |
||||
} else { |
||||
if ((this.selectContractNames && !this.selectContractNames.getAttribute('disabled') && this.loadType === 'sol') || |
||||
this.loadType === 'abi') { |
||||
this.enableAtAddress(true) |
||||
} else { |
||||
this.enableAtAddress(false) |
||||
} |
||||
} |
||||
} |
||||
|
||||
changeCurrentFile (currentFile) { |
||||
if (!this.selectContractNames) return |
||||
if (/.(.abi)$/.exec(currentFile)) { |
||||
this.createPanel.style.display = 'none' |
||||
this.orLabel.style.display = 'none' |
||||
this.compFails.style.display = 'none' |
||||
this.loadType = 'abi' |
||||
this.contractNamesContainer.style.display = 'block' |
||||
this.abiLabel.style.display = 'block' |
||||
this.abiLabel.innerHTML = currentFile |
||||
this.selectContractNames.style.display = 'none' |
||||
this.enableContractNames(true) |
||||
this.enableAtAddress(true) |
||||
} else if (/.(.sol)$/.exec(currentFile) || |
||||
/.(.vy)$/.exec(currentFile) || // vyper
|
||||
/.(.lex)$/.exec(currentFile) || // lexon
|
||||
/.(.contract)$/.exec(currentFile)) { |
||||
this.createPanel.style.display = 'block' |
||||
this.orLabel.style.display = 'block' |
||||
this.contractNamesContainer.style.display = 'block' |
||||
this.loadType = 'sol' |
||||
this.selectContractNames.style.display = 'block' |
||||
this.abiLabel.style.display = 'none' |
||||
if (this.selectContractNames.value === '') this.enableAtAddress(false) |
||||
} else { |
||||
this.loadType = 'other' |
||||
this.createPanel.style.display = 'block' |
||||
this.orLabel.style.display = 'block' |
||||
this.contractNamesContainer.style.display = 'block' |
||||
this.selectContractNames.style.display = 'block' |
||||
this.abiLabel.style.display = 'none' |
||||
if (this.selectContractNames.value === '') this.enableAtAddress(false) |
||||
} |
||||
} |
||||
|
||||
setInputParamsPlaceHolder () { |
||||
this.createPanel.innerHTML = '' |
||||
if (this.selectContractNames.selectedIndex < 0 || this.selectContractNames.children.length <= 0) { |
||||
this.createPanel.innerHTML = 'No compiled contracts' |
||||
return |
||||
} |
||||
|
||||
const selectedContract = this.getSelectedContract() |
||||
const clickCallback = async (valArray, inputsValues) => { |
||||
var selectedContract = this.getSelectedContract() |
||||
this.createInstance(selectedContract, inputsValues) |
||||
} |
||||
const createConstructorInstance = new MultiParamManager( |
||||
0, |
||||
selectedContract.getConstructorInterface(), |
||||
clickCallback, |
||||
selectedContract.getConstructorInputs(), |
||||
'Deploy', |
||||
selectedContract.bytecodeObject, |
||||
true |
||||
) |
||||
this.createPanel.appendChild(createConstructorInstance.render()) |
||||
this.createPanel.appendChild(this.deployCheckBox) |
||||
} |
||||
|
||||
getSelectedContract () { |
||||
var contract = this.selectContractNames.children[this.selectContractNames.selectedIndex] |
||||
var contractName = contract.getAttribute('value') |
||||
var compilerAtributeName = contract.getAttribute('compiler') |
||||
|
||||
return this.dropdownLogic.getSelectedContract(contractName, compilerAtributeName) |
||||
} |
||||
|
||||
async createInstance (selectedContract, args) { |
||||
if (selectedContract.bytecodeObject.length === 0) { |
||||
return modalDialogCustom.alert('This contract may be abstract, not implement an abstract parent\'s methods completely or not invoke an inherited contract\'s constructor correctly.') |
||||
} |
||||
|
||||
var continueCb = (error, continueTxExecution, cancelCb) => { |
||||
if (error) { |
||||
var msg = typeof error !== 'string' ? error.message : error |
||||
modalDialog('Gas estimation failed', yo`<div>Gas estimation errored with the following message (see below).
|
||||
The transaction execution will likely fail. Do you want to force sending? <br> |
||||
${msg} |
||||
</div>`, |
||||
{ |
||||
label: 'Send Transaction', |
||||
fn: () => { |
||||
continueTxExecution() |
||||
} |
||||
}, { |
||||
label: 'Cancel Transaction', |
||||
fn: () => { |
||||
cancelCb() |
||||
} |
||||
}) |
||||
} else { |
||||
continueTxExecution() |
||||
} |
||||
} |
||||
|
||||
const self = this |
||||
|
||||
var promptCb = (okCb, cancelCb) => { |
||||
modalDialogCustom.promptPassphrase('Passphrase requested', 'Personal mode is enabled. Please provide passphrase of account', '', okCb, cancelCb) |
||||
} |
||||
|
||||
var statusCb = (msg) => { |
||||
return this.logCallback(msg) |
||||
} |
||||
|
||||
var finalCb = (error, contractObject, address) => { |
||||
self.event.trigger('clearInstance') |
||||
|
||||
if (error) { |
||||
return this.logCallback(error) |
||||
} |
||||
self.event.trigger('newContractInstanceAdded', [contractObject, address, contractObject.name]) |
||||
|
||||
const data = self.runView.compilersArtefacts.getCompilerAbstract(contractObject.contract.file) |
||||
self.runView.compilersArtefacts.addResolvedContract(helper.addressToString(address), data) |
||||
if (self.ipfsCheckedState) { |
||||
_paq.push(['trackEvent', 'udapp', 'DeployAndPublish', this.networkName + '_' + this.networkId]) |
||||
publishToStorage('ipfs', self.runView.fileProvider, self.runView.fileManager, selectedContract) |
||||
} else { |
||||
_paq.push(['trackEvent', 'udapp', 'DeployOnly', this.networkName + '_' + this.networkId]) |
||||
} |
||||
} |
||||
|
||||
let contractMetadata |
||||
try { |
||||
contractMetadata = await this.runView.call('compilerMetadata', 'deployMetadataOf', selectedContract.name, selectedContract.contract.file) |
||||
} catch (error) { |
||||
return statusCb(`creation of ${selectedContract.name} errored: ${error.message ? error.message : error}`) |
||||
} |
||||
|
||||
const compilerContracts = this.dropdownLogic.getCompilerContracts() |
||||
const confirmationCb = this.getConfirmationCb(modalDialog, confirmDialog) |
||||
|
||||
if (selectedContract.isOverSizeLimit()) { |
||||
return modalDialog('Contract code size over limit', yo`<div>Contract creation initialization returns data with length of more than 24576 bytes. The deployment will likely fails. <br>
|
||||
More info: <a href="https://github.com/ethereum/EIPs/blob/master/EIPS/eip-170.md" target="_blank">eip-170</a> |
||||
</div>`, |
||||
{ |
||||
label: 'Force Send', |
||||
fn: () => { |
||||
this.deployContract(selectedContract, args, contractMetadata, compilerContracts, { continueCb, promptCb, statusCb, finalCb }, confirmationCb) |
||||
} |
||||
}, { |
||||
label: 'Cancel', |
||||
fn: () => { |
||||
this.logCallback(`creation of ${selectedContract.name} canceled by user.`) |
||||
} |
||||
}) |
||||
} |
||||
this.deployContract(selectedContract, args, contractMetadata, compilerContracts, { continueCb, promptCb, statusCb, finalCb }, confirmationCb) |
||||
} |
||||
|
||||
deployContract (selectedContract, args, contractMetadata, compilerContracts, callbacks, confirmationCb) { |
||||
_paq.push(['trackEvent', 'udapp', 'DeployContractTo', this.networkName + '_' + this.networkId]) |
||||
const { statusCb } = callbacks |
||||
if (!contractMetadata || (contractMetadata && contractMetadata.autoDeployLib)) { |
||||
return this.blockchain.deployContractAndLibraries(selectedContract, args, contractMetadata, compilerContracts, callbacks, confirmationCb) |
||||
} |
||||
if (Object.keys(selectedContract.bytecodeLinkReferences).length) statusCb(`linking ${JSON.stringify(selectedContract.bytecodeLinkReferences, null, '\t')} using ${JSON.stringify(contractMetadata.linkReferences, null, '\t')}`) |
||||
this.blockchain.deployContractWithLibrary(selectedContract, args, contractMetadata, compilerContracts, callbacks, confirmationCb) |
||||
} |
||||
|
||||
getConfirmationCb (modalDialog, confirmDialog) { |
||||
// this code is the same as in recorder.js. TODO need to be refactored out
|
||||
const confirmationCb = (network, tx, gasEstimation, continueTxExecution, cancelCb) => { |
||||
if (network.name !== 'Main') { |
||||
return continueTxExecution(null) |
||||
} |
||||
const amount = this.blockchain.fromWei(tx.value, true, 'ether') |
||||
const content = confirmDialog(tx, network, amount, gasEstimation, this.blockchain.determineGasFees(tx), this.blockchain.determineGasPrice.bind(this.blockchain)) |
||||
|
||||
modalDialog('Confirm transaction', content, |
||||
{ |
||||
label: 'Confirm', |
||||
fn: () => { |
||||
this.blockchain.config.setUnpersistedProperty('doNotShowTransactionConfirmationAgain', content.querySelector('input#confirmsetting').checked) |
||||
// TODO: check if this is check is still valid given the refactor
|
||||
if (!content.gasPriceStatus) { |
||||
cancelCb('Given transaction fee is not correct') |
||||
} else { |
||||
continueTxExecution(content.txFee) |
||||
} |
||||
} |
||||
}, { |
||||
label: 'Cancel', |
||||
fn: () => { |
||||
return cancelCb('Transaction canceled by user.') |
||||
} |
||||
} |
||||
) |
||||
} |
||||
|
||||
return confirmationCb |
||||
} |
||||
|
||||
loadFromAddress () { |
||||
this.event.trigger('clearInstance') |
||||
|
||||
let address = this.atAddressButtonInput.value |
||||
if (!ethJSUtil.isValidChecksumAddress(address)) { |
||||
addTooltip(yo` |
||||
<span> |
||||
It seems you are not using a checksumed address. |
||||
<br>A checksummed address is an address that contains uppercase letters, as specified in <a href="https://eips.ethereum.org/EIPS/eip-55" target="_blank">EIP-55</a>. |
||||
<br>Checksummed addresses are meant to help prevent users from sending transactions to the wrong address. |
||||
</span>`) |
||||
address = ethJSUtil.toChecksumAddress(address) |
||||
} |
||||
this.dropdownLogic.loadContractFromAddress(address, |
||||
(cb) => { |
||||
modalDialogCustom.confirm('At Address', `Do you really want to interact with ${address} using the current ABI definition?`, cb) |
||||
}, |
||||
(error, loadType, abi) => { |
||||
if (error) { |
||||
return modalDialogCustom.alert(error) |
||||
} |
||||
if (loadType === 'abi') { |
||||
return this.event.trigger('newContractABIAdded', [abi, address]) |
||||
} |
||||
var selectedContract = this.getSelectedContract() |
||||
this.event.trigger('newContractInstanceAdded', [selectedContract.object, address, this.selectContractNames.value]) |
||||
} |
||||
) |
||||
} |
||||
} |
||||
|
||||
module.exports = ContractDropdownUI |
@ -1,108 +0,0 @@ |
||||
import { CompilerAbstract } from '@remix-project/remix-solidity' |
||||
const remixLib = require('@remix-project/remix-lib') |
||||
const txHelper = remixLib.execution.txHelper |
||||
const EventManager = remixLib.EventManager |
||||
const _paq = window._paq = window._paq || [] |
||||
|
||||
class DropdownLogic { |
||||
constructor (compilersArtefacts, config, editor, runView) { |
||||
this.compilersArtefacts = compilersArtefacts |
||||
this.config = config |
||||
this.editor = editor |
||||
this.runView = runView |
||||
|
||||
this.event = new EventManager() |
||||
|
||||
this.listenToCompilationEvents() |
||||
} |
||||
|
||||
// TODO: can be moved up; the event in contractDropdown will have to refactored a method instead
|
||||
listenToCompilationEvents () { |
||||
const broadcastCompilationResult = (file, source, languageVersion, data) => { |
||||
// TODO check whether the tab is configured
|
||||
const compiler = new CompilerAbstract(languageVersion, data, source) |
||||
this.compilersArtefacts[languageVersion] = compiler |
||||
this.compilersArtefacts.__last = compiler |
||||
this.event.trigger('newlyCompiled', [true, data, source, compiler, languageVersion, file]) |
||||
} |
||||
this.runView.on('solidity', 'compilationFinished', (file, source, languageVersion, data) => |
||||
broadcastCompilationResult(file, source, languageVersion, data) |
||||
) |
||||
this.runView.on('vyper', 'compilationFinished', (file, source, languageVersion, data) => |
||||
broadcastCompilationResult(file, source, languageVersion, data) |
||||
) |
||||
this.runView.on('lexon', 'compilationFinished', (file, source, languageVersion, data) => |
||||
broadcastCompilationResult(file, source, languageVersion, data) |
||||
) |
||||
this.runView.on('yulp', 'compilationFinished', (file, source, languageVersion, data) => |
||||
broadcastCompilationResult(file, source, languageVersion, data) |
||||
) |
||||
this.runView.on('optimism-compiler', 'compilationFinished', (file, source, languageVersion, data) => |
||||
broadcastCompilationResult(file, source, languageVersion, data) |
||||
) |
||||
} |
||||
|
||||
loadContractFromAddress (address, confirmCb, cb) { |
||||
if (/.(.abi)$/.exec(this.config.get('currentFile'))) { |
||||
confirmCb(() => { |
||||
var abi |
||||
try { |
||||
abi = JSON.parse(this.editor.currentContent()) |
||||
} catch (e) { |
||||
return cb('Failed to parse the current file as JSON ABI.') |
||||
} |
||||
_paq.push(['trackEvent', 'udapp', 'AtAddressLoadWithABI']) |
||||
cb(null, 'abi', abi) |
||||
}) |
||||
} else { |
||||
_paq.push(['trackEvent', 'udapp', 'AtAddressLoadWithArtifacts']) |
||||
cb(null, 'instance') |
||||
} |
||||
} |
||||
|
||||
getCompiledContracts (compiler, compilerFullName) { |
||||
var contracts = [] |
||||
compiler.visitContracts((contract) => { |
||||
contracts.push(contract) |
||||
}) |
||||
return contracts |
||||
} |
||||
|
||||
getSelectedContract (contractName, compilerAtributeName) { |
||||
if (!contractName) return null |
||||
|
||||
var compiler = this.compilersArtefacts[compilerAtributeName] |
||||
if (!compiler) return null |
||||
|
||||
var contract = compiler.getContract(contractName) |
||||
|
||||
return { |
||||
name: contractName, |
||||
contract: contract, |
||||
compiler: compiler, |
||||
abi: contract.object.abi, |
||||
bytecodeObject: contract.object.evm.bytecode.object, |
||||
bytecodeLinkReferences: contract.object.evm.bytecode.linkReferences, |
||||
object: contract.object, |
||||
deployedBytecode: contract.object.evm.deployedBytecode, |
||||
getConstructorInterface: () => { |
||||
return txHelper.getConstructorInterface(contract.object.abi) |
||||
}, |
||||
getConstructorInputs: () => { |
||||
var constructorInteface = txHelper.getConstructorInterface(contract.object.abi) |
||||
return txHelper.inputParametersDeclarationToString(constructorInteface.inputs) |
||||
}, |
||||
isOverSizeLimit: () => { |
||||
var deployedBytecode = contract.object.evm.deployedBytecode |
||||
return (deployedBytecode && deployedBytecode.object.length / 2 > 24576) |
||||
}, |
||||
metadata: contract.object.metadata |
||||
} |
||||
} |
||||
|
||||
getCompilerContracts () { |
||||
return this.compilersArtefacts.__last.getData().contracts |
||||
} |
||||
} |
||||
|
||||
module.exports = DropdownLogic |
@ -1,162 +0,0 @@ |
||||
import { Plugin } from '@remixproject/engine' |
||||
|
||||
import * as packageJson from '../../../../../../package.json' |
||||
var yo = require('yo-yo') |
||||
var remixLib = require('@remix-project/remix-lib') |
||||
var EventManager = remixLib.EventManager |
||||
var csjs = require('csjs-inject') |
||||
var css = require('../styles/run-tab-styles') |
||||
|
||||
var modalDialogCustom = require('../../ui/modal-dialog-custom') |
||||
var modalDialog = require('../../ui/modaldialog') |
||||
var confirmDialog = require('../../ui/confirmDialog') |
||||
|
||||
var helper = require('../../../lib/helper.js') |
||||
|
||||
const profile = { |
||||
name: 'recorder', |
||||
methods: ['runScenario'], |
||||
version: packageJson.version |
||||
} |
||||
|
||||
class RecorderUI extends Plugin { |
||||
constructor (blockchain, fileManager, recorder, logCallBack, config) { |
||||
super(profile) |
||||
this.fileManager = fileManager |
||||
this.blockchain = blockchain |
||||
this.recorder = recorder |
||||
this.logCallBack = logCallBack |
||||
this.config = config |
||||
this.event = new EventManager() |
||||
} |
||||
|
||||
render () { |
||||
var css2 = csjs` |
||||
.container {} |
||||
.runTxs {} |
||||
.recorder {} |
||||
` |
||||
|
||||
this.runButton = yo`<i class="fas fa-play runtransaction ${css2.runTxs} ${css.icon}" title="Run Transactions" aria-hidden="true"></i>` |
||||
this.recordButton = yo` |
||||
<i class="fas fa-save savetransaction ${css2.recorder} ${css.icon}" |
||||
onclick=${this.triggerRecordButton.bind(this)} title="Save Transactions" aria-hidden="true"> |
||||
</i>` |
||||
|
||||
this.runButton.onclick = () => { |
||||
const file = this.config.get('currentFile') |
||||
if (!file) return modalDialogCustom.alert('A scenario file has to be selected') |
||||
this.runScenario(file) |
||||
} |
||||
} |
||||
|
||||
runScenario (file) { |
||||
if (!file) return modalDialogCustom.alert('Unable to run scenerio, no specified scenario file') |
||||
var continueCb = (error, continueTxExecution, cancelCb) => { |
||||
if (error) { |
||||
var msg = typeof error !== 'string' ? error.message : error |
||||
modalDialog('Gas estimation failed', yo`<div>Gas estimation errored with the following message (see below).
|
||||
The transaction execution will likely fail. Do you want to force sending? <br> |
||||
${msg} |
||||
</div>`, |
||||
{ |
||||
label: 'Send Transaction', |
||||
fn: () => { |
||||
continueTxExecution() |
||||
} |
||||
}, { |
||||
label: 'Cancel Transaction', |
||||
fn: () => { |
||||
cancelCb() |
||||
} |
||||
}) |
||||
} else { |
||||
continueTxExecution() |
||||
} |
||||
} |
||||
|
||||
var promptCb = (okCb, cancelCb) => { |
||||
modalDialogCustom.promptPassphrase('Passphrase requested', 'Personal mode is enabled. Please provide passphrase of account', '', okCb, cancelCb) |
||||
} |
||||
|
||||
var alertCb = (msg) => { |
||||
modalDialogCustom.alert(msg) |
||||
} |
||||
|
||||
const confirmationCb = this.getConfirmationCb(modalDialog, confirmDialog) |
||||
|
||||
this.fileManager.readFile(file).then((json) => { |
||||
// TODO: there is still a UI dependency to remove here, it's still too coupled at this point to remove easily
|
||||
this.recorder.runScenario(json, continueCb, promptCb, alertCb, confirmationCb, this.logCallBack, (error, abi, address, contractName) => { |
||||
if (error) { |
||||
return modalDialogCustom.alert(error) |
||||
} |
||||
|
||||
this.event.trigger('newScenario', [abi, address, contractName]) |
||||
}) |
||||
}).catch((error) => modalDialogCustom.alert(error)) |
||||
} |
||||
|
||||
getConfirmationCb (modalDialog, confirmDialog) { |
||||
// this code is the same as in contractDropdown.js. TODO need to be refactored out
|
||||
const confirmationCb = (network, tx, gasEstimation, continueTxExecution, cancelCb) => { |
||||
if (network.name !== 'Main') { |
||||
return continueTxExecution(null) |
||||
} |
||||
const amount = this.blockchain.fromWei(tx.value, true, 'ether') |
||||
const content = confirmDialog(tx, network, amount, gasEstimation, this.blockchain.determineGasFees(tx), this.blockchain.determineGasPrice.bind(this.blockchain)) |
||||
|
||||
modalDialog('Confirm transaction', content, |
||||
{ |
||||
label: 'Confirm', |
||||
fn: () => { |
||||
this.config.setUnpersistedProperty('doNotShowTransactionConfirmationAgain', content.querySelector('input#confirmsetting').checked) |
||||
// TODO: check if this is check is still valid given the refactor
|
||||
if (!content.gasPriceStatus) { |
||||
cancelCb('Given transaction fee is not correct') |
||||
} else { |
||||
continueTxExecution(content.txFee) |
||||
} |
||||
} |
||||
}, { |
||||
label: 'Cancel', |
||||
fn: () => { |
||||
return cancelCb('Transaction canceled by user.') |
||||
} |
||||
} |
||||
) |
||||
} |
||||
|
||||
return confirmationCb |
||||
} |
||||
|
||||
triggerRecordButton () { |
||||
this.saveScenario( |
||||
(path, cb) => { |
||||
modalDialogCustom.prompt('Save transactions as scenario', 'Transactions will be saved in a file under ' + path, 'scenario.json', cb) |
||||
}, |
||||
(error) => { |
||||
if (error) return modalDialogCustom.alert(error) |
||||
} |
||||
) |
||||
} |
||||
|
||||
async saveScenario (promptCb, cb) { |
||||
var txJSON = JSON.stringify(this.recorder.getAll(), null, 2) |
||||
var path = this.fileManager.currentPath() |
||||
promptCb(path, async input => { |
||||
var fileProvider = this.fileManager.fileProviderOf(path) |
||||
if (!fileProvider) return |
||||
var newFile = path + '/' + input |
||||
try { |
||||
newFile = await helper.createNonClashingNameAsync(newFile, this.fileManager) |
||||
await fileProvider.set(newFile, txJSON) |
||||
await this.fileManager.open(newFile) |
||||
} catch (error) { |
||||
if (error) return cb('Failed to create file. ' + newFile + ' ' + error) |
||||
} |
||||
}) |
||||
} |
||||
} |
||||
|
||||
module.exports = RecorderUI |
@ -1,449 +0,0 @@ |
||||
import { BN } from 'ethereumjs-util' |
||||
const $ = require('jquery') |
||||
const yo = require('yo-yo') |
||||
const remixLib = require('@remix-project/remix-lib') |
||||
const EventManager = remixLib.EventManager |
||||
const css = require('../styles/run-tab-styles') |
||||
const copyToClipboard = require('../../ui/copy-to-clipboard') |
||||
const modalDialogCustom = require('../../ui/modal-dialog-custom') |
||||
const addTooltip = require('../../ui/tooltip') |
||||
const helper = require('../../../lib/helper.js') |
||||
const globalRegistry = require('../../../global/registry') |
||||
|
||||
class SettingsUI { |
||||
constructor (blockchain, networkModule) { |
||||
this.blockchain = blockchain |
||||
this.event = new EventManager() |
||||
this._components = {} |
||||
|
||||
this.blockchain.event.register('transactionExecuted', (error, from, to, data, lookupOnly, txResult) => { |
||||
if (!lookupOnly) this.el.querySelector('#value').value = 0 |
||||
if (error) return |
||||
this.updateAccountBalances() |
||||
}) |
||||
this._components = { |
||||
registry: globalRegistry, |
||||
networkModule: networkModule |
||||
} |
||||
this._components.registry = globalRegistry |
||||
this._deps = { |
||||
config: this._components.registry.get('config').api |
||||
} |
||||
|
||||
this._deps.config.events.on('settings/personal-mode_changed', this.onPersonalChange.bind(this)) |
||||
|
||||
setInterval(() => { |
||||
this.updateAccountBalances() |
||||
}, 1000) |
||||
|
||||
this.accountListCallId = 0 |
||||
this.loadedAccounts = {} |
||||
} |
||||
|
||||
updateAccountBalances () { |
||||
if (!this.el) return |
||||
var accounts = $(this.el.querySelector('#txorigin')).children('option') |
||||
accounts.each((index, account) => { |
||||
this.blockchain.getBalanceInEther(account.value, (err, balance) => { |
||||
if (err) return |
||||
const updated = helper.shortenAddress(account.value, balance) |
||||
if (updated !== account.innerText) { // check if the balance has been updated and update UI accordingly.
|
||||
account.innerText = updated |
||||
} |
||||
}) |
||||
}) |
||||
} |
||||
|
||||
validateInputKey (e) { |
||||
// preventing not numeric keys
|
||||
// preventing 000 case
|
||||
if (!helper.isNumeric(e.key) || |
||||
(e.key === '0' && !parseInt(this.el.querySelector('#value').value) && this.el.querySelector('#value').value.length > 0)) { |
||||
e.preventDefault() |
||||
e.stopImmediatePropagation() |
||||
} |
||||
} |
||||
|
||||
validateValue () { |
||||
const valueEl = this.el.querySelector('#value') |
||||
if (!valueEl.value) { |
||||
// assign 0 if given value is
|
||||
// - empty
|
||||
valueEl.value = 0 |
||||
return |
||||
} |
||||
|
||||
let v |
||||
try { |
||||
v = new BN(valueEl.value, 10) |
||||
valueEl.value = v.toString(10) |
||||
} catch (e) { |
||||
// assign 0 if given value is
|
||||
// - not valid (for ex 4345-54)
|
||||
// - contains only '0's (for ex 0000) copy past or edit
|
||||
valueEl.value = 0 |
||||
} |
||||
|
||||
// if giveen value is negative(possible with copy-pasting) set to 0
|
||||
if (v.lt(0)) valueEl.value = 0 |
||||
} |
||||
|
||||
render () { |
||||
this.netUI = yo`<span class="${css.network} badge badge-secondary"></span>` |
||||
|
||||
var environmentEl = yo` |
||||
<div class="${css.crow}"> |
||||
<label id="selectExEnv" class="${css.settingsLabel}"> |
||||
Environment |
||||
</label> |
||||
<div class="${css.environment}"> |
||||
<select id="selectExEnvOptions" data-id="settingsSelectEnvOptions" class="form-control ${css.select} custom-select"> |
||||
<option id="vm-mode-london" data-id="settingsVMLondonMode" |
||||
title="Execution environment does not connect to any node, everything is local and in memory only." |
||||
value="vm-london" name="executionContext" fork="london"> JavaScript VM (London) |
||||
</option> |
||||
<option id="vm-mode-berlin" data-id="settingsVMBerlinMode" |
||||
title="Execution environment does not connect to any node, everything is local and in memory only." |
||||
value="vm-berlin" name="executionContext" fork="berlin" > JavaScript VM (Berlin) |
||||
</option> |
||||
<option id="injected-mode" data-id="settingsInjectedMode" |
||||
title="Execution environment has been provided by Metamask or similar provider." |
||||
value="injected" name="executionContext"> Injected Web3 |
||||
</option> |
||||
<option id="web3-mode" data-id="settingsWeb3Mode" |
||||
title="Execution environment connects to node at localhost (or via IPC if available), transactions will be sent to the network and can cause loss of money or worse! |
||||
If this page is served via https and you access your node via http, it might not work. In this case, try cloning the repository and serving it via http." |
||||
value="web3" name="executionContext"> Web3 Provider |
||||
</option> |
||||
</select> |
||||
<a href="https://remix-ide.readthedocs.io/en/latest/run.html#run-setup" target="_blank"><i class="${css.infoDeployAction} ml-2 fas fa-info" title="check out docs to setup Environment"></i></a> |
||||
</div> |
||||
</div> |
||||
` |
||||
const networkEl = yo` |
||||
<div class="${css.crow}"> |
||||
<div class="${css.settingsLabel}"> |
||||
</div> |
||||
<div class="${css.environment}" data-id="settingsNetworkEnv"> |
||||
${this.netUI} |
||||
</div> |
||||
</div> |
||||
` |
||||
const accountEl = yo` |
||||
<div class="${css.crow}"> |
||||
<label class="${css.settingsLabel}"> |
||||
Account |
||||
<span id="remixRunPlusWraper" title="Create a new account" onload=${this.updatePlusButton.bind(this)}> |
||||
<i id="remixRunPlus" class="fas fa-plus-circle ${css.icon}" aria-hidden="true" onclick=${this.newAccount.bind(this)}"></i> |
||||
</span> |
||||
</label> |
||||
<div class="${css.account}"> |
||||
<select data-id="runTabSelectAccount" name="txorigin" class="form-control ${css.select} custom-select pr-4" id="txorigin"></select> |
||||
<div style="margin-left: -5px;">${copyToClipboard(() => document.querySelector('#runTabView #txorigin').value)}</div> |
||||
<i id="remixRunSignMsg" data-id="settingsRemixRunSignMsg" class="mx-1 fas fa-edit ${css.icon}" aria-hidden="true" onclick=${this.signMessage.bind(this)} title="Sign a message using this account key"></i> |
||||
</div> |
||||
</div> |
||||
` |
||||
|
||||
const gasPriceEl = yo` |
||||
<div class="${css.crow}"> |
||||
<label class="${css.settingsLabel}">Gas limit</label> |
||||
<input type="number" class="form-control ${css.gasNval} ${css.col2}" id="gasLimit" value="3000000"> |
||||
</div> |
||||
` |
||||
|
||||
const valueEl = yo` |
||||
<div class="${css.crow}"> |
||||
<label class="${css.settingsLabel}" data-id="remixDRValueLabel">Value</label> |
||||
<div class="${css.gasValueContainer}"> |
||||
<input |
||||
type="number" |
||||
min="0" |
||||
pattern="^[0-9]" |
||||
step="1" |
||||
class="form-control ${css.gasNval} ${css.col2}" |
||||
id="value" |
||||
data-id="dandrValue" |
||||
value="0" |
||||
title="Enter the value and choose the unit" |
||||
onkeypress=${(e) => this.validateInputKey(e)} |
||||
onchange=${() => this.validateValue()} |
||||
> |
||||
<select name="unit" class="form-control p-1 ${css.gasNvalUnit} ${css.col2_2} custom-select" id="unit"> |
||||
<option data-unit="wei">Wei</option> |
||||
<option data-unit="gwei">Gwei</option> |
||||
<option data-unit="finney">Finney</option> |
||||
<option data-unit="ether">Ether</option> |
||||
</select> |
||||
</div> |
||||
</div> |
||||
` |
||||
|
||||
const el = yo` |
||||
<div class="${css.settings}"> |
||||
${environmentEl} |
||||
${networkEl} |
||||
${accountEl} |
||||
${gasPriceEl} |
||||
${valueEl} |
||||
</div> |
||||
` |
||||
|
||||
var selectExEnv = environmentEl.querySelector('#selectExEnvOptions') |
||||
this.setDropdown(selectExEnv) |
||||
|
||||
this.blockchain.event.register('contextChanged', (context, silent) => { |
||||
this.setFinalContext() |
||||
}) |
||||
|
||||
this.blockchain.event.register('networkStatus', ({ error, network }) => { |
||||
if (error) { |
||||
this.netUI.innerHTML = 'can\'t detect network ' |
||||
return |
||||
} |
||||
const networkProvider = this._components.networkModule.getNetworkProvider.bind(this._components.networkModule) |
||||
this.netUI.innerHTML = (networkProvider() !== 'vm') ? `${network.name} (${network.id || '-'}) network` : '' |
||||
}) |
||||
|
||||
setInterval(() => { |
||||
this.fillAccountsList() |
||||
}, 1000) |
||||
|
||||
this.el = el |
||||
|
||||
this.fillAccountsList() |
||||
return el |
||||
} |
||||
|
||||
setDropdown (selectExEnv) { |
||||
this.selectExEnv = selectExEnv |
||||
|
||||
const addProvider = (network) => { |
||||
selectExEnv.appendChild(yo`<option
|
||||
title="provider name: ${network.name}" |
||||
value="${network.name}" |
||||
name="executionContext" |
||||
> |
||||
${network.name} |
||||
</option>`) |
||||
addTooltip(yo`<span><b>${network.name}</b> provider added</span>`) |
||||
} |
||||
|
||||
const removeProvider = (name) => { |
||||
var env = selectExEnv.querySelector(`option[value="${name}"]`) |
||||
if (env) { |
||||
selectExEnv.removeChild(env) |
||||
addTooltip(yo`<span><b>${name}</b> provider removed</span>`) |
||||
} |
||||
} |
||||
this.blockchain.event.register('addProvider', provider => addProvider(provider)) |
||||
this.blockchain.event.register('removeProvider', name => removeProvider(name)) |
||||
|
||||
selectExEnv.addEventListener('change', (event) => { |
||||
const provider = selectExEnv.options[selectExEnv.selectedIndex] |
||||
const fork = provider.getAttribute('fork') // can be undefined if connected to an external source (web3 provider / injected)
|
||||
let context = provider.value |
||||
context = context.startsWith('vm') ? 'vm' : context // context has to be 'vm', 'web3' or 'injected'
|
||||
this.setExecutionContext({ context, fork }) |
||||
}) |
||||
|
||||
selectExEnv.value = this._getProviderDropdownValue() |
||||
} |
||||
|
||||
setExecutionContext (context) { |
||||
this.blockchain.changeExecutionContext(context, () => { |
||||
modalDialogCustom.prompt('External node request', this.web3ProviderDialogBody(), 'http://127.0.0.1:8545', (target) => { |
||||
this.blockchain.setProviderFromEndpoint(target, context, (alertMsg) => { |
||||
if (alertMsg) addTooltip(alertMsg) |
||||
this.setFinalContext() |
||||
}) |
||||
}, this.setFinalContext.bind(this)) |
||||
}, (alertMsg) => { |
||||
addTooltip(alertMsg) |
||||
}, this.setFinalContext.bind(this)) |
||||
} |
||||
|
||||
web3ProviderDialogBody () { |
||||
const thePath = '<path/to/local/folder/for/test/chain>' |
||||
|
||||
return yo` |
||||
<div class=""> |
||||
Note: To use Geth & https://remix.ethereum.org, configure it to allow requests from Remix:(see <a href="https://geth.ethereum.org/docs/rpc/server" target="_blank">Geth Docs on rpc server</a>)
|
||||
<div class="border p-1">geth --http --http.corsdomain https://remix.ethereum.org</div>
|
||||
<br> |
||||
To run Remix & a local Geth test node, use this command: (see <a href="https://geth.ethereum.org/getting-started/dev-mode" target="_blank">Geth Docs on Dev mode</a>) |
||||
<div class="border p-1">geth --http --http.corsdomain="${window.origin}" --http.api web3,eth,debug,personal,net --vmdebug --datadir ${thePath} --dev console</div> |
||||
<br> |
||||
<br>
|
||||
<b>WARNING:</b> It is not safe to use the --http.corsdomain flag with a wildcard: <b>--http.corsdomain *</b> |
||||
<br> |
||||
<br>For more info: <a href="https://remix-ide.readthedocs.io/en/latest/run.html#more-about-web3-provider" target="_blank">Remix Docs on Web3 Provider</a> |
||||
<br> |
||||
<br>
|
||||
Web3 Provider Endpoint |
||||
</div> |
||||
` |
||||
} |
||||
|
||||
/** |
||||
* generate a value used by the env dropdown list. |
||||
* @return {String} - can return 'vm-berlin, 'vm-london', 'injected' or 'web3' |
||||
*/ |
||||
_getProviderDropdownValue () { |
||||
const provider = this.blockchain.getProvider() |
||||
const fork = this.blockchain.getCurrentFork() |
||||
return provider === 'vm' ? provider + '-' + fork : provider |
||||
} |
||||
|
||||
setFinalContext () { |
||||
// set the final context. Cause it is possible that this is not the one we've originaly selected
|
||||
this.selectExEnv.value = this._getProviderDropdownValue() |
||||
this.event.trigger('clearInstance', []) |
||||
this.updatePlusButton() |
||||
} |
||||
|
||||
updatePlusButton () { |
||||
// enable/disable + button
|
||||
const plusBtn = document.getElementById('remixRunPlus') |
||||
const plusTitle = document.getElementById('remixRunPlusWraper') |
||||
switch (this.selectExEnv.value) { |
||||
case 'injected': |
||||
plusBtn.classList.add(css.disableMouseEvents) |
||||
plusTitle.title = "Unfortunately it's not possible to create an account using injected web3. Please create the account directly from your provider (i.e metamask or other of the same type)." |
||||
|
||||
break |
||||
case 'vm': |
||||
plusBtn.classList.remove(css.disableMouseEvents) |
||||
plusTitle.title = 'Create a new account' |
||||
|
||||
break |
||||
|
||||
case 'web3': |
||||
this.onPersonalChange() |
||||
|
||||
break |
||||
default: { |
||||
plusBtn.classList.add(css.disableMouseEvents) |
||||
plusTitle.title = `Unfortunately it's not possible to create an account using an external wallet (${this.selectExEnv.value}).` |
||||
} |
||||
} |
||||
} |
||||
|
||||
onPersonalChange () { |
||||
const plusBtn = document.getElementById('remixRunPlus') |
||||
const plusTitle = document.getElementById('remixRunPlusWraper') |
||||
if (!this._deps.config.get('settings/personal-mode')) { |
||||
plusBtn.classList.add(css.disableMouseEvents) |
||||
plusTitle.title = 'Creating an account is possible only in Personal mode. Please go to Settings to enable it.' |
||||
} else { |
||||
plusBtn.classList.remove(css.disableMouseEvents) |
||||
plusTitle.title = 'Create a new account' |
||||
} |
||||
} |
||||
|
||||
newAccount () { |
||||
this.blockchain.newAccount( |
||||
'', |
||||
(cb) => { |
||||
modalDialogCustom.promptPassphraseCreation((error, passphrase) => { |
||||
if (error) { |
||||
return modalDialogCustom.alert(error) |
||||
} |
||||
cb(passphrase) |
||||
}, () => {}) |
||||
}, |
||||
(error, address) => { |
||||
if (error) { |
||||
return addTooltip('Cannot create an account: ' + error) |
||||
} |
||||
addTooltip(`account ${address} created`) |
||||
} |
||||
) |
||||
} |
||||
|
||||
getSelectedAccount () { |
||||
return this.el.querySelector('#txorigin').selectedOptions[0].value |
||||
} |
||||
|
||||
getEnvironment () { |
||||
return this.blockchain.getProvider() |
||||
} |
||||
|
||||
signMessage () { |
||||
this.blockchain.getAccounts((err, accounts) => { |
||||
if (err) { |
||||
return addTooltip(`Cannot get account list: ${err}`) |
||||
} |
||||
|
||||
var signMessageDialog = { title: 'Sign a message', text: 'Enter a message to sign', inputvalue: 'Message to sign' } |
||||
var $txOrigin = this.el.querySelector('#txorigin') |
||||
if (!$txOrigin.selectedOptions[0] && (this.blockchain.isInjectedWeb3() || this.blockchain.isWeb3Provider())) { |
||||
return addTooltip('Account list is empty, please make sure the current provider is properly connected to remix') |
||||
} |
||||
|
||||
var account = $txOrigin.selectedOptions[0].value |
||||
|
||||
var promptCb = (passphrase) => { |
||||
const modal = modalDialogCustom.promptMulti(signMessageDialog, (message) => { |
||||
this.blockchain.signMessage(message, account, passphrase, (err, msgHash, signedData) => { |
||||
if (err) { |
||||
return addTooltip(err) |
||||
} |
||||
modal.hide() |
||||
modalDialogCustom.alert(yo` |
||||
<div> |
||||
<b>hash:</b><br> |
||||
<span id="remixRunSignMsgHash" data-id="settingsRemixRunSignMsgHash">${msgHash}</span> |
||||
<br><b>signature:</b><br> |
||||
<span id="remixRunSignMsgSignature" data-id="settingsRemixRunSignMsgSignature">${signedData}</span> |
||||
</div> |
||||
`)
|
||||
}) |
||||
}, false) |
||||
} |
||||
|
||||
if (this.blockchain.isWeb3Provider()) { |
||||
return modalDialogCustom.promptPassphrase( |
||||
'Passphrase to sign a message', |
||||
'Enter your passphrase for this account to sign the message', |
||||
'', |
||||
promptCb, |
||||
false |
||||
) |
||||
} |
||||
promptCb() |
||||
}) |
||||
} |
||||
|
||||
// TODO: unclear what's the goal of accountListCallId, feels like it can be simplified
|
||||
async fillAccountsList () { |
||||
this.accountListCallId++ |
||||
const callid = this.accountListCallId |
||||
const txOrigin = this.el.querySelector('#txorigin') |
||||
let accounts = [] |
||||
try { |
||||
accounts = await this.blockchain.getAccounts() |
||||
} catch (e) { |
||||
addTooltip(`Cannot get account list: ${e}`) |
||||
} |
||||
if (!accounts) accounts = [] |
||||
if (this.accountListCallId > callid) return |
||||
this.accountListCallId++ |
||||
for (const loadedaddress in this.loadedAccounts) { |
||||
if (accounts.indexOf(loadedaddress) === -1) { |
||||
txOrigin.removeChild(txOrigin.querySelector('option[value="' + loadedaddress + '"]')) |
||||
delete this.loadedAccounts[loadedaddress] |
||||
} |
||||
} |
||||
for (const i in accounts) { |
||||
const address = accounts[i] |
||||
if (!this.loadedAccounts[address]) { |
||||
txOrigin.appendChild(yo`<option value="${address}" >${address}</option>`) |
||||
this.loadedAccounts[address] = 1 |
||||
} |
||||
} |
||||
txOrigin.setAttribute('value', accounts[0]) |
||||
} |
||||
} |
||||
|
||||
module.exports = SettingsUI |
@ -1,225 +0,0 @@ |
||||
var csjs = require('csjs-inject') |
||||
|
||||
var css = csjs` |
||||
.runTabView { |
||||
display: flex; |
||||
flex-direction: column; |
||||
} |
||||
.runTabView::-webkit-scrollbar { |
||||
display: none; |
||||
} |
||||
.settings { |
||||
padding: 0 24px 16px; |
||||
} |
||||
.crow { |
||||
display: block; |
||||
margin-top: 8px; |
||||
} |
||||
.col1 { |
||||
width: 30%; |
||||
float: left; |
||||
align-self: center; |
||||
} |
||||
.settingsLabel { |
||||
font-size: 11px; |
||||
margin-bottom: 4px; |
||||
text-transform: uppercase; |
||||
} |
||||
.environment { |
||||
display: flex; |
||||
align-items: center; |
||||
position: relative; |
||||
width: 100%; |
||||
} |
||||
.environment a { |
||||
margin-left: 7px; |
||||
} |
||||
.account { |
||||
display: flex; |
||||
align-items: center; |
||||
} |
||||
.account i { |
||||
margin-left: 12px; |
||||
} |
||||
.col2 { |
||||
border-radius: 3px; |
||||
} |
||||
.col2_1 { |
||||
width: 164px; |
||||
min-width: 164px; |
||||
} |
||||
.col2_2 { |
||||
} |
||||
.select { |
||||
font-weight: normal; |
||||
width: 100%; |
||||
overflow: hidden; |
||||
} |
||||
.instanceContainer { |
||||
display: flex; |
||||
flex-direction: column; |
||||
margin-bottom: 2%; |
||||
border: none; |
||||
text-align: center; |
||||
padding: 0 14px 16px; |
||||
} |
||||
.pendingTxsContainer { |
||||
display: flex; |
||||
flex-direction: column; |
||||
margin-top: 2%; |
||||
border: none; |
||||
text-align: center; |
||||
} |
||||
.container { |
||||
padding: 0 24px 16px; |
||||
} |
||||
.recorderDescription { |
||||
margin: 0 15px 15px 0; |
||||
} |
||||
.contractNames { |
||||
width: 100%; |
||||
border: 1px solid |
||||
} |
||||
.subcontainer { |
||||
display: flex; |
||||
flex-direction: row; |
||||
align-items: center; |
||||
margin-bottom: 8px; |
||||
} |
||||
.subcontainer i { |
||||
width: 16px; |
||||
display: flex; |
||||
justify-content: center; |
||||
margin-left: 1px; |
||||
} |
||||
.button button{ |
||||
flex: none; |
||||
} |
||||
.button { |
||||
display: flex; |
||||
align-items: center; |
||||
margin-top: 13px; |
||||
} |
||||
.transaction { |
||||
} |
||||
.atAddress { |
||||
margin: 0; |
||||
min-width: 100px; |
||||
width: 100px; |
||||
height: 100%; |
||||
word-break: inherit; |
||||
border-top-right-radius: 0; |
||||
border-bottom-right-radius: 0; |
||||
border-right: 0; |
||||
} |
||||
.atAddressSect { |
||||
margin-top: 8px; |
||||
height: 32px; |
||||
} |
||||
.atAddressSect input { |
||||
height: 32px; |
||||
border-top-left-radius: 0 !important; |
||||
border-bottom-left-radius: 0 !important; |
||||
} |
||||
.ataddressinput { |
||||
padding: .25rem; |
||||
} |
||||
.create { |
||||
} |
||||
.input { |
||||
font-size: 10px !important; |
||||
} |
||||
.noInstancesText { |
||||
font-style: italic; |
||||
text-align: left; |
||||
padding-left: 15px; |
||||
} |
||||
.pendingTxsText { |
||||
font-style: italic; |
||||
display: flex; |
||||
justify-content: space-evenly; |
||||
align-items: center; |
||||
flex-wrap: wrap; |
||||
} |
||||
.item { |
||||
margin-right: 1em; |
||||
display: flex; |
||||
align-items: center; |
||||
} |
||||
.pendingContainer { |
||||
display: flex; |
||||
align-items: baseline; |
||||
} |
||||
.pending { |
||||
height: 25px; |
||||
text-align: center; |
||||
padding-left: 10px; |
||||
border-radius: 3px; |
||||
margin-left: 5px; |
||||
} |
||||
.disableMouseEvents { |
||||
pointer-events: none; |
||||
} |
||||
.icon { |
||||
cursor: pointer; |
||||
font-size: 12px; |
||||
cursor: pointer; |
||||
margin-left: 5px; |
||||
} |
||||
.icon:hover { |
||||
font-size: 12px; |
||||
color: var(--warning); |
||||
} |
||||
.errorIcon { |
||||
color: var(--warning); |
||||
margin-left: 15px; |
||||
} |
||||
.failDesc { |
||||
color: var(--warning); |
||||
padding-left: 10px; |
||||
display: inline; |
||||
} |
||||
.network { |
||||
margin-left: 8px; |
||||
pointer-events: none; |
||||
} |
||||
.networkItem { |
||||
margin-right: 5px; |
||||
} |
||||
.transactionActions { |
||||
display: flex; |
||||
justify-content: space-evenly; |
||||
width: 145px; |
||||
} |
||||
.orLabel { |
||||
text-align: center; |
||||
text-transform: uppercase; |
||||
} |
||||
.infoDeployAction { |
||||
margin-left: 1px; |
||||
font-size: 13px; |
||||
color: var(--info); |
||||
} |
||||
.gasValueContainer { |
||||
flex-direction: row; |
||||
display: flex; |
||||
} |
||||
.gasNval { |
||||
width: 55%; |
||||
font-size: 0.8rem; |
||||
} |
||||
.gasNvalUnit { |
||||
width: 41%; |
||||
margin-left: 10px; |
||||
font-size: 0.8rem; |
||||
} |
||||
.deployDropdown { |
||||
text-align: center; |
||||
text-transform: uppercase; |
||||
} |
||||
.checkboxAlign { |
||||
padding-top: 2px; |
||||
} |
||||
` |
||||
|
||||
module.exports = css |
@ -1,136 +0,0 @@ |
||||
const helper = require('../../../lib/helper.js') |
||||
const modalDialogCustom = require('../../ui/modal-dialog-custom') |
||||
const remixPath = require('path') |
||||
|
||||
class TestTabLogic { |
||||
constructor (fileManager) { |
||||
this.fileManager = fileManager |
||||
this.currentPath = '/tests' |
||||
} |
||||
|
||||
setCurrentPath (path) { |
||||
if (path.indexOf('/') === 0) return |
||||
this.currentPath = helper.removeMultipleSlashes(helper.removeTrailingSlashes(path)) |
||||
} |
||||
|
||||
generateTestFolder (path) { |
||||
// Todo move this check to File Manager after refactoring
|
||||
// Checking to ignore the value which contains only whitespaces
|
||||
if (!path || !(/\S/.test(path))) return |
||||
path = helper.removeMultipleSlashes(path) |
||||
const fileProvider = this.fileManager.fileProviderOf(path.split('/')[0]) |
||||
fileProvider.exists(path).then(res => { |
||||
if (!res) fileProvider.createDir(path) |
||||
}) |
||||
} |
||||
|
||||
async pathExists (path) { |
||||
// Checking to ignore the value which contains only whitespaces
|
||||
if (!path || !(/\S/.test(path))) return |
||||
const fileProvider = this.fileManager.fileProviderOf(path.split('/')[0]) |
||||
const res = await fileProvider.exists(path, (e, res) => { return res }) |
||||
return res |
||||
} |
||||
|
||||
async generateTestFile () { |
||||
let fileName = this.fileManager.currentFile() |
||||
const hasCurrent = !!fileName && this.fileManager.currentFile().split('.').pop().toLowerCase() === 'sol' |
||||
if (!hasCurrent) fileName = this.currentPath + '/newFile.sol' |
||||
const fileProvider = this.fileManager.fileProviderOf(this.currentPath) |
||||
if (!fileProvider) return |
||||
const splittedFileName = fileName.split('/') |
||||
const fileNameToImport = (!hasCurrent) ? fileName : this.currentPath + '/' + splittedFileName[splittedFileName.length - 1] |
||||
try { |
||||
const newFile = await helper.createNonClashingNameAsync(fileNameToImport, this.fileManager, '_test') |
||||
if (!await fileProvider.set(newFile, this.generateTestContractSample(hasCurrent, fileName))) { await this.fileManager.open(newFile) } |
||||
await this.fileManager.syncEditor(newFile) |
||||
} catch (error) { |
||||
return modalDialogCustom.alert('Failed to create test file ' + fileNameToImport + ' ' + error) |
||||
} |
||||
} |
||||
|
||||
dirList (path) { |
||||
return this.fileManager.dirList(path) |
||||
} |
||||
|
||||
isRemixDActive () { |
||||
return this.fileManager.isRemixDActive() |
||||
} |
||||
|
||||
async getTests (cb) { |
||||
if (!this.currentPath) return cb(null, []) |
||||
const provider = this.fileManager.fileProviderOf(this.currentPath) |
||||
if (!provider) return cb(null, []) |
||||
const tests = [] |
||||
let files = [] |
||||
try { |
||||
if (await this.fileManager.exists(this.currentPath)) files = await this.fileManager.readdir(this.currentPath) |
||||
} catch (e) { |
||||
cb(e.message) |
||||
} |
||||
for (var file in files) { |
||||
const filepath = provider && provider.type ? provider.type + '/' + file : file |
||||
if (/.(_test.sol)$/.exec(file)) tests.push(filepath) |
||||
} |
||||
cb(null, tests, this.currentPath) |
||||
} |
||||
|
||||
// @todo(#2758): If currently selected file is compiled and compilation result is available,
|
||||
// 'contractName' should be <compiledContractName> + '_testSuite'
|
||||
generateTestContractSample (hasCurrent, fileToImport, contractName = 'testSuite') { |
||||
let relative = remixPath.relative(this.currentPath, remixPath.dirname(fileToImport)) |
||||
if (relative === '') relative = '.' |
||||
const comment = hasCurrent ? `import "${relative}/${remixPath.basename(fileToImport)}";` : '// <import file to test>' |
||||
return `// SPDX-License-Identifier: GPL-3.0
|
||||
|
||||
pragma solidity >=0.4.22 <0.9.0; |
||||
|
||||
// This import is automatically injected by Remix
|
||||
import "remix_tests.sol";
|
||||
|
||||
// This import is required to use custom transaction context
|
||||
// Although it may fail compilation in 'Solidity Compiler' plugin
|
||||
// But it will work fine in 'Solidity Unit Testing' plugin
|
||||
import "remix_accounts.sol"; |
||||
${comment} |
||||
|
||||
// File name has to end with '_test.sol', this file can contain more than one testSuite contracts
|
||||
contract ${contractName} { |
||||
|
||||
/// 'beforeAll' runs before all other tests
|
||||
/// More special functions are: 'beforeEach', 'beforeAll', 'afterEach' & 'afterAll'
|
||||
function beforeAll() public { |
||||
// <instantiate contract>
|
||||
Assert.equal(uint(1), uint(1), "1 should be equal to 1"); |
||||
} |
||||
|
||||
function checkSuccess() public { |
||||
// Use 'Assert' methods: https://remix-ide.readthedocs.io/en/latest/assert_library.html
|
||||
Assert.ok(2 == 2, 'should be true'); |
||||
Assert.greaterThan(uint(2), uint(1), "2 should be greater than to 1"); |
||||
Assert.lesserThan(uint(2), uint(3), "2 should be lesser than to 3"); |
||||
} |
||||
|
||||
function checkSuccess2() public pure returns (bool) { |
||||
// Use the return value (true or false) to test the contract
|
||||
return true; |
||||
} |
||||
|
||||
function checkFailure() public { |
||||
Assert.notEqual(uint(1), uint(1), "1 should not be equal to 1"); |
||||
} |
||||
|
||||
/// Custom Transaction Context: https://remix-ide.readthedocs.io/en/latest/unittesting.html#customization
|
||||
/// #sender: account-1
|
||||
/// #value: 100
|
||||
function checkSenderAndValue() public payable { |
||||
// account index varies 0-9, value is in wei
|
||||
Assert.equal(msg.sender, TestsAccounts.getAccount(1), "Invalid sender"); |
||||
Assert.equal(msg.value, 100, "Invalid value"); |
||||
} |
||||
} |
||||
` |
||||
} |
||||
} |
||||
|
||||
module.exports = TestTabLogic |
@ -1,212 +0,0 @@ |
||||
'use strict' |
||||
var yo = require('yo-yo') |
||||
var csjs = require('csjs-inject') |
||||
var css = csjs` |
||||
.li_tv { |
||||
list-style-type: none; |
||||
-webkit-margin-before: 0px; |
||||
-webkit-margin-after: 0px; |
||||
-webkit-margin-start: 0px; |
||||
-webkit-margin-end: 0px; |
||||
-webkit-padding-start: 0px; |
||||
} |
||||
.ul_tv { |
||||
list-style-type: none; |
||||
-webkit-margin-before: 0px; |
||||
-webkit-margin-after: 0px; |
||||
-webkit-margin-start: 0px; |
||||
-webkit-margin-end: 0px; |
||||
-webkit-padding-start: 0px; |
||||
} |
||||
.caret_tv { |
||||
width: 10px; |
||||
flex-shrink: 0; |
||||
padding-right: 5px; |
||||
} |
||||
.label_item { |
||||
word-break: break-all; |
||||
} |
||||
.label_key { |
||||
min-width: max-content; |
||||
max-width: 80%; |
||||
word-break: break-word; |
||||
} |
||||
.label_value { |
||||
min-width: 10%; |
||||
} |
||||
.cursor_pointer { |
||||
cursor: pointer; |
||||
} |
||||
` |
||||
|
||||
var EventManager = require('../../lib/events') |
||||
|
||||
/** |
||||
* TreeView |
||||
* - extendable by specifying custom `extractData` and `formatSelf` function |
||||
* - trigger `nodeClick` and `leafClick` |
||||
*/ |
||||
class TreeView { |
||||
constructor (opts) { |
||||
this.event = new EventManager() |
||||
this.extractData = opts.extractData || this.extractDataDefault |
||||
this.formatSelf = opts.formatSelf || this.formatSelfDefault |
||||
this.loadMore = opts.loadMore |
||||
this.view = null |
||||
this.expandPath = [] |
||||
} |
||||
|
||||
render (json, expand) { |
||||
var view = this.renderProperties(json, expand) |
||||
if (!this.view) { |
||||
this.view = view |
||||
} |
||||
return view |
||||
} |
||||
|
||||
update (json) { |
||||
if (this.view) { |
||||
yo.update(this.view, this.render(json)) |
||||
} |
||||
} |
||||
|
||||
renderObject (item, parent, key, expand, keyPath) { |
||||
var data = this.extractData(item, parent, key) |
||||
var children = (data.children || []).map((child, index) => { |
||||
return this.renderObject(child.value, data, child.key, expand, keyPath + '/' + child.key) |
||||
}) |
||||
return this.formatData(key, data, children, expand, keyPath) |
||||
} |
||||
|
||||
renderProperties (json, expand, key) { |
||||
key = key || '' |
||||
var children = Object.keys(json).map((innerkey) => { |
||||
return this.renderObject(json[innerkey], json, innerkey, expand, innerkey) |
||||
}) |
||||
return yo`<ul key=${key} data-id="treeViewUl${key}" class="${css.ul_tv} ml-0 px-2">${children}</ul>` |
||||
} |
||||
|
||||
formatData (key, data, children, expand, keyPath) { |
||||
var self = this |
||||
var li = yo`<li key=${keyPath} data-id="treeViewLi${keyPath}" class=${css.li_tv}></li>` |
||||
var caret = yo`<div class="px-1 fas fa-caret-right caret ${css.caret_tv}"></div>` |
||||
var label = yo` |
||||
<div key=${keyPath} data-id="treeViewDiv${keyPath}" class="d-flex flex-row align-items-center"> |
||||
${caret} |
||||
<span class="w-100">${self.formatSelf(key, data, li)}</span> |
||||
</div>` |
||||
const expanded = self.expandPath.includes(keyPath) |
||||
li.appendChild(label) |
||||
if (data.children) { |
||||
var list = yo`<ul key=${keyPath} data-id="treeViewUlList${keyPath}" class="pl-2 ${css.ul_tv}">${children}</ul>` |
||||
list.style.display = expanded ? 'block' : 'none' |
||||
caret.className = list.style.display === 'none' ? `fas fa-caret-right caret ${css.caret_tv}` : `fas fa-caret-down caret ${css.caret_tv}` |
||||
caret.setAttribute('data-id', `treeViewToggle${keyPath}`) |
||||
label.onclick = function () { |
||||
self.expand(keyPath) |
||||
if (self.isExpanded(keyPath)) { |
||||
if (!self.expandPath.includes(keyPath)) self.expandPath.push(keyPath) |
||||
} else { |
||||
self.expandPath = self.expandPath.filter(path => !path.startsWith(keyPath)) |
||||
} |
||||
} |
||||
label.oncontextmenu = function (event) { |
||||
self.event.trigger('nodeRightClick', [keyPath, data, label, event]) |
||||
} |
||||
li.appendChild(list) |
||||
if (data.hasNext) { |
||||
list.appendChild(yo`<li><span class="w-100 text-primary ${css.cursor_pointer}" data-id="treeViewLoadMore" onclick="${() => self.loadMore(data.cursor)}">Load more</span></li>`) |
||||
} |
||||
} else { |
||||
caret.style.visibility = 'hidden' |
||||
label.oncontextmenu = function (event) { |
||||
self.event.trigger('leafRightClick', [keyPath, data, label, event]) |
||||
} |
||||
label.onclick = function (event) { |
||||
self.event.trigger('leafClick', [keyPath, data, label, event]) |
||||
} |
||||
} |
||||
return li |
||||
} |
||||
|
||||
isExpanded (path) { |
||||
var current = this.nodeAt(path) |
||||
if (current) { |
||||
return current.style.display !== 'none' |
||||
} |
||||
return false |
||||
} |
||||
|
||||
expand (path) { |
||||
var caret = this.caretAt(path) |
||||
var node = this.nodeAt(path) |
||||
if (node) { |
||||
node.style.display = node.style.display === 'none' ? 'block' : 'none' |
||||
caret.className = node.style.display === 'none' ? `fas fa-caret-right caret ${css.caret_tv}` : `fas fa-caret-down caret ${css.caret_tv}` |
||||
this.event.trigger('nodeClick', [path, node]) |
||||
} |
||||
} |
||||
|
||||
caretAt (path) { |
||||
var label = this.labelAt(path) |
||||
if (label) { |
||||
return label.querySelector('.caret') |
||||
} |
||||
} |
||||
|
||||
itemAt (path) { |
||||
return this.view.querySelector(`li[key="${path}"]`) |
||||
} |
||||
|
||||
labelAt (path) { |
||||
return this.view.querySelector(`div[key="${path}"]`) |
||||
} |
||||
|
||||
nodeAt (path) { |
||||
return this.view.querySelector(`ul[key="${path}"]`) |
||||
} |
||||
|
||||
updateNodeFromJSON (path, jsonTree, expand) { |
||||
var newTree = this.renderProperties(jsonTree, expand, path) |
||||
var current = this.nodeAt(path) |
||||
if (current && current.parentElement) { |
||||
current.parentElement.replaceChild(newTree, current) |
||||
} |
||||
} |
||||
|
||||
formatSelfDefault (key, data) { |
||||
return yo` |
||||
<div class="d-flex mt-2 flex-row ${css.label_item}"> |
||||
<label class="small font-weight-bold pr-1 ${css.label_key}">${key}:</label> |
||||
<label class="m-0 ${css.label_value}">${data.self}</label> |
||||
</div> |
||||
` |
||||
} |
||||
|
||||
extractDataDefault (item, parent, key) { |
||||
var ret = {} |
||||
if (item instanceof Array) { |
||||
ret.children = item.map((item, index) => { |
||||
return { key: index, value: item } |
||||
}) |
||||
ret.self = 'Array' |
||||
ret.isNode = true |
||||
ret.isLeaf = false |
||||
} else if (item instanceof Object) { |
||||
ret.children = Object.keys(item).map((key) => { |
||||
return { key: key, value: item[key] } |
||||
}) |
||||
ret.self = 'Object' |
||||
ret.isNode = true |
||||
ret.isLeaf = false |
||||
} else { |
||||
ret.self = item |
||||
ret.children = null |
||||
ret.isNode = false |
||||
ret.isLeaf = true |
||||
} |
||||
return ret |
||||
} |
||||
} |
||||
|
||||
module.exports = TreeView |
@ -1,212 +0,0 @@ |
||||
var yo = require('yo-yo') |
||||
var remixLib = require('@remix-project/remix-lib') |
||||
var EventManager = remixLib.EventManager |
||||
var Commands = require('../../lib/commands') |
||||
|
||||
// -------------- styling ----------------------
|
||||
var css = require('./styles/auto-complete-popup-styles') |
||||
|
||||
/* USAGE: |
||||
|
||||
var autoCompletePopup = new AutoCompletePopup({ |
||||
options: [] |
||||
}) |
||||
autoCompletePopup.event.register('handleSelect', function (input) { }) |
||||
autoCompletePopup.event.register('updateList', function () { }) |
||||
|
||||
*/ |
||||
|
||||
class AutoCompletePopup { |
||||
constructor (opts = {}) { |
||||
var self = this |
||||
self.event = new EventManager() |
||||
self.isOpen = false |
||||
self.opts = opts |
||||
self.data = { |
||||
_options: [] |
||||
} |
||||
self._components = {} |
||||
self._view = null |
||||
self._startingElement = 0 |
||||
self._elementsToShow = 4 |
||||
self._selectedElement = 0 |
||||
this.extraCommands = [] |
||||
} |
||||
|
||||
render () { |
||||
var self = this |
||||
const autoComplete = yo` |
||||
<div class="${css.popup} alert alert-secondary"> |
||||
<div> |
||||
${self.data._options.map((item, index) => { |
||||
return yo` |
||||
<div data-id="autoCompletePopUpAutoCompleteItem" class="${css.autoCompleteItem} ${css.listHandlerHide} item ${self._selectedElement === index ? 'border border-primary' : ''}"> |
||||
<div value=${index} onclick=${(event) => { self.handleSelect(event.srcElement.innerText) }}> |
||||
${getKeyOf(item)}
|
||||
</div> |
||||
<div> |
||||
${getValueOf(item)} |
||||
</div> |
||||
</div> |
||||
` |
||||
})} |
||||
</div> |
||||
<div class="${css.listHandlerHide}"> |
||||
<div class="${css.pageNumberAlignment}">Page ${(self._startingElement / self._elementsToShow) + 1} of ${Math.ceil(self.data._options.length / self._elementsToShow)}</div> |
||||
</div> |
||||
</div> |
||||
` |
||||
function setUpPopUp (autoComplete) { |
||||
handleOpenPopup(autoComplete) |
||||
handleListSize(autoComplete) |
||||
} |
||||
|
||||
function handleOpenPopup (autoComplete) { |
||||
autoComplete.style.display = self.data._options.length > 0 ? 'block' : 'none' |
||||
} |
||||
|
||||
function handleListSize (autoComplete) { |
||||
if (self.data._options.length >= self._startingElement) { |
||||
for (let i = self._startingElement; i < (self._elementsToShow + self._startingElement); i++) { |
||||
const el = autoComplete.querySelectorAll('.item')[i] |
||||
if (el) { |
||||
el.classList.remove(css.listHandlerHide) |
||||
el.classList.add(css.listHandlerShow) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
setUpPopUp(autoComplete) |
||||
if (!this._view) this._view = autoComplete |
||||
return autoComplete |
||||
} |
||||
|
||||
handleSelect (text) { |
||||
this.removeAutoComplete() |
||||
this.event.trigger('handleSelect', [text]) |
||||
} |
||||
|
||||
moveUp () { |
||||
if (this._selectedElement === 0) return |
||||
this._selectedElement-- |
||||
this._startingElement = this._selectedElement > 0 ? this._selectedElement - 1 : 0 |
||||
this.event.trigger('updateList') |
||||
yo.update(this._view, this.render()) |
||||
} |
||||
|
||||
moveDown () { |
||||
if (this.data._options.length <= this._selectedElement + 1) return |
||||
this._selectedElement++ |
||||
this._startingElement = this._selectedElement - 1 |
||||
this.event.trigger('updateList') |
||||
yo.update(this._view, this.render()) |
||||
} |
||||
|
||||
handleAutoComplete (event, inputString) { |
||||
if (this.isOpen && (event.which === 27 || event.which === 8 || event.which === 46)) { |
||||
// backspace or any key that should remove the autocompletion
|
||||
this.removeAutoComplete() |
||||
return true |
||||
} |
||||
if (this.isOpen && (event.which === 13 || event.which === 9)) { |
||||
// enter and tab (validate completion)
|
||||
event.preventDefault() |
||||
if (this.data._options[this._selectedElement]) { |
||||
this.handleSelect(getKeyOf(this.data._options[this._selectedElement])) |
||||
} |
||||
this.removeAutoComplete() |
||||
return true |
||||
} |
||||
if (this.isOpen && event.which === 38) { |
||||
// move up
|
||||
event.preventDefault() |
||||
this.isOpen = true |
||||
this.moveUp() |
||||
return true |
||||
} |
||||
if (this.isOpen && event.which === 40) { |
||||
// move down
|
||||
event.preventDefault() |
||||
this.isOpen = true |
||||
this.moveDown() |
||||
return true |
||||
} |
||||
if (event.which === 13 || event.which === 9) { |
||||
// enter || tab and autocompletion is off, just returning false
|
||||
return false |
||||
} |
||||
const textList = inputString.split(' ') |
||||
const autoCompleteInput = textList.length > 1 ? textList[textList.length - 1] : textList[0] |
||||
if (inputString.length >= 2) { |
||||
// more than 2 letters, start completion
|
||||
this.data._options = [] |
||||
Commands.allPrograms.forEach(item => { |
||||
const program = getKeyOf(item) |
||||
if (program.substring(0, program.length - 1).includes(autoCompleteInput.trim())) { |
||||
this.data._options.push(item) |
||||
} else if (autoCompleteInput.trim().includes(program) || (program === autoCompleteInput.trim())) { |
||||
Commands.allCommands.forEach(item => { |
||||
const command = getKeyOf(item) |
||||
if (command.includes(autoCompleteInput.trim())) { |
||||
this.data._options.push(item) |
||||
} |
||||
}) |
||||
} |
||||
}) |
||||
this.extraCommands.forEach(item => { |
||||
const command = getKeyOf(item) |
||||
if (command.includes(autoCompleteInput.trim())) { |
||||
this.data._options.push(item) |
||||
} |
||||
}) |
||||
|
||||
if (this.data._options.length === 1 && event.which === 9) { |
||||
// if only one option and tab is pressed, we resolve it
|
||||
event.preventDefault() |
||||
textList.pop() |
||||
textList.push(getKeyOf(this.data._options[0])) |
||||
this.handleSelect(`${textList}`.replace(/,/g, ' ')) |
||||
this.removeAutoComplete() |
||||
return |
||||
} |
||||
if (this.data._options.length) this.isOpen = true |
||||
yo.update(this._view, this.render()) |
||||
return true |
||||
} |
||||
return false |
||||
} |
||||
|
||||
removeAutoComplete () { |
||||
if (!this.isOpen) return |
||||
this._view.style.display = 'none' |
||||
this.isOpen = false |
||||
this.data._options = [] |
||||
this._startingElement = 0 |
||||
this._selectedElement = 0 |
||||
yo.update(this._view, this.render()) |
||||
} |
||||
|
||||
extendAutocompletion () { |
||||
// TODO: this is not using the appManager interface. Terminal should be put as module
|
||||
this.opts.appManager.event.on('activate', async (profile) => { |
||||
if (!profile.methods) return |
||||
profile.methods.forEach((method) => { |
||||
const key = `remix.call('${profile.name}', '${method}')` |
||||
const keyValue = {} |
||||
keyValue[key] = `call ${profile.name} - ${method}` |
||||
if (this.extraCommands.includes(keyValue)) return |
||||
this.extraCommands.push(keyValue) |
||||
}) |
||||
}) |
||||
} |
||||
} |
||||
|
||||
function getKeyOf (item) { |
||||
return Object.keys(item)[0] |
||||
} |
||||
|
||||
function getValueOf (item) { |
||||
return Object.values(item)[0] |
||||
} |
||||
|
||||
module.exports = AutoCompletePopup |
@ -1,70 +0,0 @@ |
||||
var yo = require('yo-yo') |
||||
var csjs = require('csjs-inject') |
||||
var EventManager = require('../../lib/events') |
||||
|
||||
module.exports = class Card { |
||||
constructor (api, events, opts) { |
||||
const self = this |
||||
self._api = api |
||||
self._events = events |
||||
self._opts = opts |
||||
self._view = {} |
||||
self.event = new EventManager() |
||||
} |
||||
|
||||
render () { |
||||
const self = this |
||||
if (self._view.el) return self._view.el |
||||
|
||||
self._view.cardBody = yo`<div></div>` |
||||
self._view.arrow = yo`<i class="${css.arrow} fas fa-angle-down" onclick="${() => trigger(this)}"></i>` |
||||
|
||||
self._view.expandCollapseButton = yo` |
||||
<div>${self._view.arrow}</div>` |
||||
|
||||
self._view.statusBar = yo`<div>${self._opts.collapsedView}</div>` |
||||
self._view.cardHeader = yo` |
||||
<div class="d-flex justify-content-between align-items-center" onclick=${() => trigger(self._view.arrow)}> |
||||
<div class="pr-1 d-flex flex-row"> |
||||
<div>${self._opts.title}</div> |
||||
${self._view.statusBar} |
||||
</div> |
||||
<div>${self._view.expandCollapseButton}</div> |
||||
</div>` |
||||
|
||||
function trigger (el) { |
||||
var body = self._view.cardBody |
||||
var status = self._view.statusBar |
||||
if (el.classList) { |
||||
el.classList.toggle('fa-angle-up') |
||||
var arrow = el.classList.toggle('fa-angle-down') ? 'up' : 'down' |
||||
self.event.trigger('expandCollapseCard', [arrow, body, status]) |
||||
} |
||||
} |
||||
|
||||
// HTML
|
||||
self._view.el = yo` |
||||
<div class="${css.cardContainer} list-group-item border-0"> |
||||
${self._view.cardHeader} |
||||
${self._view.cardBody} |
||||
</div>` |
||||
|
||||
return self._view.el |
||||
} |
||||
} |
||||
|
||||
const css = csjs` |
||||
.cardContainer { |
||||
padding : 0 24px 16px; |
||||
margin : 0; |
||||
background : none; |
||||
} |
||||
.arrow { |
||||
font-weight : bold; |
||||
cursor : pointer; |
||||
font-size : 14px; |
||||
} |
||||
.arrow:hover { |
||||
} |
||||
|
||||
` |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue