commit
bf589f8e68
@ -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; |
||||
} |
||||
} |
||||
}; |
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== |
@ -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" |
||||
} |
||||
]) |
@ -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" |
||||
} |
After Width: | Height: | Size: 3.4 KiB |
Loading…
Reference in new issue