Merge branch 'master' into desktope2e-remixai

pull/5127/head
STetsing 3 months ago committed by GitHub
commit 108c41428b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 15
      .circleci/config.yml
  2. 3
      apps/quick-dapp/.eslintrc
  3. 1
      apps/quick-dapp/README.md
  4. 11
      apps/quick-dapp/package.json
  5. 70
      apps/quick-dapp/project.json
  6. 127
      apps/quick-dapp/src/App.css
  7. 71
      apps/quick-dapp/src/App.tsx
  8. 426
      apps/quick-dapp/src/actions/index.ts
  9. BIN
      apps/quick-dapp/src/assets/edit-dapp.png
  10. 132
      apps/quick-dapp/src/components/ContractGUI/index.tsx
  11. 107
      apps/quick-dapp/src/components/CreateInstance/index.tsx
  12. 329
      apps/quick-dapp/src/components/DeployPanel/index.tsx
  13. 159
      apps/quick-dapp/src/components/DeployPanel/theme.tsx
  14. 86
      apps/quick-dapp/src/components/EditInstance/index.tsx
  15. 76
      apps/quick-dapp/src/components/EditableText/index.tsx
  16. 50
      apps/quick-dapp/src/components/ImageUpload/index.tsx
  17. 31
      apps/quick-dapp/src/components/LoadingScreen/index.tsx
  18. 70
      apps/quick-dapp/src/components/MultipleContainers/components/Container/Container.tsx
  19. 13
      apps/quick-dapp/src/components/MultipleContainers/components/Container/Remove.tsx
  20. 2
      apps/quick-dapp/src/components/MultipleContainers/components/Container/index.ts
  21. 29
      apps/quick-dapp/src/components/MultipleContainers/components/Item/Action.tsx
  22. 18
      apps/quick-dapp/src/components/MultipleContainers/components/Item/Handle.tsx
  23. 112
      apps/quick-dapp/src/components/MultipleContainers/components/Item/Item.tsx
  24. 3
      apps/quick-dapp/src/components/MultipleContainers/components/Item/index.ts
  25. 3
      apps/quick-dapp/src/components/MultipleContainers/components/index.ts
  26. 669
      apps/quick-dapp/src/components/MultipleContainers/index.tsx
  27. 153
      apps/quick-dapp/src/components/MultipleContainers/multipleContainersKeyboardCoordinates.ts
  28. 3
      apps/quick-dapp/src/contexts/index.ts
  29. 13
      apps/quick-dapp/src/index.css
  30. 16
      apps/quick-dapp/src/index.html
  31. 9
      apps/quick-dapp/src/main.tsx
  32. 7
      apps/quick-dapp/src/polyfills.ts
  33. 19
      apps/quick-dapp/src/profile.json
  34. 33
      apps/quick-dapp/src/reducers/state.ts
  35. 24
      apps/quick-dapp/src/remix-client.ts
  36. 23
      apps/quick-dapp/tsconfig.app.json
  37. 16
      apps/quick-dapp/tsconfig.json
  38. 90
      apps/quick-dapp/webpack.config.js
  39. 70
      apps/quick-dapp/yarn.lock
  40. 19
      apps/remix-ide-e2e/src/commands/pinGrid.ts
  41. 27
      apps/remix-ide-e2e/src/commands/renamePath.ts
  42. 34
      apps/remix-ide-e2e/src/commands/switchEnvironment.ts
  43. 16
      apps/remix-ide-e2e/src/tests/dgit_local.test.ts
  44. 20
      apps/remix-ide-e2e/src/tests/grid.test.ts
  45. 8
      apps/remix-ide-e2e/src/tests/plugin_api.ts
  46. 322
      apps/remix-ide-e2e/src/tests/quickDapp.test.ts
  47. 80
      apps/remix-ide-e2e/src/tests/remixd.test.ts
  48. 5
      apps/remix-ide-e2e/src/tests/runAndDeploy.test.ts
  49. 6
      apps/remix-ide-e2e/yarn.lock
  50. 2
      apps/remix-ide/project.json
  51. 42
      apps/remix-ide/src/app/tabs/locales/en/quickDapp.json
  52. BIN
      apps/remix-ide/src/assets/img/quickDappLogo.webp
  53. 18
      apps/remix-ide/src/blockchain/blockchain.tsx
  54. 2
      apps/remix-ide/src/remixAppManager.js
  55. 2
      libs/remix-ui/run-tab/src/lib/components/environment.tsx
  56. 1
      libs/remix-ui/run-tab/src/lib/components/universalDappUI.tsx
  57. 10
      libs/remix-ui/run-tab/src/lib/run-tab.tsx
  58. 2
      libs/remix-ui/tabs/src/lib/remix-ui-tabs.tsx
  59. 3
      libs/remix-ui/terminal/src/lib/remix-ui-terminal.tsx

@ -16,6 +16,7 @@ jobs:
xlarge
working_directory: ~/remix-project
steps:
- run: sudo apt update && sudo apt install zstd
- checkout
- restore_cache:
keys:
@ -44,8 +45,8 @@ jobs:
key: soljson-v7-{{ checksum "soljson-versions.txt" }}
paths:
- dist/apps/remix-ide/assets/js/soljson
- run: mkdir persist && zip -0 -r persist/dist.zip dist
- run: mkdir persist && tar -cf - dist | zstd -1 -o persist/dist.tar.zst
- persist_to_workspace:
root: .
paths:
@ -607,11 +608,13 @@ jobs:
- checkout
- attach_workspace:
at: .
- run: unzip ./persist/dist.zip
- run: sudo apt update && sudo apt install python3-pip -y zstd
- run: zstd -d persist/dist.tar.zst -o persist/dist.tar
- run: tar -xf persist/dist.tar
- run: yarn install --cwd ./apps/remix-ide-e2e --modules-folder ../../node_modules || yarn install --cwd ./apps/remix-ide-e2e --modules-folder ../../node_modules
- run: mkdir node_modules/hardhat && wget https://unpkg.com/hardhat/console.sol -O node_modules/hardhat/console.sol
- run: ls -la ./dist/apps/remix-ide/assets/js
- run: sudo apt update && sudo apt install python3-pip -y
- when:
condition:
equal: [ "chrome", << parameters.browser >> ]
@ -663,7 +666,9 @@ jobs:
- checkout
- attach_workspace:
at: .
- run: unzip ./persist/dist.zip
- run: sudo apt update && sudo apt install zstd
- run: zstd -d persist/dist.tar.zst -o persist/dist.tar
- run: tar -xf persist/dist.tar
- run: unzip ./persist/plugin-<< parameters.plugin >>.zip
- run: yarn install --cwd ./apps/remix-ide-e2e --modules-folder ../../node_modules || yarn install --cwd ./apps/remix-ide-e2e --modules-folder ../../node_modules
- browser-tools/install-browser-tools:

@ -0,0 +1,3 @@
{
"extends": "../../.eslintrc.json",
}

@ -0,0 +1 @@
# Remix QuickDapp Plugin

@ -0,0 +1,11 @@
{
"name": "quick-dapp",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"dependencies": {
"@dnd-kit/core": "^6.1.0",
"@dnd-kit/sortable": "^8.0.0",
"@drafish/surge-client": "^1.1.5"
}
}

@ -0,0 +1,70 @@
{
"name": "quick-dapp",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "apps/quick-dapp/src",
"projectType": "application",
"implicitDependencies": [],
"targets": {
"build": {
"executor": "@nrwl/webpack:webpack",
"outputs": ["{options.outputPath}"],
"defaultConfiguration": "development",
"dependsOn": ["install"],
"options": {
"compiler": "babel",
"outputPath": "dist/apps/quick-dapp",
"index": "apps/quick-dapp/src/index.html",
"baseHref": "./",
"main": "apps/quick-dapp/src/main.tsx",
"polyfills": "apps/quick-dapp/src/polyfills.ts",
"tsConfig": "apps/quick-dapp/tsconfig.app.json",
"assets": ["apps/quick-dapp/src/profile.json", "apps/quick-dapp/src/assets/edit-dapp.png"],
"styles": ["apps/quick-dapp/src/index.css"],
"scripts": [],
"webpackConfig": "apps/quick-dapp/webpack.config.js"
},
"configurations": {
"development": {
},
"production": {
}
}
},
"lint": {
"executor": "@nrwl/linter:eslint",
"outputs": ["{options.outputFile}"],
"options": {
"lintFilePatterns": ["apps/quick-dapp/**/*.ts"],
"eslintConfig": "apps/quick-dapp/.eslintrc"
}
},
"install": {
"executor": "nx:run-commands",
"options": {
"commands": [
"cd apps/quick-dapp && yarn"
],
"parallel": false
}
},
"serve": {
"executor": "@nrwl/webpack:dev-server",
"defaultConfiguration": "development",
"options": {
"buildTarget": "quick-dapp:build",
"hmr": true,
"baseHref": "/"
},
"configurations": {
"development": {
"buildTarget": "quick-dapp:build:development",
"port": 2025
},
"production": {
"buildTarget": "quick-dapp:build:production"
}
}
}
},
"tags": []
}

@ -0,0 +1,127 @@
/* You can add global styles to this file, and also import other style files */
.item-wrapper {
transform: translate3d(var(--translate-x, 0), var(--translate-y, 0), 0)
scaleX(var(--scale-x, 1)) scaleY(var(--scale-y, 1));
transform-origin: 0 0;
touch-action: manipulation;
&:hover {
.item-remove {
visibility: visible;
}
}
}
.item-remove {
visibility: hidden;
top: 5px;
right: 5px;
width: 20px;
height: 20px;
background-color: var(--gray-dark);
.fas {
color: var(--text-bg-mark);
}
}
.item-action {
touch-action: none;
outline: none !important;
appearance: none;
background-color: transparent;
-webkit-tap-highlight-color: transparent;
@media (hover: hover) {
&:hover {
background-color: var(--light);
}
}
.fas {
color: var(--text-bg-mark);
}
}
.bg-light {
.item-action {
@media (hover: hover) {
&:hover {
background-color: var(--dark);
}
}
}
}
.container {
flex-direction: column;
&.placeholder {
justify-content: center;
align-items: center;
cursor: pointer;
}
}
.container-header {
&:hover {
.container-actions > * {
opacity: 1 !important;
}
}
}
.container-actions {
> *:first-child:not(:last-child) {
opacity: 0;
&:focus-visible {
opacity: 1;
}
}
}
.instance-input {
background-color: var(--custom-select) !important;
font-size: 10px;
}
.has-args {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.udapp_intro {
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
white-space: pre-wrap;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.udapp_intro:hover {
-webkit-line-clamp: inherit;
}
.cursor_pointer {
cursor: pointer;
}
.cursor_pointer:hover {
color: var(--secondary);
}
.custom-dropdown-items {
padding: 0.25rem 0.25rem;
border-radius: .25rem;
background: var(--custom-select);
}
.custom-dropdown-items a {
border-radius: .25rem;
text-transform: none;
text-decoration: none;
font-weight: normal;
font-size: 0.875rem;
padding: 0.25rem 0.25rem;
width: auto;
color: var(--text);
}

@ -0,0 +1,71 @@
import React, { useEffect, useReducer, useState } from 'react';
import { IntlProvider } from 'react-intl'
import CreateInstance from './components/CreateInstance';
import EditInstance from './components/EditInstance';
import DeployPanel from './components/DeployPanel';
import LoadingScreen from './components/LoadingScreen';
import { appInitialState, appReducer } from './reducers/state';
import {
connectRemix,
initDispatch,
updateState,
selectTheme,
} from './actions';
import { AppContext } from './contexts';
import remixClient from './remix-client';
import './App.css';
function App(): JSX.Element {
const [locale, setLocale] = useState<{code: string; messages: any}>({
code: 'en',
messages: null,
})
const [appState, dispatch] = useReducer(appReducer, appInitialState);
useEffect(() => {
updateState(appState);
}, [appState]);
useEffect(() => {
initDispatch(dispatch);
updateState(appState);
connectRemix().then(() => {
remixClient.call('theme', 'currentTheme').then((theme: any) => {
selectTheme(theme.name);
});
remixClient.on('theme', 'themeChanged', (theme: any) => {
selectTheme(theme.name);
});
// @ts-ignore
remixClient.call('locale', 'currentLocale').then((locale: any) => {
setLocale(locale)
})
// @ts-ignore
remixClient.on('locale', 'localeChanged', (locale: any) => {
setLocale(locale)
})
});
}, []);
return (
<AppContext.Provider
value={{
dispatch,
appState,
}}
>
<IntlProvider locale={locale.code} messages={locale.messages}>
{Object.keys(appState.instance.abi).length > 0 ? (
<div className="row m-0 pt-3">
<EditInstance />
<DeployPanel />
</div>
) : (
<div className="row m-0 pt-3">
<CreateInstance />
</div>
)}
<LoadingScreen />
</IntlProvider>
</AppContext.Provider>
);
}
export default App;

@ -0,0 +1,426 @@
import axios from 'axios';
import { omitBy } from 'lodash';
import semver from 'semver';
import { execution } from '@remix-project/remix-lib';
import SurgeClient from '@drafish/surge-client';
import remixClient from '../remix-client';
import { themeMap } from '../components/DeployPanel/theme';
const { encodeFunctionId } = execution.txHelper;
const surgeClient = new SurgeClient({
// surge backend doesn't support cross-domain, that's why the proxy goes
// here is the codebase of proxy: https://github.com/drafish/vercel-proxy
proxy: 'https://vercel-proxy-bice-six.vercel.app',
onError: (err: Error) => {
console.log(err);
},
});
const getVersion = (solcVersion) => {
let version = '0.8.25'
try {
const arr = solcVersion.split('+')
if (arr && arr[0]) version = arr[0]
if (semver.lt(version, '0.6.0')) {
return { version: version, canReceive: false };
} else {
return { version: version, canReceive: true };
}
} catch (e) {
return { version, canReceive: true };
}
};
let dispatch: any, state: any;
export const initDispatch = (_dispatch: any) => {
dispatch = _dispatch;
};
export const updateState = (_state: any) => {
state = _state;
};
export const connectRemix = async () => {
await dispatch({
type: 'SET_LOADING',
payload: {
screen: true,
},
});
await remixClient.onload();
// @ts-expect-error
await remixClient.call('layout', 'minimize', 'terminal', true);
await dispatch({
type: 'SET_LOADING',
payload: {
screen: false,
},
});
};
export const saveDetails = async (payload: any) => {
const { abi, userInput, natSpec } = state.instance;
await dispatch({
type: 'SET_INSTANCE',
payload: {
abi: {
...abi,
[payload.id]: {
...abi[payload.id],
details:
natSpec.checked && !payload.details
? natSpec.methods[payload.id]
: payload.details,
},
},
userInput: {
...omitBy(userInput, (item) => item === ''),
methods: omitBy(
{
...userInput.methods,
[payload.id]: payload.details,
},
(item) => item === ''
),
},
},
});
};
export const saveTitle = async (payload: any) => {
const { abi } = state.instance;
await dispatch({
type: 'SET_INSTANCE',
payload: {
abi: {
...abi,
[payload.id]: { ...abi[payload.id], title: payload.title },
},
},
});
};
export const getInfoFromNatSpec = async (value: boolean) => {
const { abi, userInput, natSpec } = state.instance;
const input = value
? {
...natSpec,
...userInput,
methods: { ...natSpec.methods, ...userInput.methods },
}
: userInput;
Object.keys(abi).forEach((id) => {
abi[id].details = input.methods[id] || '';
});
await dispatch({
type: 'SET_INSTANCE',
payload: {
abi,
title: input.title || '',
details: input.details || '',
natSpec: { ...natSpec, checked: value },
},
});
};
export const deploy = async (payload: any, callback: any) => {
const surgeToken = localStorage.getItem('__SURGE_TOKEN');
const surgeEmail = localStorage.getItem('__SURGE_EMAIL');
let isLogin = false;
if (surgeToken && surgeEmail === payload.email) {
try {
await surgeClient.whoami();
isLogin = true;
} catch (error) {
/* empty */
}
}
if (!isLogin) {
try {
await surgeClient.login({
user: payload.email,
password: payload.password,
});
localStorage.setItem('__SURGE_EMAIL', payload.email);
localStorage.setItem('__SURGE_PASSWORD', payload.password);
localStorage.setItem('__DISQUS_SHORTNAME', payload.shortname);
} catch (error: any) {
callback({ code: 'ERROR', error: error.message });
return;
}
}
const { data } = await axios.get(
// It's the json file contains all the static files paths of dapp-template.
// It's generated through the build process automatically.
'https://dev.remix-dapp.pages.dev/manifest.json'
);
let paths: any = [];
Object.keys(data).forEach((key) => {
if (data[key].src === 'index.html') {
const { src, file, css, assets } = data[key];
paths = paths.concat([src, file, ...css, ...assets]);
} else {
paths.push(data[key].file);
}
});
const { logo, ...instance } = state.instance;
const instanceJson = JSON.stringify({
...instance,
shortname: payload.shortname,
shareTo: payload.shareTo,
})
const files: Record<string, string> = {
'dir/instance.json': instanceJson,
};
// console.log(
// JSON.stringify({
// ...instance,
// shareTo: payload.shareTo,
// })
// );
for (let index = 0; index < paths.length; index++) {
const path = paths[index];
// download all the static files from the dapp-template domain.
// here is the codebase of dapp-template: https://github.com/drafish/remix-dapp
const resp = await axios.get(`https://dev.remix-dapp.pages.dev/${path}`);
files[`dir/${path}`] = resp.data;
}
files['dir/logo.png'] = logo
files['dir/CORS'] = '*'
files['dir/index.html'] = files['dir/index.html'].replace(
'assets/css/themes/remix-dark_tvx1s2.css',
themeMap[instance.theme].url
);
try {
await surgeClient.publish({
files,
domain: `${payload.subdomain}.surge.sh`,
onProgress: ({
id,
progress,
file,
}: {
id: string;
progress: number;
file: string;
}) => {
// console.log({ id, progress, file });
},
onTick: (tick: string) => {},
});
} catch ({ message }: any) {
if (message === '403') {
callback({ code: 'ERROR', error: 'this domain belongs to someone else' });
} else {
callback({ code: 'ERROR', error: 'gateway timeout, please try again' });
}
return;
}
try {
// some times deployment might fail even if it says successfully, that's why we need to do the double check.
const instanceResp = await axios.get(`https://${payload.subdomain}.surge.sh/instance.json`);
if (instanceResp.status === 200 && JSON.stringify(instanceResp.data) === instanceJson) {
callback({ code: 'SUCCESS', error: '' });
return;
}
} catch (error) {}
callback({ code: 'ERROR', error: 'deploy failed, please try again' });
return;
};
export const teardown = async (payload: any, callback: any) => {
const surgeToken = localStorage.getItem('__SURGE_TOKEN');
const surgeEmail = localStorage.getItem('__SURGE_EMAIL');
let isLogin = false;
if (surgeToken && surgeEmail === payload.email) {
try {
await surgeClient.whoami();
isLogin = true;
} catch (error) {
/* empty */
}
}
if (!isLogin) {
try {
await surgeClient.login({
user: payload.email,
password: payload.password,
});
localStorage.setItem('__SURGE_EMAIL', payload.email);
localStorage.setItem('__SURGE_PASSWORD', payload.password);
localStorage.setItem('__DISQUS_SHORTNAME', payload.shortname);
} catch (error: any) {
callback({ code: 'ERROR', error: error.message });
return;
}
}
try {
await surgeClient.teardown(`${payload.subdomain}.surge.sh`);
} catch ({ message }: any) {
if (message === '403') {
callback({ code: 'ERROR', error: 'this domain belongs to someone else' });
} else {
callback({ code: 'ERROR', error: 'gateway timeout, please try again' });
}
return;
}
callback({ code: 'SUCCESS', error: '' });
return;
}
export const initInstance = async ({
methodIdentifiers,
devdoc,
solcVersion,
...payload
}: any) => {
const functionHashes: any = {};
const natSpec: any = { checked: false, methods: {} };
if (methodIdentifiers && devdoc) {
for (const fun in methodIdentifiers) {
functionHashes[`0x${methodIdentifiers[fun]}`] = fun;
}
natSpec.title = devdoc.title;
natSpec.details = devdoc.details;
Object.keys(functionHashes).forEach((hash) => {
const method = functionHashes[hash];
if (devdoc.methods[method]) {
const { details, params, returns } = devdoc.methods[method];
const detailsStr = details ? `@dev ${details}` : '';
const paramsStr = params
? Object.keys(params)
.map((key) => `@param ${key} ${params[key]}`)
.join('\n')
: '';
const returnsStr = returns
? Object.keys(returns)
.map(
(key) =>
`@return${/^_\d$/.test(key) ? '' : ' ' + key} ${returns[key]}`
)
.join('\n')
: '';
natSpec.methods[hash] = [detailsStr, paramsStr, returnsStr]
.filter((str) => str !== '')
.join('\n');
}
});
}
const abi: any = {};
const lowLevel: any = {}
payload.abi.forEach((item: any) => {
if (item.type === 'function') {
item.id = encodeFunctionId(item);
abi[item.id] = item;
}
if (item.type === 'fallback') {
lowLevel.fallback = item;
}
if (item.type === 'receive') {
lowLevel.receive = item;
}
});
const ids = Object.keys(abi);
const items =
ids.length > 2
? {
A: ids.slice(0, ids.length / 2 + 1),
B: ids.slice(ids.length / 2 + 1),
}
: { A: ids };
const logo = await axios.get('https://dev.remix-dapp.pages.dev/logo.png', { responseType: 'arraybuffer' })
await dispatch({
type: 'SET_INSTANCE',
payload: {
...payload,
abi,
items,
containers: Object.keys(items),
natSpec,
solcVersion: getVersion(solcVersion),
...lowLevel,
logo: logo.data,
},
});
};
export const resetInstance = async () => {
const abi = state.instance.abi;
const ids = Object.keys(abi);
ids.forEach((id) => {
abi[id] = { ...abi[id], title: '', details: '' };
});
const items =
ids.length > 1
? {
A: ids.slice(0, ids.length / 2 + 1),
B: ids.slice(ids.length / 2 + 1),
}
: { A: ids };
await dispatch({
type: 'SET_INSTANCE',
payload: {
items,
containers: Object.keys(items),
title: '',
details: '',
abi,
},
});
};
export const emptyInstance = async () => {
await dispatch({
type: 'SET_INSTANCE',
payload: {
name: '',
address: '',
network: '',
abi: {},
items: {},
containers: [],
title: '',
details: '',
theme: 'Dark',
userInput: { methods: {} },
natSpec: { checked: false, methods: {} },
},
});
};
export const selectTheme = async (selectedTheme: string) => {
await dispatch({ type: 'SET_INSTANCE', payload: { theme: selectedTheme } });
const linkEles = document.querySelectorAll('link');
const nextTheme = themeMap[selectedTheme]; // Theme
for (const link of linkEles) {
if (link.href.indexOf('/assets/css/themes/') > 0) {
link.href = 'https://remix.ethereum.org/' + nextTheme.url;
document.documentElement.style.setProperty('--theme', nextTheme.quality);
break;
}
}
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

@ -0,0 +1,132 @@
import React, { useEffect, useState } from 'react';
import { useIntl } from 'react-intl';
import { execution } from '@remix-project/remix-lib';
import { saveDetails, saveTitle } from '../../actions';
const txHelper = execution.txHelper;
const getFuncABIInputs = (funABI: any) => {
if (!funABI.inputs) {
return '';
}
return txHelper.inputParametersDeclarationToString(funABI.inputs);
};
export function ContractGUI(props: { funcABI: any, funcId: any }) {
const intl = useIntl()
const isConstant =
props.funcABI.constant !== undefined ? props.funcABI.constant : false;
const lookupOnly =
props.funcABI.stateMutability === 'view' ||
props.funcABI.stateMutability === 'pure' ||
isConstant;
const inputs = getFuncABIInputs(props.funcABI);
const [title, setTitle] = useState<string>('');
const [buttonOptions, setButtonOptions] = useState<{
title: string;
content: string;
classList: string;
dataId: string;
}>({ title: '', content: '', classList: '', dataId: '' });
useEffect(() => {
if (props.funcABI.name) {
setTitle(props.funcABI.name);
} else {
setTitle(props.funcABI.type === 'receive' ? '(receive)' : '(fallback)');
}
}, [props.funcABI]);
useEffect(() => {
if (lookupOnly) {
setButtonOptions({
title: title + ' - call',
content: 'call',
classList: 'btn-info',
dataId: title + ' - call',
});
} else if (
props.funcABI.stateMutability === 'payable' ||
props.funcABI.payable
) {
setButtonOptions({
title: title + ' - transact (payable)',
content: 'transact',
classList: 'btn-danger',
dataId: title + ' - transact (payable)',
});
} else {
setButtonOptions({
title: title + ' - transact (not payable)',
content: 'transact',
classList: 'btn-warning',
dataId: title + ' - transact (not payable)',
});
}
}, [lookupOnly, props.funcABI, title]);
return (
<div className={`d-inline-block`} style={{ width: '90%' }}>
<div className="p-2">
<input
data-id={`functionTitle${props.funcId}`}
className="form-control"
placeholder={intl.formatMessage({ id: 'quickDapp.functionTitle' })}
value={props.funcABI.title}
onChange={({ target: { value } }) => {
saveTitle({ id: props.funcABI.id, title: value });
}}
/>
</div>
<div className="p-2 d-flex">
<div
className="d-flex p-0 wrapperElement"
data-id={buttonOptions.dataId}
data-title={buttonOptions.title}
>
<button
disabled
className={`text-nowrap overflow-hidden text-truncate btn btn-sm ${
buttonOptions.classList
} ${
props.funcABI.inputs && props.funcABI.inputs.length > 0
? 'has-args'
: ''
}`}
data-id={buttonOptions.dataId}
data-title={buttonOptions.title}
style={{ pointerEvents: 'none', width: 100 }}
>
{title}
</button>
</div>
<input
disabled
className="instance-input w-100 p-2 border-0 rounded-right"
data-id={'multiParamManagerBasicInputField'}
placeholder={inputs}
data-title={inputs}
style={{
height: '2rem',
visibility: !(
props.funcABI.inputs && props.funcABI.inputs.length > 0
)
? 'hidden'
: 'visible',
}}
/>
</div>
<div className="p-2">
<textarea
data-id={`functionInstructions${props.funcId}`}
className="form-control"
placeholder={intl.formatMessage({ id: 'quickDapp.functionInstructions' })}
value={props.funcABI.details}
onChange={({ target: { value } }) => {
saveDetails({ id: props.funcABI.id, details: value });
}}
/>
</div>
</div>
);
}

@ -0,0 +1,107 @@
import React, { useState } from 'react';
import { Alert, Button, Form } from 'react-bootstrap';
import { FormattedMessage, useIntl } from 'react-intl';
import { initInstance } from '../../actions';
const CreateInstance: React.FC = () => {
const intl = useIntl()
const [formVal, setFormVal] = useState({
address: '',
abi: [],
name: '',
network: '',
});
const [error, setError] = useState('')
return (
<Form
className="w-50 m-auto"
onSubmit={(e: any) => {
e.preventDefault();
initInstance({ ...formVal });
}}
>
<Form.Group className="mb-2" controlId="formAddress">
<Form.Label className="text-uppercase mb-0"><FormattedMessage id="quickDapp.address" /></Form.Label>
<Form.Control
type="address"
placeholder={intl.formatMessage({ id: 'quickDapp.enterAddress' })}
value={formVal.address}
onChange={(e) => {
setFormVal({ ...formVal, address: e.target.value });
}}
/>
</Form.Group>
<Form.Group className="mb-2" controlId="formAbi">
<Form.Label className="text-uppercase mb-0">abi</Form.Label>
<Form.Control
as="textarea"
rows={3}
type="abi"
placeholder={intl.formatMessage({ id: 'quickDapp.enterAbi' })}
onChange={(e) => {
setError('')
let abi = [];
if (e.target.value !== '') {
try {
abi = JSON.parse(e.target.value);
} catch (error) {
setError(error.toString())
}
}
setFormVal({ ...formVal, abi });
}}
/>
{error && <Form.Text className='text-danger'>
{error}
</Form.Text>}
</Form.Group>
<Form.Group className="mb-2" controlId="formName">
<Form.Label className="text-uppercase mb-0"><FormattedMessage id="quickDapp.name" /></Form.Label>
<Form.Control
type="name"
placeholder={intl.formatMessage({ id: 'quickDapp.enterName' })}
value={formVal.name}
onChange={(e) => {
setFormVal({ ...formVal, name: e.target.value });
}}
/>
</Form.Group>
<Form.Group className="mb-2" controlId="formNetwork">
<Form.Label className="text-uppercase mb-0"><FormattedMessage id="quickDapp.network" /></Form.Label>
<Form.Control
type="network"
placeholder={intl.formatMessage({ id: 'quickDapp.enterNetwork' })}
value={formVal.network}
onChange={(e) => {
setFormVal({ ...formVal, network: e.target.value });
}}
/>
</Form.Group>
<Button
variant="primary"
type="submit"
className="mt-2"
data-id="createDapp"
disabled={
!formVal.address ||
!formVal.name ||
!formVal.network ||
!formVal.abi.length
}
>
<FormattedMessage id="quickDapp.submit" />
</Button>
<Alert className="mt-4" variant="info" data-id="quickDappTooltips">
<FormattedMessage id="quickDapp.text1" />
<br />
<FormattedMessage id="quickDapp.text2" />
</Alert>
<img src='./assets/edit-dapp.png' />
</Form>
);
};
export default CreateInstance;

@ -0,0 +1,329 @@
import React, { useContext, useState, useEffect } from 'react';
import { Form, Button, Alert, InputGroup } from 'react-bootstrap';
import { FormattedMessage, useIntl } from 'react-intl';
import {
deploy,
teardown,
emptyInstance,
resetInstance,
getInfoFromNatSpec,
} from '../../actions';
import { ThemeUI } from './theme';
import { CustomTooltip } from '@remix-ui/helper';
import { AppContext } from '../../contexts';
function DeployPanel(): JSX.Element {
const intl = useIntl()
const { appState, dispatch } = useContext(AppContext);
const { verified, natSpec, noTerminal } = appState.instance;
const [formVal, setFormVal] = useState<any>({
email: localStorage.getItem('__SURGE_EMAIL') || '',
password: localStorage.getItem('__SURGE_PASSWORD') || '',
subdomain: '',
shortname: localStorage.getItem('__DISQUS_SHORTNAME') || '',
shareTo: [],
});
const setShareTo = (type: string) => {
let shareTo = formVal.shareTo;
if (formVal.shareTo.includes(type)) {
shareTo = shareTo.filter((item: string) => item !== type);
} else {
shareTo.push(type);
}
setFormVal({ ...formVal, shareTo });
};
const [deployState, setDeployState] = useState({
code: '',
error: '',
loading: false,
});
const [teardownState, setTeardownState] = useState({
code: '',
error: '',
loading: false,
});
useEffect(() => {
window.scrollTo(0, document.body.scrollHeight);
}, [deployState, teardownState])
return (
<div className="col-3 d-inline-block">
<h3 className="mb-3" data-id="quick-dapp-admin">QuickDapp <FormattedMessage id="quickDapp.admin" /></h3>
<Button
data-id="resetFunctions"
onClick={() => {
resetInstance();
}}
>
<FormattedMessage id="quickDapp.resetFunctions" />
</Button>
<Button
data-id="deleteDapp"
className="ml-3"
onClick={() => {
emptyInstance();
}}
>
<FormattedMessage id="quickDapp.deleteDapp" />
</Button>
<Alert variant="info" className="my-2">
<FormattedMessage
id="quickDapp.text3"
values={{
a: (chunks) => (
<a target="_blank" href="https://surge.sh/help/">
{chunks}
</a>
),
}}
/>
</Alert>
<Form
onSubmit={(e) => {
e.preventDefault();
setDeployState({ code: '', error: '', loading: true });
deploy(formVal, (state: any) => {
setDeployState({ ...state, loading: false });
});
}}
>
<Form.Group className="mb-2" controlId="formEmail">
<Form.Label className="text-uppercase mb-0"><FormattedMessage id="quickDapp.email" /></Form.Label>
<Form.Control
data-id="surgeEmail"
type="email"
placeholder={intl.formatMessage({ id: 'quickDapp.surgeEmail' })}
required
value={formVal.email}
onChange={(e) => {
setFormVal({ ...formVal, email: e.target.value });
}}
/>
</Form.Group>
<Form.Group className="mb-2" controlId="formPassword">
<Form.Label className="text-uppercase mb-0"><FormattedMessage id="quickDapp.password" /></Form.Label>
<Form.Control
data-id="surgePassword"
type="password"
placeholder={intl.formatMessage({ id: 'quickDapp.surgePassword' })}
required
value={formVal.password}
onChange={(e) => {
setFormVal({ ...formVal, password: e.target.value });
}}
/>
</Form.Group>
<Form.Group className="mb-2" controlId="formPassword">
<Form.Label className="text-uppercase mb-0"><FormattedMessage id="quickDapp.subdomain" /></Form.Label>
<InputGroup>
<InputGroup.Text>https://</InputGroup.Text>
<Form.Control
data-id="surgeSubdomain"
type="subdomain"
placeholder={intl.formatMessage({ id: 'quickDapp.uniqueSubdomain' })}
required
value={formVal.subdomain}
onChange={(e) => {
setFormVal({ ...formVal, subdomain: e.target.value });
}}
/>
<InputGroup.Text>.surge.sh</InputGroup.Text>
</InputGroup>
</Form.Group>
{/* <Form.Group className="mb-3" controlId="formShortname">
<Form.Label>Disqus Shortname (Optional)</Form.Label>
<Form.Control
type="shortname"
placeholder="Disqus Shortname"
value={formVal.shortname}
onChange={(e) => {
setFormVal({ ...formVal, shortname: e.target.value });
}}
/>
</Form.Group> */}
<Form.Group className="mb-2" controlId="formShareTo">
<Form.Label className="text-uppercase mb-0">
<FormattedMessage id="quickDapp.shareTo" />
</Form.Label>
<br />
<div className="d-inline-flex align-items-center custom-control custom-checkbox">
<input
id="shareToTwitter"
className="form-check-input custom-control-input"
type="checkbox"
name="group1"
value="twitter"
onChange={(e) => {
setShareTo(e.target.value);
}}
checked={formVal.shareTo.includes('twitter')}
/>
<label
htmlFor="shareToTwitter"
className="m-0 form-check-label custom-control-label"
style={{ paddingTop: 1 }}
>
Twitter
</label>
</div>
<div className="d-inline-flex align-items-center custom-control custom-checkbox ml-3">
<input
id="shareToFacebook"
className="form-check-input custom-control-input"
type="checkbox"
name="group1"
value="facebook"
onChange={(e) => {
setShareTo(e.target.value);
}}
checked={formVal.shareTo.includes('facebook')}
/>
<label
htmlFor="shareToFacebook"
className="m-0 form-check-label custom-control-label"
style={{ paddingTop: 1 }}
>
Facebook
</label>
</div>
</Form.Group>
<Form.Group className="mb-2" controlId="formShareTo">
<Form.Label className="text-uppercase mb-0">
<FormattedMessage id="quickDapp.useNatSpec" />
</Form.Label>
<br />
<span
data-id="useNatSpec"
id="useNatSpec"
className="btn ai-switch pl-0 py-0"
onClick={async () => {
getInfoFromNatSpec(!natSpec.checked);
}}
>
<CustomTooltip
placement="top"
tooltipText={intl.formatMessage({ id: 'quickDapp.useNatSpecTooltip' })}
>
<i
className={
natSpec.checked
? 'fas fa-toggle-on fa-lg'
: 'fas fa-toggle-off fa-lg'
}
></i>
</CustomTooltip>
</span>
</Form.Group>
<Form.Group className="mb-2" controlId="formVerified">
<Form.Label className="text-uppercase mb-0">
<FormattedMessage id="quickDapp.verifiedByEtherscan" />
</Form.Label>
<div className="d-flex py-1 align-items-center custom-control custom-checkbox">
<input
id="verifiedByEtherscan"
className="form-check-input custom-control-input"
type="checkbox"
onChange={(e) => {
dispatch({
type: 'SET_INSTANCE',
payload: { verified: e.target.checked },
});
}}
checked={verified}
/>
<label
htmlFor="verifiedByEtherscan"
className="m-0 form-check-label custom-control-label"
style={{ paddingTop: 1 }}
>
<FormattedMessage id="quickDapp.verified" />
</label>
</div>
</Form.Group>
<Form.Group className="mb-2" controlId="formNoTerminal">
<Form.Label className="text-uppercase mb-0">
<FormattedMessage id="quickDapp.noTerminal" />
</Form.Label>
<div className="d-flex py-1 align-items-center custom-control custom-checkbox">
<input
id="noTerminal"
className="form-check-input custom-control-input"
type="checkbox"
onChange={(e) => {
dispatch({
type: 'SET_INSTANCE',
payload: { noTerminal: e.target.checked },
});
}}
checked={noTerminal}
/>
<label
htmlFor="noTerminal"
className="m-0 form-check-label custom-control-label"
style={{ paddingTop: 1 }}
>
<FormattedMessage id="quickDapp.no" />
</label>
</div>
</Form.Group>
<ThemeUI />
<Button
data-id="deployDapp"
variant="primary"
type="submit"
className="mt-3"
disabled={!formVal.email || !formVal.password || !formVal.subdomain}
>
{deployState.loading && (
<i className="fas fa-spinner fa-spin mr-1"></i>
)}
<FormattedMessage id="quickDapp.deploy" />
</Button>
<Button
data-id="teardownDapp"
variant="primary"
className="mt-3 ml-3"
disabled={!formVal.email || !formVal.password || !formVal.subdomain}
// hide this button for now, just for e2e use
style={{ display: 'none' }}
onClick={() => {
setTeardownState({ code: '', error: '', loading: true });
teardown(formVal, (state) => {
setTeardownState({ ...state, loading: false });
})
}}
>
{teardownState.loading && (
<i className="fas fa-spinner fa-spin mr-1"></i>
)}
<FormattedMessage id="quickDapp.teardown" />
</Button>
{deployState.code !== '' && (
<Alert variant={deployState.code === 'SUCCESS' ? "success" : "danger"} className="mt-4" data-id="deployResult">
{deployState.code === 'SUCCESS' ? <>
<FormattedMessage id="quickDapp.text4" /> <br /> <FormattedMessage id="quickDapp.text5" />
<br />
<a
data-id="dappUrl"
target="_blank"
href={`https://${formVal.subdomain}.surge.sh`}
>{`https://${formVal.subdomain}.surge.sh`}</a>
</> : deployState.error}
</Alert>
)}
{teardownState.code !== '' && (
<Alert variant={teardownState.code === 'SUCCESS' ? "success" : "danger"} className="mt-4" data-id="teardownResult">
{teardownState.code === 'SUCCESS' ? <FormattedMessage id="quickDapp.text6" /> : teardownState.error}
</Alert>
)}
</Form>
</div>
);
}
export default DeployPanel;

@ -0,0 +1,159 @@
import { Ref, useContext, useEffect } from 'react';
import { FormattedMessage } from 'react-intl';
import { AppContext } from '../../contexts';
import { selectTheme } from '../../actions';
import { Dropdown } from 'react-bootstrap';
import React from 'react';
export const themeMap: Record<string, any> = {
Dark: { quality: 'dark', url: 'assets/css/themes/remix-dark_tvx1s2.css' },
Light: { quality: 'light', url: 'assets/css/themes/remix-light_powaqg.css' },
Violet: { quality: 'light', url: 'assets/css/themes/remix-violet.css' },
Unicorn: { quality: 'light', url: 'assets/css/themes/remix-unicorn.css' },
Midcentury: {
quality: 'light',
url: 'assets/css/themes/remix-midcentury_hrzph3.css',
},
Black: { quality: 'dark', url: 'assets/css/themes/remix-black_undtds.css' },
Candy: { quality: 'light', url: 'assets/css/themes/remix-candy_ikhg4m.css' },
HackerOwl: { quality: 'dark', url: 'assets/css/themes/remix-hacker_owl.css' },
Cerulean: {
quality: 'light',
url: 'assets/css/themes/bootstrap-cerulean.min.css',
},
Flatly: {
quality: 'light',
url: 'assets/css/themes/bootstrap-flatly.min.css',
},
Spacelab: {
quality: 'light',
url: 'assets/css/themes/bootstrap-spacelab.min.css',
},
Cyborg: {
quality: 'dark',
url: 'assets/css/themes/bootstrap-cyborg.min.css',
},
};
const CustomToggle = React.forwardRef(
(
{
children,
onClick,
icon,
className = '',
}: {
children: React.ReactNode;
onClick: (e: any) => void;
icon: string;
className: string;
},
ref: Ref<HTMLButtonElement>
) => (
<button
ref={ref}
onClick={(e) => {
e.preventDefault();
onClick(e);
}}
id="dropdown-custom-components"
data-id="selectThemesOptions"
className={className.replace('dropdown-toggle', '')}
>
<div className="d-flex">
<div className="mr-auto text-nowrap overflow-hidden">{children}</div>
{icon && (
<div className="pr-1">
<i className={`${icon} pr-1`}></i>
</div>
)}
<div>
<i className="fad fa-sort-circle"></i>
</div>
</div>
</button>
)
);
const CustomMenu = React.forwardRef(
(
{
children,
style,
'data-id': dataId,
className,
'aria-labelledby': labeledBy,
}: {
children: React.ReactNode;
style?: React.CSSProperties;
'data-id'?: string;
className: string;
'aria-labelledby'?: string;
},
ref: Ref<HTMLDivElement>
) => {
const height = window.innerHeight * 0.6;
return (
<div
ref={ref}
style={style}
className={className}
aria-labelledby={labeledBy}
data-id={dataId}
>
<ul
className="overflow-auto list-unstyled mb-0"
style={{ maxHeight: height + 'px' }}
>
{children}
</ul>
</div>
);
}
);
export function ThemeUI() {
const { appState } = useContext(AppContext);
const { theme } = appState.instance;
const themeList = Object.keys(themeMap);
useEffect(() => {
selectTheme(theme);
}, []);
return (
<div className="d-block">
<label className="text-uppercase mb-0"><FormattedMessage id="quickDapp.themes" /></label>
<Dropdown className="w-100">
<Dropdown.Toggle
as={CustomToggle}
className="btn btn-light btn-block w-100 d-inline-block border border-dark form-control"
icon={''}
>
{theme} - {themeMap[theme].quality}
</Dropdown.Toggle>
<Dropdown.Menu
as={CustomMenu}
className="w-100 custom-dropdown-items"
data-id="custom-dropdown-items"
>
{themeList.map((item) => (
<Dropdown.Item
key={item}
onClick={() => {
selectTheme(item);
}}
data-id={`dropdown-item-${item}`}
>
{theme === item && (
<span className="fas fa-check text-success mr-2"></span>
)}
{item} - {themeMap[item].quality}
</Dropdown.Item>
))}
</Dropdown.Menu>
</Dropdown>
</div>
);
}

@ -0,0 +1,86 @@
import { useContext } from 'react';
import { omitBy } from 'lodash';
import { useIntl } from 'react-intl';
import { MultipleContainers } from '../MultipleContainers';
import { AppContext } from '../../contexts';
import ImageUpload from '../ImageUpload'
function EditInstance(): JSX.Element {
const intl = useIntl()
const { appState, dispatch } = useContext(AppContext);
const { abi, items, containers, title, details, userInput, natSpec } =
appState.instance;
return (
<div className="col-9 d-inline-block row">
<div className="row">
<ImageUpload />
<div className="col-9 pl-0">
<div className="my-2 p-3 bg-light">
<input
data-id="dappTitle"
className="form-control"
placeholder={intl.formatMessage({ id: 'quickDapp.dappTitle' })}
value={title}
onChange={({ target: { value } }) => {
dispatch({
type: 'SET_INSTANCE',
payload: {
title: natSpec.checked && !value ? natSpec.title : value,
userInput: omitBy(
{ ...userInput, title: value },
(item) => item === ''
),
},
});
}}
/>
</div>
<div className="my-2 p-3 bg-light">
<textarea
data-id="dappInstructions"
className="form-control"
placeholder={intl.formatMessage({ id: 'quickDapp.dappInstructions' })}
value={details}
onChange={({ target: { value } }) => {
dispatch({
type: 'SET_INSTANCE',
payload: {
details: natSpec.checked && !value ? natSpec.details : value,
userInput: omitBy(
{ ...userInput, details: value },
(item) => item === ''
),
},
});
}}
/>
</div>
</div>
</div>
<MultipleContainers
abi={abi}
items={items}
containers={containers}
setItemsAndContainers={(
newItems: any = items,
newContainers: any = containers
) => {
dispatch({
type: 'SET_INSTANCE',
payload: {
items: newItems,
containers: newContainers,
},
});
}}
handle
scrollable
containerStyle={{
maxHeight: '90vh',
}}
/>
</div>
);
}
export default EditInstance;

@ -0,0 +1,76 @@
import React, { useEffect, useState } from 'react';
const EditableText = ({
value,
onSave,
textarea,
placeholder,
}: {
value: string;
onSave: (str: string) => void;
textarea?: boolean;
placeholder?: string;
}) => {
const [isEditing, setIsEditing] = useState(false);
const [tempText, setTempText] = useState(value);
useEffect(() => {
setTempText(value);
}, [value]);
const handleEdit = () => {
setIsEditing(true);
};
const handleSave = () => {
onSave(tempText);
setIsEditing(false);
};
const handleCancel = () => {
setIsEditing(false);
};
const handleChange = (event: {
target: { value: React.SetStateAction<string> };
}) => {
setTempText(event.target.value);
};
const InputElement = textarea ? 'textarea' : 'input';
const TextElement = textarea ? 'span' : 'h1';
return isEditing ? (
<>
<InputElement
className="form-control"
placeholder={placeholder}
value={tempText}
onChange={handleChange}
style={{ height: textarea ? 100 : 'auto' }}
/>
<div className="d-flex justify-content-end">
<i
className="fas ml-2 mt-2 fa-save cursor_pointer"
onClick={handleSave}
/>
<i
className="fas ml-2 mt-2 fa-ban cursor_pointer"
onClick={handleCancel}
/>
</div>
</>
) : (
<div className="d-flex justify-content-between align-items-center">
<TextElement className="udapp_intro">
{value ? value : placeholder}
</TextElement>
<i
className="fas fa-edit ml-2 float-right cursor_pointer"
onClick={handleEdit}
/>
</div>
);
};
export default EditableText;

@ -0,0 +1,50 @@
import React, { useContext, useEffect, useState } from 'react'
import { useIntl } from 'react-intl';
import { CustomTooltip } from '@remix-ui/helper';
import { AppContext } from '../../contexts'
const ImageUpload = () => {
const intl = useIntl()
const { appState, dispatch } = useContext(AppContext)
const { logo } = appState.instance
const [preview, setPreview] = useState(null)
useEffect(() => {
if (logo) {
const base64data = btoa(new Uint8Array(logo).reduce((data, byte) => data + String.fromCharCode(byte), ''))
setPreview('data:image/jpeg;base64,' + base64data)
} else {
setPreview(null)
}
}, [logo])
const handleImageChange = (e) => {
if (e.target.files && e.target.files[0]) {
const reader: any = new FileReader()
reader.onloadend = () => {
dispatch({ type: 'SET_INSTANCE', payload: { logo: reader.result } })
}
reader.readAsArrayBuffer(e.target.files[0])
}
}
return (
<div className="col-3 pr-0">
<input data-id="uploadLogo" className="d-none" type="file" accept="image/*" onChange={handleImageChange} id="upload-button" />
<CustomTooltip
placement="right"
tooltipText={intl.formatMessage({ id: 'quickDapp.uploadLogoTooltip' })}
>
<label htmlFor="upload-button" className="cursor_pointer d-flex justify-content-center align-items-center position-relative" style={{ height: 170 }}>
{logo ? (
<img src={preview} alt="preview" style={{ width: 120, height: 120 }} />
) : (
<i className="fas fa-upload" style={{ fontSize: 120 }}></i>
)}
</label>
</CustomTooltip>
</div>
)
}
export default ImageUpload

@ -0,0 +1,31 @@
import React, { useContext } from 'react';
import BounceLoader from 'react-spinners/BounceLoader';
import { AppContext } from '../../contexts';
const LoadingScreen: React.FC = () => {
const { appState } = useContext(AppContext);
const loading = appState.loading.screen;
return loading ? (
<div
className="w-100 h-100 position-fixed bg-dark z-3"
style={{
top: 0,
opacity: 0.8
}}
>
<BounceLoader
color="#a7b0ae"
size={100}
className="position-absolute m-0"
style={{
top: '40%',
left: '50%',
transform: 'translate(-50%,-50%)',
}}
/>
</div>
) : null;
};
export default LoadingScreen;

@ -0,0 +1,70 @@
import React, { forwardRef } from 'react';
import { Handle } from '../Item';
import { Remove } from './Remove'
export interface Props {
children: React.ReactNode;
columns?: number;
label?: string;
style?: React.CSSProperties;
hover?: boolean;
handleProps?: React.HTMLAttributes<any>;
placeholder?: boolean;
onClick?(): void;
onRemove?(): void;
}
export const Container = forwardRef<HTMLDivElement, Props>(
(
{
children,
columns = 1,
handleProps,
hover,
onClick,
onRemove,
label,
placeholder,
style,
...props
}: Props,
ref
) => {
return (
<div
{...props}
ref={ref}
style={
{
...style,
'--columns': columns,
} as React.CSSProperties
}
className={`col pr-0 d-flex rounded container ${hover && 'hover'} ${
placeholder && 'placeholder'
}`}
onClick={onClick}
tabIndex={onClick ? 0 : undefined}
>
{label ? (
<div
className={`px-2 py-1 d-flex align-items-center justify-content-between container-header`}
>
{label}
<div className={`d-flex container-actions`}>
<Remove onClick={onRemove} data-id={`remove${label.replace(/\s*/g,"")}`} />
<Handle {...handleProps} data-id={`handle${label.replace(/\s*/g,"")}`} />
</div>
</div>
) : null}
{placeholder ? (
children
) : (
<ul className="p-0 m-0 list-unstyled" style={{ overflowY: 'auto' }} data-id={`container${label.replace(/\s*/g,"")}`}>
{children}
</ul>
)}
</div>
);
}
);

@ -0,0 +1,13 @@
import React from 'react';
import { Action, Props as ActionProps } from '../Item/Action';
export function Remove(props: ActionProps) {
return (
<Action
{...props}
>
<i className="fas fa-times"></i>
</Action>
);
}

@ -0,0 +1,2 @@
export { Container } from './Container'
export type { Props as ContainerProps } from './Container'

@ -0,0 +1,29 @@
import React, { forwardRef, CSSProperties } from 'react';
export interface Props extends React.HTMLAttributes<HTMLButtonElement> {
active?: {
fill: string;
background: string;
};
cursor?: CSSProperties['cursor'];
}
export const Action = forwardRef<HTMLButtonElement, Props>(
({ active, className, cursor, style, ...props }, ref) => {
return (
<button
ref={ref}
{...props}
className={`d-flex align-items-center justify-content-center border-0 rounded p-3 item-action`}
tabIndex={0}
style={
{
...style,
cursor,
width: 12,
} as CSSProperties
}
/>
);
}
);

@ -0,0 +1,18 @@
import React, { forwardRef } from 'react';
import { Action, Props as ActionProps } from './Action';
export const Handle = forwardRef<HTMLButtonElement, ActionProps>(
(props, ref) => {
return (
<Action
ref={ref}
cursor="grab"
data-cypress="draggable-handle"
{...props}
>
<i className="fas fa-grip-vertical"></i>
</Action>
);
}
);

@ -0,0 +1,112 @@
import React, { useEffect } from 'react';
import type { DraggableSyntheticListeners } from '@dnd-kit/core';
import type { Transform } from '@dnd-kit/utilities';
import { Handle } from './Handle';
export interface Props {
dragOverlay?: boolean;
disabled?: boolean;
dragging?: boolean;
handle?: boolean;
handleProps?: any;
height?: number;
index?: number;
fadeIn?: boolean;
transform?: Transform | null;
listeners?: DraggableSyntheticListeners;
sorting?: boolean;
style?: React.CSSProperties;
transition?: string | null;
wrapperStyle?: React.CSSProperties;
children: React.ReactNode;
onRemove?(): void;
id?: any;
}
export const Item = React.memo(
React.forwardRef<HTMLLIElement, Props>(
(
{
dragOverlay,
dragging,
disabled,
fadeIn,
handle,
handleProps,
height,
index,
listeners,
onRemove,
sorting,
style,
transition,
transform,
children,
wrapperStyle,
id,
...props
},
ref
) => {
useEffect(() => {
if (!dragOverlay) {
return;
}
document.body.style.cursor = 'grabbing';
return () => {
document.body.style.cursor = '';
};
}, [dragOverlay]);
return (
<li
className={`position-relative mb-3 list-unstyled item-wrapper`}
style={
{
...wrapperStyle,
transition: [transition, wrapperStyle?.transition]
.filter(Boolean)
.join(', '),
'--translate-x': transform
? `${Math.round(transform.x)}px`
: undefined,
'--translate-y': transform
? `${Math.round(transform.y)}px`
: undefined,
'--scale-x': transform?.scaleX
? `${transform.scaleX}`
: undefined,
'--scale-y': transform?.scaleY
? `${transform.scaleY}`
: undefined,
'--index': index,
} as React.CSSProperties
}
ref={ref}
>
<div
style={style}
data-cypress="draggable-item"
{...(!handle ? listeners : undefined)}
{...props}
tabIndex={!handle ? 0 : undefined}
>
<div className="border-dark bg-light d-flex">
{children}
<Handle {...handleProps} {...listeners} data-id={`handle${id}`} />
</div>
<button
data-id={`remove${id}`}
className={`d-flex justify-content-center align-items-center position-absolute border-0 rounded-circle item-remove`}
onClick={onRemove}
>
<i className="fas fa-times"></i>
</button>
</div>
</li>
);
}
)
);

@ -0,0 +1,3 @@
export { Item } from './Item';
export { Action } from './Action';
export { Handle } from './Handle';

@ -0,0 +1,3 @@
export { Container } from './Container';
export type { ContainerProps } from './Container';
export { Item, Action, Handle } from './Item';

@ -0,0 +1,669 @@
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { createPortal, unstable_batchedUpdates } from 'react-dom';
import { FormattedMessage, useIntl } from 'react-intl';
import {
CancelDrop,
closestCenter,
pointerWithin,
rectIntersection,
CollisionDetection,
DndContext,
DragOverlay,
DropAnimation,
getFirstCollision,
KeyboardSensor,
MouseSensor,
TouchSensor,
Modifiers,
UniqueIdentifier,
useSensors,
useSensor,
MeasuringStrategy,
KeyboardCoordinateGetter,
defaultDropAnimationSideEffects,
} from '@dnd-kit/core';
import {
AnimateLayoutChanges,
SortableContext,
useSortable,
arrayMove,
defaultAnimateLayoutChanges,
verticalListSortingStrategy,
SortingStrategy,
horizontalListSortingStrategy,
} from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { coordinateGetter as multipleContainersCoordinateGetter } from './multipleContainersKeyboardCoordinates';
import { Item, Container, ContainerProps } from './components';
import { ContractGUI } from '../ContractGUI';
export default {
title: 'Presets/Sortable/Multiple Containers',
};
// This function is used to animate layout changes.
const animateLayoutChanges: AnimateLayoutChanges = (args) =>
defaultAnimateLayoutChanges({ ...args, wasDragging: true });
// This is a container component that can be dragged and dropped.
function DroppableContainer({
children,
columns = 1,
disabled,
id,
items,
style,
...props
}: ContainerProps & {
disabled?: boolean;
id: UniqueIdentifier;
items: UniqueIdentifier[];
style?: React.CSSProperties;
}) {
const {
active,
attributes,
isDragging,
listeners,
over,
setNodeRef,
transition,
transform,
} = useSortable({
id,
data: {
type: 'container',
children: items,
},
animateLayoutChanges,
});
const isOverContainer = over
? (id === over.id && active?.data.current?.type !== 'container') ||
items.includes(over.id)
: false;
// Return the container.
return (
<Container
ref={disabled ? undefined : setNodeRef}
style={{
...style,
transition,
transform: CSS.Translate.toString(transform),
opacity: isDragging ? 0.5 : undefined,
}}
hover={isOverContainer}
handleProps={{
...attributes,
...listeners,
}}
columns={columns}
{...props}
>
{children}
</Container>
);
}
// This setting is used for drop animation.
const dropAnimation: DropAnimation = {
sideEffects: defaultDropAnimationSideEffects({
styles: {
active: {
opacity: '0.5',
},
},
}),
};
// This type is used to define the items.
type Items = Record<UniqueIdentifier, UniqueIdentifier[]>;
// This interface is used to define the props for the MultipleContainers component.
interface Props {
adjustScale?: boolean;
cancelDrop?: CancelDrop;
columns?: number;
containerStyle?: React.CSSProperties;
coordinateGetter?: KeyboardCoordinateGetter;
getItemStyles?(args: {
value: UniqueIdentifier;
index: number;
overIndex: number;
isDragging: boolean;
containerId: UniqueIdentifier;
isSorting: boolean;
isDragOverlay: boolean;
}): React.CSSProperties;
wrapperStyle?(args: { index: number }): React.CSSProperties;
itemCount?: number;
abi?: any;
items: Items;
containers: any;
setItemsAndContainers: (item?: any, containers?: any) => void;
handle?: boolean;
strategy?: SortingStrategy;
modifiers?: Modifiers;
scrollable?: boolean;
vertical?: boolean;
}
const PLACEHOLDER_ID = 'placeholder';
const empty: UniqueIdentifier[] = [];
// This is a complex component. It allows items in multiple containers to be sorted using drag and drop.
// The containers themselves can also be sorted. The DndContext component from the @dnd-kit/core package provides the drag and drop context.
// The MultipleContainers component is the main component, it handles the sorting logic when an item is dragged over another item or when an item is dropped.
export function MultipleContainers({
adjustScale = false,
cancelDrop,
columns,
handle = false,
items,
containers,
setItemsAndContainers,
abi,
containerStyle,
coordinateGetter = multipleContainersCoordinateGetter,
getItemStyles = () => ({}),
wrapperStyle = () => ({}),
modifiers,
strategy = verticalListSortingStrategy,
vertical = false,
scrollable,
}: Props) {
const intl = useIntl()
const [activeId, setActiveId] = useState<UniqueIdentifier | null>(null);
const lastOverId = useRef<UniqueIdentifier | null>(null);
const recentlyMovedToNewContainer = useRef(false);
const isSortingContainer = activeId ? containers.includes(activeId) : false;
/**
* Custom collision detection strategy optimized for multiple containers
*
* - First, find any droppable containers intersecting with the pointer.
* - If there are none, find intersecting containers with the active draggable.
* - If there are no intersecting containers, return the last matched intersection
*
*/
const collisionDetectionStrategy: CollisionDetection = useCallback(
(args) => {
if (activeId && activeId in items) {
return closestCenter({
...args,
droppableContainers: args.droppableContainers.filter(
(container) => container.id in items
),
});
}
// Start by finding any intersecting droppable
const pointerIntersections = pointerWithin(args);
const intersections =
pointerIntersections.length > 0
? // If there are droppables intersecting with the pointer, return those
pointerIntersections
: rectIntersection(args);
let overId = getFirstCollision(intersections, 'id');
if (overId != null) {
if (overId in items) {
const containerItems = items[overId];
// If a container is matched and it contains items (columns 'A', 'B', 'C')
if (containerItems.length > 0) {
// Return the closest droppable within that container
overId = closestCenter({
...args,
droppableContainers: args.droppableContainers.filter(
(container) =>
container.id !== overId &&
containerItems.includes(container.id)
),
})[0]?.id;
}
}
lastOverId.current = overId;
return [{ id: overId }];
}
// When a draggable item moves to a new container, the layout may shift
// and the `overId` may become `null`. We manually set the cached `lastOverId`
// to the id of the draggable item that was moved to the new container, otherwise
// the previous `overId` will be returned which can cause items to incorrectly shift positions
if (recentlyMovedToNewContainer.current) {
lastOverId.current = activeId;
}
// If no droppable is matched, return the last match
return lastOverId.current ? [{ id: lastOverId.current }] : [];
},
[activeId, items]
);
const [clonedItems, setClonedItems] = useState<Items | null>(null);
const sensors = useSensors(
useSensor(MouseSensor),
useSensor(TouchSensor),
useSensor(KeyboardSensor, {
coordinateGetter,
})
);
const findContainer = (id: UniqueIdentifier) => {
if (id in items) {
return id;
}
return Object.keys(items).find((key) => items[key].includes(id));
};
const getIndex = (id: UniqueIdentifier) => {
const container = findContainer(id);
if (!container) {
return -1;
}
const index = items[container].indexOf(id);
return index;
};
const onDragCancel = () => {
if (clonedItems) {
// Reset items to their original state in case items have been
// Dragged across containers
setItemsAndContainers(clonedItems);
}
setActiveId(null);
setClonedItems(null);
};
useEffect(() => {
requestAnimationFrame(() => {
recentlyMovedToNewContainer.current = false;
});
}, [items]);
return (
<DndContext
sensors={sensors}
collisionDetection={collisionDetectionStrategy}
measuring={{
droppable: {
strategy: MeasuringStrategy.Always,
},
}}
onDragStart={({ active }) => {
setActiveId(active.id);
setClonedItems(items);
}}
onDragOver={({ active, over }) => {
const overId = over?.id;
if (overId == null || active.id in items) {
return;
}
const overContainer = findContainer(overId);
const activeContainer = findContainer(active.id);
if (!overContainer || !activeContainer) {
return;
}
if (activeContainer !== overContainer) {
const activeItems = items[activeContainer];
const overItems = items[overContainer];
const overIndex = overItems.indexOf(overId);
const activeIndex = activeItems.indexOf(active.id);
let newIndex: number;
if (overId in items) {
newIndex = overItems.length + 1;
} else {
const isBelowOverItem =
over &&
active.rect.current.translated &&
active.rect.current.translated.top >
over.rect.top + over.rect.height;
const modifier = isBelowOverItem ? 1 : 0;
newIndex =
overIndex >= 0 ? overIndex + modifier : overItems.length + 1;
}
recentlyMovedToNewContainer.current = true;
setItemsAndContainers({
...items,
[activeContainer]: items[activeContainer].filter(
(item) => item !== active.id
),
[overContainer]: [
...items[overContainer].slice(0, newIndex),
items[activeContainer][activeIndex],
...items[overContainer].slice(
newIndex,
items[overContainer].length
),
],
});
}
}}
onDragEnd={({ active, over }) => {
if (active.id in items && over?.id) {
const activeIndex = containers.indexOf(active.id);
const overIndex = containers.indexOf(over.id);
setItemsAndContainers(
undefined,
arrayMove(containers, activeIndex, overIndex)
);
}
const activeContainer = findContainer(active.id);
if (!activeContainer) {
setActiveId(null);
return;
}
const overId = over?.id;
if (overId == null) {
setActiveId(null);
return;
}
if (overId === PLACEHOLDER_ID) {
const newContainerId = getNextContainerId();
unstable_batchedUpdates(() => {
setItemsAndContainers(
{
...items,
[activeContainer]: items[activeContainer].filter(
(id) => id !== activeId
),
[newContainerId]: [active.id],
},
[...containers, newContainerId]
);
setActiveId(null);
});
return;
}
const overContainer = findContainer(overId);
if (overContainer) {
const activeIndex = items[activeContainer].indexOf(active.id);
const overIndex = items[overContainer].indexOf(overId);
if (activeIndex !== overIndex) {
setItemsAndContainers({
...items,
[overContainer]: arrayMove(
items[overContainer],
activeIndex,
overIndex
),
});
}
}
setActiveId(null);
}}
cancelDrop={cancelDrop}
onDragCancel={onDragCancel}
modifiers={modifiers}
>
<div
className="row pt-0"
style={{
boxSizing: 'border-box',
padding: 20,
gridAutoFlow: vertical ? 'row' : 'column',
}}
>
<SortableContext
items={[...containers, PLACEHOLDER_ID]}
strategy={
vertical
? verticalListSortingStrategy
: horizontalListSortingStrategy
}
>
{containers.map((containerId: any) => (
<DroppableContainer
key={containerId}
id={containerId}
label={`${intl.formatMessage({ id: 'quickDapp.column' })} ${containerId}`}
columns={columns}
items={items[containerId]}
style={containerStyle}
onRemove={() => handleRemove(containerId)}
>
<SortableContext items={items[containerId]} strategy={strategy}>
{items[containerId].map((value, index) => {
return (
<SortableItem
disabled={isSortingContainer}
key={value}
id={value}
abi={abi}
index={index}
handle={handle}
style={getItemStyles}
wrapperStyle={wrapperStyle}
containerId={containerId}
getIndex={getIndex}
onRemove={() => {
setItemsAndContainers({
...items,
[containerId]: items[containerId].filter(
(id) => id !== value
),
});
}}
/>
);
})}
</SortableContext>
</DroppableContainer>
))}
{containers.length < 3 && (
<DroppableContainer
id={PLACEHOLDER_ID}
key={PLACEHOLDER_ID}
disabled={isSortingContainer}
items={empty}
onClick={handleAddColumn}
placeholder
>
+ <FormattedMessage id='quickDapp.addColumn' />
</DroppableContainer>
)}
</SortableContext>
</div>
{createPortal(
<DragOverlay adjustScale={adjustScale} dropAnimation={dropAnimation}>
{activeId
? containers.includes(activeId)
? renderContainerDragOverlay(activeId)
: renderSortableItemDragOverlay(activeId)
: null}
</DragOverlay>,
document.body
)}
</DndContext>
);
function renderSortableItemDragOverlay(id: UniqueIdentifier) {
return (
<Item
id={id}
handle={handle}
style={getItemStyles({
containerId: findContainer(id) as UniqueIdentifier,
overIndex: -1,
index: getIndex(id),
value: id,
isSorting: true,
isDragging: true,
isDragOverlay: true,
})}
wrapperStyle={wrapperStyle({ index: 0 })}
dragOverlay
>
<ContractGUI funcABI={abi[id]} funcId={id} />
</Item>
);
}
function renderContainerDragOverlay(containerId: UniqueIdentifier) {
return (
<Container label={`Column ${containerId}`} columns={columns}>
{items[containerId].map((item, index) => (
<Item
key={item}
id={item}
handle={handle}
style={getItemStyles({
containerId,
overIndex: -1,
index: getIndex(item),
value: item,
isDragging: false,
isSorting: false,
isDragOverlay: false,
})}
wrapperStyle={wrapperStyle({ index })}
>
<ContractGUI funcABI={abi[item]} funcId={item} />
</Item>
))}
</Container>
);
}
function handleRemove(containerID: UniqueIdentifier) {
const newContainers = containers.filter((id: any) => id !== containerID);
const newItems: any = {};
newContainers.forEach((id: string) => {
newItems[id] = items[id];
});
setItemsAndContainers(newItems, newContainers);
}
function handleAddColumn() {
const newContainerId = getNextContainerId();
unstable_batchedUpdates(() => {
setItemsAndContainers(
{
...items,
[newContainerId]: [],
},
[...containers, newContainerId]
);
});
}
function getNextContainerId() {
const containerIds = Object.keys(items);
const lastContainerId = containerIds[containerIds.length - 1];
return String.fromCharCode(lastContainerId.charCodeAt(0) + 1);
}
}
interface SortableItemProps {
containerId: UniqueIdentifier;
id: UniqueIdentifier;
abi: any;
index: number;
handle: boolean;
disabled?: boolean;
style(args: any): React.CSSProperties;
getIndex(id: UniqueIdentifier): number;
wrapperStyle({ index }: { index: number }): React.CSSProperties;
onRemove?: () => void;
}
// The SortableItem component represents an individual item that can be dragged and dropped.
function SortableItem({
disabled,
id,
abi,
index,
handle,
style,
containerId,
getIndex,
wrapperStyle,
onRemove,
}: SortableItemProps) {
const {
setNodeRef,
setActivatorNodeRef,
listeners,
isDragging,
isSorting,
over,
overIndex,
transform,
transition,
} = useSortable({
id,
});
const mounted = useMountStatus();
const mountedWhileDragging = isDragging && !mounted;
return (
<Item
id={id}
ref={disabled ? undefined : setNodeRef}
dragging={isDragging}
sorting={isSorting}
handle={handle}
handleProps={handle ? { ref: setActivatorNodeRef } : undefined}
index={index}
wrapperStyle={wrapperStyle({ index })}
style={style({
index,
value: id,
isDragging,
isSorting,
overIndex: over ? getIndex(over.id) : overIndex,
containerId,
})}
transition={transition}
transform={transform}
fadeIn={mountedWhileDragging}
listeners={listeners}
onRemove={onRemove}
>
<ContractGUI funcABI={abi[id]} funcId={id} />
</Item>
);
}
// The useMountStatus function is used to track the mount status of a component.
function useMountStatus() {
const [isMounted, setIsMounted] = useState(false);
useEffect(() => {
const timeout = setTimeout(() => setIsMounted(true), 500);
return () => clearTimeout(timeout);
}, []);
return isMounted;
}

@ -0,0 +1,153 @@
import {
closestCorners,
getFirstCollision,
KeyboardCode,
DroppableContainer,
KeyboardCoordinateGetter,
} from '@dnd-kit/core';
// Define the directions that can be used with the keyboard
const directions: string[] = [
KeyboardCode.Down,
KeyboardCode.Right,
KeyboardCode.Up,
KeyboardCode.Left,
];
// This is a custom coordinate getter for keyboard events.
// It's used to handle keyboard navigation when moving items around in a drag and drop context.
// It determines the next position of an item based on the direction of the keyboard event.
// The function filters droppable containers based on their position relative to the currently dragged item,
// then finds the closest container in the direction of the keyboard event and returns its coordinates.
export const coordinateGetter: KeyboardCoordinateGetter = (
event,
{ context: { active, droppableRects, droppableContainers, collisionRect } }
) => {
// If the key pressed is one of the defined directions
if (directions.includes(event.code)) {
// Prevent the default browser behaviour
event.preventDefault();
// If there is no active draggable or collision rectangle, return
if (!active || !collisionRect) {
return;
}
// Create an array to store the droppable containers that meet the criteria
const filteredContainers: DroppableContainer[] = [];
// For each enabled droppable container
droppableContainers.getEnabled().forEach((entry) => {
// If the container is not defined or it is disabled, return
if (!entry || entry?.disabled) {
return;
}
// Get the rectangle of the droppable container
const rect = droppableRects.get(entry.id);
// If the rectangle is not defined, return
if (!rect) {
return;
}
// Get the data of the droppable container
const data = entry.data.current;
// If the data is defined
if (data) {
const { type, children } = data;
// If the droppable container is of type 'container' and it has children
if (type === 'container' && children?.length > 0) {
// If the active draggable is not of type 'container', return
if (active.data.current?.type !== 'container') {
return;
}
}
}
// Depending on the direction of the keyboard event
switch (event.code) {
// If the direction is down and the top of the collision rectangle is above the top of the container rectangle
case KeyboardCode.Down:
if (collisionRect.top < rect.top) {
// Add the container to the array of filtered containers
filteredContainers.push(entry);
}
break;
// If the direction is up and the top of the collision rectangle is below the top of the container rectangle
case KeyboardCode.Up:
if (collisionRect.top > rect.top) {
// Add the container to the array of filtered containers
filteredContainers.push(entry);
}
break;
// If the direction is left and the left of the collision rectangle is to the right of the right of the container rectangle
case KeyboardCode.Left:
if (collisionRect.left >= rect.left + rect.width) {
// Add the container to the array of filtered containers
filteredContainers.push(entry);
}
break;
// If the direction is right and the right of the collision rectangle is to the left of the left of the container rectangle
case KeyboardCode.Right:
if (collisionRect.left + collisionRect.width <= rect.left) {
// Add the container to the array of filtered containers
filteredContainers.push(entry);
}
break;
}
});
// Get the closest corners of the collision rectangle and the filtered containers
const collisions = closestCorners({
active,
collisionRect: collisionRect,
droppableRects,
droppableContainers: filteredContainers,
pointerCoordinates: null,
});
// Get the id of the first collision
const closestId = getFirstCollision(collisions, 'id');
// If there is a closest id
if (closestId != null) {
// Get the droppable container with the closest id
const newDroppable = droppableContainers.get(closestId);
// Get the node and rectangle of the droppable container
const newNode = newDroppable?.node.current;
const newRect = newDroppable?.rect.current;
// If there is a node and rectangle
if (newNode && newRect) {
// If the droppable container is the placeholder
if (newDroppable.id === 'placeholder') {
// Return the center coordinates of the droppable container
return {
x: newRect.left + (newRect.width - collisionRect.width) / 2,
y: newRect.top + (newRect.height - collisionRect.height) / 2,
};
}
// If the droppable container is of type 'container'
if (newDroppable.data.current?.type === 'container') {
// Return specific coordinates within the droppable container
return {
x: newRect.left + 20,
y: newRect.top + 74,
};
}
// Otherwise, return the top left coordinates of the droppable container
return {
x: newRect.left,
y: newRect.top,
};
}
}
}
// If none of the above conditions are met, return undefined
return undefined;
};

@ -0,0 +1,3 @@
import { createContext } from 'react'
export const AppContext = createContext<any>({})

@ -0,0 +1,13 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}

@ -0,0 +1,16 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>QuickDapp</title>
<link rel="stylesheet" integrity="ha384-50oBUHEmvpQ+1lW4y57PTFmhCaXp0ML5d60M1M7uH2+nqUivzIebhndOJK28anvf" crossorigin="anonymous" href="https://remix.ethereum.org/assets/fontawesome/css/all.css">
</head>
<body>
<script>
var global = window
</script>
<div id="root"></div>
</body>
</html>

@ -0,0 +1,9 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import './index.css';
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render(<App />);

@ -0,0 +1,7 @@
/**
* Polyfill stable language features. These imports will be optimized by `@babel/preset-env`.
*
* See: https://github.com/zloirock/core-js#babel
*/
import 'core-js/stable'
import 'regenerator-runtime/runtime'

@ -0,0 +1,19 @@
{
"name": "quick-dapp",
"displayName": "Quick Dapp",
"description": "Edit & deploy a Dapp",
"version": "1.0.0",
"methods": [
"edit"
],
"kind": "none",
"icon": "assets/img/quickDappLogo.webp",
"location": "mainPanel",
"url": "plugins/quick-dapp/index.html",
"repo": "https://github.com/ethereum/remix-project/tree/master/apps/quick-dapp",
"maintainedBy": "Remix",
"authorContact": "https://github.com/drafish",
"targets": [
"remix"
]
}

@ -0,0 +1,33 @@
export const appInitialState: any = {
loading: { screen: true },
instance: {
name: '',
address: '',
network: '',
abi: {},
items: {},
containers: [],
theme: 'Dark',
userInput: { methods: {} },
natSpec: { checked: false, methods: {} },
},
};
export const appReducer = (state = appInitialState, action: any): any => {
switch (action.type) {
case 'SET_LOADING':
return {
...state,
loading: { ...state.loading, ...action.payload },
};
case 'SET_INSTANCE':
return {
...state,
instance: { ...state.instance, ...action.payload },
};
default:
throw new Error();
}
};

@ -0,0 +1,24 @@
import { PluginClient } from '@remixproject/plugin';
import { createClient } from '@remixproject/plugin-webview';
import { initInstance } from './actions';
class RemixClient extends PluginClient {
constructor() {
super();
createClient(this);
}
edit({ address, abi, network, name, devdoc, methodIdentifiers, solcVersion }: any): void {
initInstance({
address,
abi,
network,
name,
devdoc,
methodIdentifiers,
solcVersion,
});
}
}
export default new RemixClient();

@ -0,0 +1,23 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"types": ["node"]
},
"files": [
"../../node_modules/@nrwl/react/typings/cssmodule.d.ts",
"../../node_modules/@nrwl/react/typings/image.d.ts"
],
"exclude": [
"jest.config.ts",
"**/*.spec.ts",
"**/*.test.ts",
"**/*.spec.tsx",
"**/*.test.tsx",
"**/*.spec.js",
"**/*.test.js",
"**/*.spec.jsx",
"**/*.test.jsx"
],
"include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"]
}

@ -0,0 +1,16 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"jsx": "react-jsx",
"allowJs": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true
},
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.app.json"
}
]
}

@ -0,0 +1,90 @@
const {composePlugins, withNx} = require('@nrwl/webpack')
const webpack = require('webpack')
const TerserPlugin = require('terser-webpack-plugin')
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin')
// Nx plugins for webpack.
module.exports = composePlugins(withNx(), (config) => {
// Update the webpack config as needed here.
// e.g. `config.plugins.push(new MyPlugin())`
// add fallback for node modules
config.resolve.fallback = {
...config.resolve.fallback,
crypto: require.resolve('crypto-browserify'),
stream: require.resolve('stream-browserify'),
path: require.resolve('path-browserify'),
http: require.resolve('stream-http'),
https: require.resolve('https-browserify'),
constants: require.resolve('constants-browserify'),
os: false, //require.resolve("os-browserify/browser"),
timers: false, // require.resolve("timers-browserify"),
zlib: require.resolve('browserify-zlib'),
fs: false,
module: false,
tls: false,
net: false,
readline: false,
child_process: false,
buffer: require.resolve('buffer/'),
vm: require.resolve('vm-browserify'),
}
// add externals
config.externals = {
...config.externals,
solc: 'solc',
}
// add public path
config.output.publicPath = './'
// add copy & provide plugin
config.plugins.push(
new webpack.ProvidePlugin({
Buffer: ['buffer', 'Buffer'],
url: ['url', 'URL'],
process: 'process/browser',
})
)
// set the define plugin to load the WALLET_CONNECT_PROJECT_ID
config.plugins.push(
new webpack.DefinePlugin({
WALLET_CONNECT_PROJECT_ID: JSON.stringify(process.env.WALLET_CONNECT_PROJECT_ID),
})
)
// souce-map loader
config.module.rules.push({
test: /\.js$/,
use: ['source-map-loader'],
enforce: 'pre',
})
config.ignoreWarnings = [/Failed to parse source map/] // ignore source-map-loader warnings
// set minimizer
config.optimization.minimizer = [
new TerserPlugin({
parallel: true,
terserOptions: {
ecma: 2015,
compress: false,
mangle: false,
format: {
comments: false,
},
},
extractComments: false,
}),
new CssMinimizerPlugin(),
]
config.watchOptions = {
ignored: /node_modules/,
}
config.experiments.syncWebAssembly = true
return config
})

@ -0,0 +1,70 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
"@dnd-kit/accessibility@^3.1.0":
version "3.1.0"
resolved "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.0.tgz#1054e19be276b5f1154ced7947fc0cb5d99192e0"
integrity sha512-ea7IkhKvlJUv9iSHJOnxinBcoOI3ppGnnL+VDJ75O45Nss6HtZd8IdN8touXPDtASfeI2T2LImb8VOZcL47wjQ==
dependencies:
tslib "^2.0.0"
"@dnd-kit/core@^6.1.0":
version "6.1.0"
resolved "https://registry.npmjs.org/@dnd-kit/core/-/core-6.1.0.tgz#e81a3d10d9eca5d3b01cbf054171273a3fe01def"
integrity sha512-J3cQBClB4TVxwGo3KEjssGEXNJqGVWx17aRTZ1ob0FliR5IjYgTxl5YJbKTzA6IzrtelotH19v6y7uoIRUZPSg==
dependencies:
"@dnd-kit/accessibility" "^3.1.0"
"@dnd-kit/utilities" "^3.2.2"
tslib "^2.0.0"
"@dnd-kit/sortable@^8.0.0":
version "8.0.0"
resolved "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-8.0.0.tgz#086b7ac6723d4618a4ccb6f0227406d8a8862a96"
integrity sha512-U3jk5ebVXe1Lr7c2wU7SBZjcWdQP+j7peHJfCspnA81enlu88Mgd7CC8Q+pub9ubP7eKVETzJW+IBAhsqbSu/g==
dependencies:
"@dnd-kit/utilities" "^3.2.2"
tslib "^2.0.0"
"@dnd-kit/utilities@^3.2.2":
version "3.2.2"
resolved "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz#5a32b6af356dc5f74d61b37d6f7129a4040ced7b"
integrity sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==
dependencies:
tslib "^2.0.0"
"@drafish/surge-client@^1.1.5":
version "1.1.5"
resolved "https://registry.npmjs.org/@drafish/surge-client/-/surge-client-1.1.5.tgz#7663f336dcd23bdc490deb9be01b9f83fab35e04"
integrity sha512-kWzs5PlnWDh4sl+WlNkbkNG+o3SNecW7xXvcM4WnQaQe6CB8anwXXmGtp5JEw3zJxKTKRB5W2xViyEszhh89ZQ==
dependencies:
buffer "^6.0.3"
pako "^2.1.0"
base64-js@^1.3.1:
version "1.5.1"
resolved "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
buffer@^6.0.3:
version "6.0.3"
resolved "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6"
integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==
dependencies:
base64-js "^1.3.1"
ieee754 "^1.2.1"
ieee754@^1.2.1:
version "1.2.1"
resolved "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==
pako@^2.1.0:
version "2.1.0"
resolved "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz#266cc37f98c7d883545d11335c00fbd4062c9a86"
integrity sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==
tslib@^2.0.0:
version "2.6.3"
resolved "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz#0438f810ad7a9edcde7a241c3d80db693c8cbfe0"
integrity sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==

@ -3,11 +3,20 @@ import EventEmitter from 'events'
class pinGrid extends EventEmitter {
command (this: NightwatchBrowser, provider: string, status: boolean): NightwatchBrowser {
this.api.useCss().waitForElementVisible('[data-id="settingsSelectEnvOptions"]')
.click('[data-id="settingsSelectEnvOptions"] button')
.waitForElementVisible(`[data-id="dropdown-item-another-chain"]`)
.click(`[data-id="dropdown-item-another-chain"]`)
.waitForElementVisible(`[data-id="${provider}-${status ? 'unpinned' : 'pinned'}"]`)
this.api.useCss()
.perform((done) => {
// check if the providers plugin is loaded.
this.api.isVisible({ selector: '[data-id="remixUIGSDeploy using a Browser Extension."]', suppressNotFoundErrors: true}, (result) => {
if (!result.value) {
this.api.waitForElementVisible('[data-id="settingsSelectEnvOptions"]')
.click('[data-id="settingsSelectEnvOptions"] button')
.waitForElementVisible(`[data-id="dropdown-item-another-chain"]`)
.click(`[data-id="dropdown-item-another-chain"]`)
.perform(() => done())
} else done()
})
})
.waitForElementVisible(`[data-id="${provider}-${status ? 'unpinned' : 'pinned'}"]`, 60000)
.click(`[data-id="${provider}-${status ? 'unpinned' : 'pinned'}"]`)
.perform((done) => {
done()

@ -2,7 +2,7 @@ import EventEmitter from 'events'
import { NightwatchBrowser } from 'nightwatch'
class RenamePath extends EventEmitter {
command (this: NightwatchBrowser, path: string, newFileName: string, renamedPath: string) {
command(this: NightwatchBrowser, path: string, newFileName: string, renamedPath: string) {
this.api.perform((done) => {
renamePath(this.api, path, newFileName, renamedPath, () => {
done()
@ -13,9 +13,9 @@ class RenamePath extends EventEmitter {
}
}
function renamePath (browser: NightwatchBrowser, path: string, newFileName: string, renamedPath: string, done: VoidFunction) {
function renamePath(browser: NightwatchBrowser, path: string, newFileName: string, renamedPath: string, done: VoidFunction) {
browser.execute(function (path: string) {
function contextMenuClick (element) {
function contextMenuClick(element) {
const evt = element.ownerDocument.createEvent('MouseEvents')
const RIGHT_CLICK_BUTTON_CODE = 2 // the same for FF and IE
@ -32,15 +32,18 @@ function renamePath (browser: NightwatchBrowser, path: string, newFileName: stri
}
contextMenuClick(document.querySelector('[data-path="' + path + '"]'))
}, [path], function () {
browser
.click('#menuitemrename')
.sendKeys('[data-input-path="' + path + '"]', newFileName)
.sendKeys('[data-input-path="' + path + '"]', browser.Keys.ENTER)
.waitForElementNotPresent('[data-path="' + path + '"]')
.waitForElementPresent('[data-path="' + renamedPath + '"]')
.perform(() => {
done()
})
try {
browser
.click('#menuitemrename')
.sendKeys('[data-input-path="' + path + '"]', newFileName)
.sendKeys('[data-input-path="' + path + '"]', browser.Keys.ENTER)
.waitForElementNotPresent('[data-path="' + path + '"]')
.waitForElementPresent('[data-path="' + renamedPath + '"]');
} catch (error) {
console.error('An error occurred:', error.message);
} finally {
done(); // Ensure done is called even if there's an error
}
})
}

@ -4,13 +4,35 @@ import EventEmitter from 'events'
class switchEnvironment extends EventEmitter {
command (this: NightwatchBrowser, provider: string): NightwatchBrowser {
this.api.useCss().waitForElementVisible('[data-id="settingsSelectEnvOptions"]')
.click('[data-id="settingsSelectEnvOptions"] button')
.waitForElementVisible(`[data-id="dropdown-item-${provider}"]`)
.click(`[data-id="dropdown-item-${provider}"]`)
.perform((done) => {
done()
this.emit('complete')
})
this.api.isPresent({ selector: `[data-id="selected-provider-${provider}"]`, suppressNotFoundErrors: true, timeout: 5000}, (result) => {
if (result.value) {
done()
} else {
browser.perform(() => {
this.api
.click('[data-id="settingsSelectEnvOptions"] button') // open dropdown
.isPresent({ selector: `[data-id="dropdown-item-${provider}"]`, suppressNotFoundErrors: true, timeout: 5000}, (result) => {
console.log(result)
this.api.click('[data-id="settingsSelectEnvOptions"] button') // close dropdown
if (!result.value) {
this.api.pinGrid(provider, true)
.click('[data-id="settingsSelectEnvOptions"] button')
.waitForElementVisible(`[data-id="dropdown-item-${provider}"]`)
.click(`[data-id="dropdown-item-${provider}"]`)
.perform(() => done())
} else {
browser.click('[data-id="settingsSelectEnvOptions"] button')
.waitForElementVisible(`[data-id="dropdown-item-${provider}"]`)
.click(`[data-id="dropdown-item-${provider}"]`)
.perform(() => done())
}
})
})
}
})
}).perform(() => this.emit('complete'))
return this
}
}

@ -241,6 +241,8 @@ module.exports = {
'check if the branch is in the filePanel #group2': function (browser: NightwatchBrowser) {
browser
.clickLaunchIcon('filePanel')
.waitForElementVisible('*[data-id="workspaceGitBranchesDropdown"]')
.pause(1000)
.click('[data-id="workspaceGitBranchesDropdown"]')
.expect.element('[data-id="workspaceGit-testbranch"]').text.to.contain('✓ ')
},
@ -314,18 +316,30 @@ module.exports = {
},
'switch back to master #group2': function (browser: NightwatchBrowser) {
browser
.waitForElementVisible({
selector: "//*[@data-id='branches-panel-content']//*[@data-id='branches-toggle-branch-master']",
locateStrategy: 'xpath',
})
.click({
selector: "//*[@data-id='branches-panel-content']//*[@data-id='branches-toggle-branch-master']",
locateStrategy: 'xpath',
})
.pause(1000)
.click({
selector: "//*[@data-id='branches-panel-content']//*[@data-id='branches-toggle-branch-master']",
locateStrategy: 'xpath',
abortOnFailure: false,
suppressNotFoundErrors: true
})
.waitForElementVisible({
selector: "//*[@data-id='branches-panel-content']//*[@data-id='branches-toggle-current-branch-master']",
locateStrategy: 'xpath',
timeout: 60000
})
},
'check if test file is gone #group2': function (browser: NightwatchBrowser) {
browser
.pause()
.pause(2000)
.clickLaunchIcon('filePanel')
.waitForElementNotPresent('*[data-id="treeViewLitreeViewItemtest.txt"]')
},

@ -32,5 +32,25 @@ module.exports = {
.click('[data-id="settingsSelectEnvOptions"] button')
.waitForElementNotPresent(`[data-id="dropdown-item-vm-sepolia-fork"]`)
.click('[data-id="settingsSelectEnvOptions"] button') // close the dropdown
},
'remember pin upon reload': function (browser: NightwatchBrowser) {
browser
.pinGrid('vm-paris', true)
.click('[data-id="settingsSelectEnvOptions"] button') // open the dropdown
.waitForElementPresent(`[data-id="dropdown-item-vm-paris"]`)
.refreshPage()
.waitForElementVisible('[data-id="treeViewLitreeViewItemcontracts"]') // wait loaded
.clickLaunchIcon('udapp')
.click('[data-id="settingsSelectEnvOptions"] button') // open the dropdown
.waitForElementPresent(`[data-id="dropdown-item-vm-paris"]`)
.click('[data-id="settingsSelectEnvOptions"] button') // close the dropdown
.pinGrid('vm-paris', false)
.click('[data-id="settingsSelectEnvOptions"] button') // open the dropdown
.waitForElementNotPresent(`[data-id="dropdown-item-vm-paris"]`)
.refreshPage()
.waitForElementVisible('[data-id="treeViewLitreeViewItemcontracts"]') // wait loaded
.clickLaunchIcon('udapp')
.click('[data-id="settingsSelectEnvOptions"] button') // open the dropdown
.waitForElementNotPresent(`[data-id="dropdown-item-vm-paris"]`)
}
}

@ -179,6 +179,14 @@ module.exports = {
},
'Should select another provider #group1': async function (browser: NightwatchBrowser) {
await browser
.frameParent()
.useCss()
.clickLaunchIcon('udapp')
.pinGrid('vm-berlin', true)
.clickLaunchIcon('localPlugin')
.useXpath()
.frame(0)
await clickAndCheckLog(browser, 'udapp:setEnvironmentMode', null, null, { context: 'vm-berlin' })
await browser
.frameParent()

@ -0,0 +1,322 @@
'use strict'
import { NightwatchBrowser } from 'nightwatch'
import path from 'path'
import axios from 'axios'
import crypto from 'crypto'
import init from '../helpers/init'
const passphrase = process.env.account_passphrase
const password = process.env.account_password
const extension_id = 'nkbihfbeogaeaoehlefnkodbefgpgknn'
const extension_url = `chrome-extension://${extension_id}/home.html`
const address = '0x3b3f6501A7fE68d22eFbc07d4424D4b9115C3038'
const surgeEmail = 'e2e@remix.org'
const surgePassword = 'remixe2e'
const surgeSubdomain = 'remixe2e'
const logoFilePath = path.resolve(__dirname, '../../../remix-ide/assets/img/remixLogo.webp')
const logoHash = 'ba8db45b3af49365bd482c7037dacaf1c549dc73c070ad963922adfeece4f37d'
const checkBrowserIsChrome = function (browser: NightwatchBrowser) {
return browser.browserName.indexOf('chrome') > -1
}
const tests = {
'@disabled': true,
before: function (browser: NightwatchBrowser, done: VoidFunction) {
init(browser, done)
},
'@sources': function () {
return sources
},
'Should connect to Sepolia Test Network using MetaMask #group1': function (browser: NightwatchBrowser) {
if (!checkBrowserIsChrome(browser)) return
browser.waitForElementPresent('*[data-id="remixIdeSidePanel"]')
.setupMetamask(passphrase, password)
.useCss().switchBrowserTab(0)
.refreshPage()
.waitForElementVisible('*[data-id="remixIdeIconPanel"]', 10000)
.click('*[data-id="landingPageStartSolidity"]')
.clickLaunchIcon('udapp')
.switchEnvironment('injected-MetaMask')
.waitForElementPresent('*[data-id="settingsNetworkEnv"]')
.assert.containsText('*[data-id="settingsNetworkEnv"]', 'Sepolia (11155111) network')
.pause(5000)
.switchBrowserWindow(extension_url, 'MetaMask', (browser) => {
browser
.waitForElementVisible('*[data-testid="page-container-footer-next"]', 60000)
.click('*[data-testid="page-container-footer-next"]') // this connects the metamask account to remix
.pause(2000)
.waitForElementVisible('*[data-testid="page-container-footer-next"]', 60000)
.click('*[data-testid="page-container-footer-next"]')
// .waitForElementVisible('*[data-testid="popover-close"]')
// .click('*[data-testid="popover-close"]')
})
.switchBrowserTab(0) // back to remix
},
'Should load quick-dapp plugin #group1': function (browser: NightwatchBrowser) {
if (!checkBrowserIsChrome(browser)) return
browser.waitForElementPresent('*[data-id="remixIdeSidePanel"]')
.useCss()
.addFile('Storage.sol', sources[0]['Storage.sol'])
.clickLaunchIcon('udapp')
.clickLaunchIcon('udapp')
.addAtAddressInstance(address, true, true, false)
.waitForElementPresent(`*[data-id="unpinnedInstance${address}"]`)
.clickInstance(0)
.click('*[data-id="instanceEditIcon"]')
.pause(5000)
.frame(0)
.assert.containsText('*[data-id="quick-dapp-admin"]', 'QuickDapp Admin')
},
'Should edit and deploy a dapp to surge.sh #group1': function (browser: NightwatchBrowser) {
if (!checkBrowserIsChrome(browser)) return
browser.click('.container.placeholder')
.perform((done) => {
browser.findElement('*[data-id="containerColumnC"]', (el: any) => {
browser.dragAndDrop('*[data-id="handle0x6057361d"]', el.value.getId())
.dragAndDrop('*[data-id="handleColumnA"]', el.value.getId())
.click('*[data-id="remove0x1003e2d2"]')
.click('*[data-id="removeColumnA"]')
.perform(() => done())
})
})
.setValue('input[data-id="surgeEmail"]', surgeEmail)
.setValue('input[data-id="surgePassword"]', surgePassword)
.setValue('input[data-id="surgeSubdomain"]', surgeSubdomain)
.setValue('input[data-id="functionTitle0x6057361d"]', 'Function Store Title')
.setValue('input[data-id="functionTitle0x2e64cec1"]', 'Function Retrive Title')
.execute((function() {
document.querySelector('input[data-id="uploadLogo"]').classList.remove('d-none');
}))
.setValue('input[data-id="uploadLogo"]', logoFilePath)
.execute((function() {
document.querySelector('input[data-id="uploadLogo"]').classList.add('d-none');
}))
.click('[for="shareToTwitter"]')
.click('[for="shareToFacebook"]')
.click('*[data-id="useNatSpec"]')
.click('[for="verifiedByEtherscan"]')
.click('[for="noTerminal"]')
.click('*[data-id="selectThemesOptions"]')
.click('*[data-id="dropdown-item-Light"]')
.click('*[data-id="deployDapp"]')
.waitForElementVisible('*[data-id="deployResult"]', 20000)
.perform((done) => {
browser.getAttribute('*[data-id="deployResult"]', 'class', function (result) {
// @ts-expect-error
if (result.value.includes('alert-danger')) {
browser.click('*[data-id="deployDapp"]').waitForElementVisible('*[data-id="deployResult"]', 20000).perform(() => done())
} else {
done()
}
})
})
.assert.containsText('*[data-id="deployResult"]', `https://${surgeSubdomain}.surge.sh`)
},
'Should load and call dapp successfully #group1': function (browser: NightwatchBrowser) {
if (!checkBrowserIsChrome(browser)) return
browser
.switchBrowserTab(1).url(`https://${surgeSubdomain}.surge.sh`)
.pause(5000)
.switchBrowserWindow(extension_url, 'MetaMask', (browser) => {
browser
.waitForElementVisible('*[data-testid="page-container-footer-next"]', 60000)
.click('*[data-testid="page-container-footer-next"]') // this connects the metamask account to remix
.pause(2000)
.waitForElementVisible('*[data-testid="page-container-footer-next"]', 60000)
.click('*[data-testid="page-container-footer-next"]')
// .waitForElementVisible('*[data-testid="popover-close"]')
// .click('*[data-testid="popover-close"]')
})
.switchBrowserTab(1)
.setValue('input[placeholder="uint256 num"]', '11')
.pause(1000)
.click('*[data-id="store - transact (not payable)"]')
.perform((done) => {
browser.switchBrowserWindow(extension_url, 'MetaMask', (browser) => {
browser
.isVisible({
selector: 'button[data-testid="popover-close"]',
locateStrategy: 'css selector',
suppressNotFoundErrors: true,
timeout: 3000
}, (okVisible) => {
console.log('okVisible', okVisible)
if (!okVisible.value) {
console.log('popover not found')
} else {
browser.click('button[data-testid="popover-close"]').click('.transaction-status-label--unapproved')
}
})
.waitForElementPresent('[data-testid="page-container-footer-next"]', 60000)
.click('[data-testid="page-container-footer-next"]') // approve the tx
.perform(() => done())
})
})
.switchBrowserTab(1) // back to dapp
.waitForElementVisible('.Toastify__toast--success', 60000)
.assert.containsText('.Toastify__toast--success', 'success')
.click('*[data-id="retrieve - call"]')
.waitForElementVisible('*[data-id="treeViewDiv0"]', 20000)
.assert.containsText('*[data-id="treeViewDiv0"]', 'uint256: 11')
.perform((done) => {
axios.get(`https://${surgeSubdomain}.surge.sh/logo.png?t=${new Date().getTime()}`, { responseType: 'arraybuffer' }).then((resp) => {
const hash = crypto.createHash('sha256');
hash.update(resp.data);
const hashValue = hash.digest('hex');
console.log('Hash:', hashValue);
browser.assert.strictEqual(hashValue, logoHash, 'Hash values match!').perform(() => done())
})
})
.assert.containsText('*[data-id="functionTitle0x6057361d"]', 'Function Store Title')
.assert.containsText('*[data-id="functionTitle0x2e64cec1"]', 'Function Retrive Title')
.assert.containsText('*[data-id="dappTitle"]', 'Storage')
.assert.containsText('*[data-id="dappInstructions"]', 'Store & retrieve value in a variable')
.assert.elementPresent('.fa-twitter.btn', 'Twitter icon should be present')
.assert.elementPresent('.fa-facebook.btn', 'Facebook icon should be present')
.checkElementStyle(':root', '--secondary', '#b3bcc483')
.element('css selector', '#terminal-view', function (result) {
browser.assert.strictEqual(result.status, -1, 'terminal should not shown')
})
.element('css selector', '*[data-id="function0x1003e2d2"]', function (result) {
browser.assert.strictEqual(result.status, -1, 'function add should not shown')
})
.getLocation('*[data-id="function0x6057361d"]', function (result: any) {
const funcStoreLocation = result.value
browser.getLocation('*[data-id="function0x2e64cec1"]', function (result: any) {
const funcRetriveLocation = result.value
browser.assert.strictEqual(funcStoreLocation.y, funcRetriveLocation.y, 'Both functions should be on the same horizontal line')
browser.assert.ok(funcStoreLocation.x > funcRetriveLocation.x, 'Function Store should be on the right of Function Retrive')
})
})
.getAttribute('a[data-id="viewSourceCode"]', 'href', function (result) {
browser.assert.strictEqual(result.value, `https://remix.ethereum.org/address/${address}`, 'view source code url should match')
})
},
'Should reset and delete and submit dapp params #group1': function (browser: NightwatchBrowser) {
if (!checkBrowserIsChrome(browser)) return
browser.switchBrowserTab(0).frame(0)
.click('*[data-id="resetFunctions"]')
.assert.elementPresent('*[data-id="remove0x1003e2d2"]', 'Function add should be present again')
.click('*[data-id="deleteDapp"]')
.assert.containsText('*[data-id="quickDappTooltips"]', 'QuickDapp only work for Injected Provider currently')
.setValue('input[id="formAddress"]', address)
.setValue('textarea[id="formAbi"]', abi)
.setValue('input[id="formName"]', 'Storage')
.setValue('input[id="formNetwork"]', 'Sepolia (11155111) network')
.click('*[data-id="createDapp"]')
.assert.containsText('*[data-id="quick-dapp-admin"]', 'QuickDapp Admin')
},
'Should teardown dapp successfully #group1': function (browser: NightwatchBrowser) {
if (!checkBrowserIsChrome(browser)) return
browser.setValue('input[data-id="surgeSubdomain"]', surgeSubdomain)
.execute((function() {
// @ts-expect-error
document.querySelector('*[data-id="teardownDapp"]').style.display = 'inline-block';
}))
.pause(500)
.click('*[data-id="teardownDapp"]')
.waitForElementVisible('*[data-id="teardownResult"]', 30000)
.assert.containsText('*[data-id="teardownResult"]', 'Teardown successfully!')
}
}
const branch = process.env.CIRCLE_BRANCH;
const isMasterBranch = branch === 'master';
module.exports = {
...(branch ? (isMasterBranch ? tests : {}) : tests),
};
const sources = [
{
'Storage.sol': {
content:
`
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.2 <0.9.0;
/**
* @title Storage
* @dev Store & retrieve value in a variable
* @custom:dev-run-script ./scripts/deploy_with_ethers.ts
*/
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;
}
function add(uint256 num) public {
number = number + num;
}
}`
}
}
]
const abi = JSON.stringify([
{
"inputs": [
{
"internalType": "uint256",
"name": "num",
"type": "uint256"
}
],
"name": "add",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "retrieve",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "num",
"type": "uint256"
}
],
"name": "store",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
}
])

@ -2,10 +2,9 @@
import { NightwatchBrowser } from 'nightwatch'
import init from '../helpers/init'
import { join } from 'path'
import { ChildProcess, spawn } from 'child_process'
import { ChildProcess, exec, spawn } from 'child_process'
import { homedir } from 'os'
import kill from 'tree-kill'
import treeKill from 'tree-kill'
let remixd: ChildProcess
const assetsTestContract = `import "./contract.sol";
@ -50,15 +49,30 @@ const sources = [
}
]
module.exports = {
'@disabled': true,
before: function (browser, done) {
init(browser, done)
},
after: function (browser) {
browser.perform((done) => {
console.log('remixd', remixd.pid)
kill(remixd.pid)
try {
console.log('remixd pid', remixd.pid);
treeKill(remixd.pid, 'SIGKILL', (err) => {
console.log('remixd killed', err)
})
console.log('Service disconnected successfully.');
} catch (error) {
console.error('Failed to disconnect service:', error);
}
try {
resetGitToHead()
} catch (error) {
console.error('Failed to restore git changes:', error);
}
done()
})
},
@ -66,12 +80,14 @@ module.exports = {
'@sources': function () {
return sources
},
'run Remixd tests #group1': function (browser) {
'run Remixd tests #group1': function (browser: NightwatchBrowser) {
browser.perform(async (done) => {
try {
remixd = await spawnRemixd(join(process.cwd(), '/apps/remix-ide', '/contracts'))
} catch (err) {
console.error(err)
// fail
browser.assert.fail('Failed to start remixd')
}
console.log('working directory', process.cwd())
connectRemixd(browser, done)
@ -86,7 +102,12 @@ module.exports = {
remix try to resolve it against the node_modules and installed_contracts folder.
*/
browser.perform(async (done) => {
try{
remixd = await spawnRemixd(join(process.cwd(), '/apps/remix-ide', '/contracts'))
} catch (err) {
console.error(err)
browser.assert.fail('Failed to start remixd')
}
console.log('working directory', process.cwd())
connectRemixd(browser, done)
})
@ -97,7 +118,12 @@ module.exports = {
},
'Import from node_modules and reference a github import #group3': function (browser) {
browser.perform(async (done) => {
try{
remixd = await spawnRemixd(join(process.cwd(), '/apps/remix-ide', '/contracts'))
} catch (err) {
console.error(err)
browser.assert.fail('Failed to start remixd')
}
console.log('working directory', process.cwd())
connectRemixd(browser, done)
})
@ -117,7 +143,12 @@ module.exports = {
'Should listen on compilation result from hardhat #group4': function (browser: NightwatchBrowser) {
browser.perform(async (done) => {
try{
remixd = await spawnRemixd(join(process.cwd(), '/apps/remix-ide/hardhat-boilerplate'))
} catch (err) {
console.error(err)
browser.assert.fail('Failed to start remixd')
}
console.log('working directory', process.cwd())
connectRemixd(browser, done)
})
@ -188,7 +219,12 @@ module.exports = {
browser.perform(async (done) => {
console.log('working directory', homedir() + '/foundry_tmp/hello_foundry')
try{
remixd = await spawnRemixd(join(homedir(), '/foundry_tmp/hello_foundry'))
} catch (err) {
console.error(err)
browser.assert.fail('Failed to start remixd')
}
connectRemixd(browser, done)
})
.perform(async (done) => {
@ -249,7 +285,12 @@ module.exports = {
'Should disable git when running remixd #group9': function (browser: NightwatchBrowser) {
browser.perform(async (done) => {
try{
remixd = await spawnRemixd(join(process.cwd(), '/apps/remix-ide', '/contracts/hardhat'))
} catch (err) {
console.error(err)
browser.assert.fail('Failed to start remixd')
}
console.log('working directory', process.cwd())
connectRemixd(browser, done)
})
@ -281,6 +322,7 @@ module.exports = {
remixd = await spawnRemixd(join(process.cwd(), '/apps/remix-ide', '/contracts'))
} catch (err) {
console.error(err)
browser.assert.fail('Failed to start remixd')
}
console.log('working directory', process.cwd())
connectRemixd(browser, done)
@ -322,8 +364,11 @@ function runTests(browser: NightwatchBrowser, done: any) {
.setEditorValue('contract test1Changed { function get () returns (uint) { return 10; }}')
.testEditorValue('contract test1Changed { function get () returns (uint) { return 10; }}')
.setEditorValue('contract test1 { function get () returns (uint) { return 10; }}')
.waitForElementVisible('[data-path="folder1"]')
.waitForElementVisible('[data-path="folder1/contract_' + browserName + '.sol"]')
.click('[data-path="folder1/contract_' + browserName + '.sol"]') // rename a file and check
.pause(1000)
.renamePath('folder1/contract_' + browserName + '.sol', 'renamed_contract_' + browserName, 'folder1/renamed_contract_' + browserName + '.sol')
.pause(1000)
.removeFile('folder1/contract_' + browserName + '_toremove.sol', 'localhost')
@ -558,3 +603,26 @@ async function installSlither(): Promise<void> {
console.log(e)
}
}
function resetGitToHead() {
if (process.env.CIRCLECI) {
console.log("Running on CircleCI, resetting Git to HEAD...");
} else {
console.log("Not running on CircleCI, skipping Git reset.");
return
}
const command = 'git reset --hard HEAD && git clean -fd';
exec(command, (error, stdout, stderr) => {
if (error) {
console.error(`Error executing command: ${command}\n${error.message}`);
return;
}
if (stderr) {
console.error(`Error output from command: ${command}\n${stderr}`);
return;
}
console.log(`Git reset to HEAD successfully.\n${stdout}`);
});
}

@ -163,16 +163,17 @@ module.exports = {
.click('.remixui_compilerConfigSection')
.setValue('#evmVersionSelector', 'london')
.click('*[data-id="compilerContainerCompileBtn"]')
.pause(5000)
.clickLaunchIcon('udapp')
.switchEnvironment('vm-london')
.clickLaunchIcon('filePanel')
.waitForElementVisible('*[data-id="treeViewLitreeViewItemscripts"]')
.click('*[data-id="treeViewLitreeViewItemscripts"]')
.waitForElementVisible('*[data-id="treeViewLitreeViewItemscripts/deploy_with_web3.ts"]')
.openFile('scripts/deploy_with_web3.ts')
.click('[data-id="play-editor"]')
.waitForElementPresent('[data-id="treeViewDivDraggableItem.states/vm-london/state.json"]')
.click('[data-id="treeViewDivDraggableItem.states/vm-london/state.json"]')
.pause(100000)
.pause(1000)
.getEditorValue((content) => {
browser
.assert.ok(content.includes('"latestBlockNumber": "0x1"'), 'State is saved')

@ -923,9 +923,9 @@ ejs@3.1.8:
jake "^10.8.5"
elliptic@^6.5.4:
version "6.5.4"
resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.4.tgz#da37cebd31e79a1367e941b592ed1fbebd58abbb"
integrity sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==
version "6.5.7"
resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.7.tgz#8ec4da2cb2939926a1b9a73619d768207e647c8b"
integrity sha512-ESVCtTwiA+XhY3wyh24QqRGBoP3rEdDUl3EDUUo9tft074fi19IrdpH7hLCMMP3CIj7jb3W96rn8lt/BqIlt5Q==
dependencies:
bn.js "^4.11.9"
brorand "^1.1.0"

@ -3,7 +3,7 @@
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "apps/remix-ide/src",
"projectType": "application",
"implicitDependencies": ["doc-gen", "doc-viewer", "etherscan", "vyper", "solhint", "walletconnect", "circuit-compiler", "learneth"],
"implicitDependencies": ["doc-gen", "doc-viewer", "etherscan", "vyper", "solhint", "walletconnect", "circuit-compiler", "learneth", "quick-dapp"],
"targets": {
"build": {
"executor": "@nrwl/webpack:webpack",

@ -0,0 +1,42 @@
{
"quickDapp.address": "address",
"quickDapp.enterAddress": "Enter address",
"quickDapp.enterAbi": "Enter abi",
"quickDapp.name": "name",
"quickDapp.enterName": "Enter name",
"quickDapp.network": "network",
"quickDapp.enterNetwork": "Enter network",
"quickDapp.submit": "Submit",
"quickDapp.text1": "QuickDapp only work for Injected Provider currently. More providers will be adapted in further iterations.",
"quickDapp.text2": "Click the edit icon in a deployed contract will input the parameters automatically.",
"quickDapp.admin": "Admin",
"quickDapp.resetFunctions": "Reset Functions",
"quickDapp.deleteDapp": "Delete Dapp",
"quickDapp.text3": "QuickDapp deploys to Surge.sh. Surge accounts are free until you reach a level of use. The email & password you input below will register you with a Surge account. The subdomain is your choice but it must be unique. More about <a>surge.sh</a>",
"quickDapp.email": "Email",
"quickDapp.surgeEmail": "Surge email",
"quickDapp.password": "Password",
"quickDapp.surgePassword": "Surge password",
"quickDapp.subdomain": "Subdomain",
"quickDapp.uniqueSubdomain": "Unique subdomain name",
"quickDapp.shareTo": "Share To (Optional)",
"quickDapp.useNatSpec": "Use NatSpec (Optional)",
"quickDapp.useNatSpecTooltip": "Retrieve info from the contract's NatSpec",
"quickDapp.verifiedByEtherscan": "Verified by Etherscan (Optional)",
"quickDapp.verified": "Verified",
"quickDapp.noTerminal": "No Terminal (Optional)",
"quickDapp.no": "No",
"quickDapp.themes": "Themes",
"quickDapp.deploy": "Deploy",
"quickDapp.teardown": "Teardown",
"quickDapp.text4": "Deployed successfully!",
"quickDapp.text5": "Click the link below to view your dapp",
"quickDapp.text6": "Teardown successfully!",
"quickDapp.uploadLogoTooltip": "Click here to change logo",
"quickDapp.dappTitle": "Dapp Title",
"quickDapp.dappInstructions": "Dapp Instructions",
"quickDapp.functionTitle": "Title of function",
"quickDapp.functionInstructions": "Instructions for function",
"quickDapp.addColumn": "Add column",
"quickDapp.column": "Column"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

@ -80,6 +80,7 @@ export class Blockchain extends Plugin {
providers: {[key: string]: VMProvider | InjectedProvider | NodeProvider}
transactionContextAPI: TransactionContextAPI
registeredPluginEvents: string[]
defaultPinnedProviders: string[]
pinnedProviders: string[]
// NOTE: the config object will need to be refactored out in remix-lib
@ -112,7 +113,8 @@ export class Blockchain extends Plugin {
this.networkcallid = 0
this.networkStatus = { network: { name: ' - ', id: ' - ' } }
this.registeredPluginEvents = []
this.pinnedProviders = ['vm-cancun', 'vm-shanghai', 'vm-mainnet-fork', 'vm-london', 'vm-berlin', 'vm-paris', 'walletconnect', 'injected-MetaMask', 'basic-http-provider', 'ganache-provider', 'hardhat-provider', 'foundry-provider']
this.defaultPinnedProviders = ['vm-cancun', 'vm-mainnet-fork', 'walletconnect', 'injected-MetaMask', 'basic-http-provider', 'hardhat-provider', 'foundry-provider']
this.pinnedProviders = []
this.setupEvents()
this.setupProviders()
}
@ -139,11 +141,25 @@ export class Blockchain extends Plugin {
this.on('environmentExplorer', 'providerPinned', (name, provider) => {
this.emit('shouldAddProvidertoUdapp', name, provider)
this.pinnedProviders.push(name)
this.call('config', 'setAppParameter', 'settings/pinned-providers', JSON.stringify(this.pinnedProviders))
})
this.on('environmentExplorer', 'providerUnpinned', (name, provider) => {
this.emit('shouldRemoveProviderFromUdapp', name, provider)
const index = this.pinnedProviders.indexOf(name)
this.pinnedProviders.splice(index, 1)
this.call('config', 'setAppParameter', 'settings/pinned-providers', JSON.stringify(this.pinnedProviders))
})
this.call('config', 'getAppParameter', 'settings/pinned-providers').then((providers) => {
if (!providers) {
this.call('config', 'setAppParameter', 'settings/pinned-providers', JSON.stringify(this.defaultPinnedProviders))
this.pinnedProviders = this.defaultPinnedProviders
} else {
this.pinnedProviders = JSON.parse(providers)
}
}).catch((error) => { console.log(error) })
}
onDeactivation() {

@ -92,7 +92,7 @@ let requiredModules = [ // services + layout views + system views
// dependentModules shouldn't be manually activated (e.g hardhat is activated by remixd)
const dependentModules = ['foundry', 'hardhat', 'truffle', 'slither']
const loadLocalPlugins = ['doc-gen', 'doc-viewer', 'etherscan', 'vyper', 'solhint', 'walletconnect', 'circuit-compiler', 'learneth']
const loadLocalPlugins = ['doc-gen', 'doc-viewer', 'etherscan', 'vyper', 'solhint', 'walletconnect', 'circuit-compiler', 'learneth', 'quick-dapp']
const sensitiveCalls = {
fileManager: ['writeFile', 'copyFile', 'rename', 'copyDir'],

@ -39,7 +39,7 @@ export function EnvironmentUI(props: EnvironmentProps) {
</a>
</CustomTooltip>
</label>
<div className="udapp_environment">
<div className="udapp_environment" data-id={`selected-provider-${currentProvider && currentProvider.name}`}>
<Dropdown id="selectExEnvOptions" data-id="settingsSelectEnvOptions" className="udapp_selectExEnvOptions">
<Dropdown.Toggle as={CustomToggle} id="dropdown-custom-components" className="btn btn-light btn-block w-100 d-inline-block border border-dark form-control" icon={null}>
{isL2(currentProvider && currentProvider.displayName)}

@ -300,6 +300,7 @@ export function UniversalDappUI(props: UdappProps) {
{props.exEnvironment && props.exEnvironment.startsWith('injected') && (
<CustomTooltip placement="top" tooltipClasses="text-nowrap" tooltipId="udapp_udappEditTooltip" tooltipText={<FormattedMessage id="udapp.tooltipTextEdit" />}>
<i
data-id="instanceEditIcon"
className="fas fa-edit pr-3"
onClick={() => {
props.editInstance(props.instance)

@ -356,13 +356,15 @@ export function RunTabUI(props: RunTabProps) {
getFuncABIInputs={getFuncABIValues}
exEnvironment={runTab.selectExEnv}
editInstance={(instance) => {
plugin.call('dapp-draft', 'edit', {
const { metadata, abi, object } = instance.contractData;
plugin.call('quick-dapp', 'edit', {
address: instance.address,
abi: instance.contractData.abi,
abi: abi,
name: instance.name,
network: runTab.networkName,
devdoc: instance.contractData.object.devdoc,
methodIdentifiers: instance.contractData.object.evm.methodIdentifiers,
devdoc: object.devdoc,
methodIdentifiers: object.evm.methodIdentifiers,
solcVersion: JSON.parse(metadata).compiler.version,
})
}}
/>

@ -320,7 +320,7 @@ export const TabsUI = (props: TabsUIProps) => {
>
<TabList className="d-flex flex-row align-items-center">
{props.tabs.map((tab, i) => (
<Tab className="" key={tab.name}>
<Tab className="" key={tab.name} data-id={tab.id}>
{renderTab(tab, i)}
</Tab>
))}

@ -27,7 +27,7 @@ import RenderUnKnownTransactions from './components/RenderUnknownTransactions' /
import RenderCall from './components/RenderCall' // eslint-disable-line
import RenderKnownTransactions from './components/RenderKnownTransactions' // eslint-disable-line
import parse from 'html-react-parser'
import { EMPTY_BLOCK, KNOWN_TRANSACTION, RemixUiTerminalProps, SET_ISVM, UNKNOWN_TRANSACTION } from './types/terminalTypes'
import { EMPTY_BLOCK, KNOWN_TRANSACTION, RemixUiTerminalProps, SET_ISVM, SET_OPEN, UNKNOWN_TRANSACTION } from './types/terminalTypes'
import { wrapScript } from './utils/wrapScript'
import { TerminalContext } from './context'
const _paq = (window._paq = window._paq || [])
@ -570,6 +570,7 @@ export const RemixUiTerminal = (props: RemixUiTerminalProps) => {
props.plugin.on('layout', 'change', (panels) => {
setIsOpen(!panels.terminal.minimized)
dispatch({ type: SET_OPEN, payload: !panels.terminal.minimized })
})
return () => {

Loading…
Cancel
Save