parent
f913319a88
commit
f2cbc51ec2
@ -0,0 +1,3 @@ |
||||
{ |
||||
"extends": "../../.eslintrc.json", |
||||
} |
@ -0,0 +1,134 @@ |
||||
# Remix LearnEth Plugin |
||||
|
||||
## Available Scripts |
||||
|
||||
In the project directory, you can run: |
||||
|
||||
### `npm run serve:plugin --plugin=learneth` |
||||
|
||||
Runs the app in the development mode.\ |
||||
Open [http://localhost:2024](http://localhost:2024) to view it in the browser. |
||||
|
||||
The page will reload if you make edits.\ |
||||
You will also see any lint errors in the console. |
||||
|
||||
### `npm run build:plugin --plugin=learneth` |
||||
|
||||
Builds the app for production to the `dist/apps/learneth` folder.\ |
||||
It correctly bundles React in production mode and optimizes the build for the best performance. |
||||
|
||||
The build is minified and the filenames include the hashes.\ |
||||
Your app is ready to be deployed! |
||||
|
||||
## Loading the plugin in remix |
||||
|
||||
When testing with localhost you should use the HTTP version of either REMIX or REMIX ALPHA. Click on the plugin manager icon and |
||||
add the plugin 'Connect to a local plugin'. Your plugin will be at http://localhost:2024/. |
||||
|
||||
## Setting up the REMIX IDE for working with the plugin |
||||
|
||||
The plugin only works when a compiler environment is loaded as well, for example on the home screen of the IDE you select 'Solidity' or 'Vyper'. Without this the plugin |
||||
cannot compile and test files in the workshops. |
||||
|
||||
## Setting up your Github workshops repo |
||||
|
||||
You can create your own workshops that can be imported in the plugin. |
||||
When importing a github repo the plugin will look for a directory structure describing the workshops. |
||||
For example: https://github.com/ethereum/remix-workshops |
||||
|
||||
### Root directories |
||||
|
||||
Root directories are individual workshops, the name used will be the name of the workshop unless you override this with the name property in the config.yml. |
||||
|
||||
### README.md |
||||
|
||||
The readme in each directory contains an explanation of what the workshop is about. If an additional summary property is provided in the config.yml that will be used in the overview section of the plugin. |
||||
|
||||
### config.yml |
||||
|
||||
This config file contains metadata describing some properties of your workshop, for example |
||||
|
||||
``` |
||||
--- |
||||
id: someid |
||||
name: my workshop name |
||||
summary: something about this workshop |
||||
level: 4 |
||||
tags: |
||||
- solidity |
||||
- beginner |
||||
``` |
||||
|
||||
Level: a level of difficulty indicator ( 1 - 5 ) |
||||
|
||||
Tags: an array of tags |
||||
|
||||
id: this is used by the system to let REMIX call startTutorial(repo,branch,id). See below for more instructions. |
||||
|
||||
### Steps |
||||
|
||||
Each workshop contains what we call steps. |
||||
Each step is a directory containing: |
||||
|
||||
- a readme describing the step, what to do. |
||||
- sol files: |
||||
- these can be sol files and test sol files. The test files should be name yoursolname_test.sol |
||||
- ANSWER files: these are named yoursolname_answer.sol and can be used to show the solution or the correct answer. The plugin will load the |
||||
file in the IDE when a user clicks on 'Show Answer' |
||||
- js files |
||||
- vyper files |
||||
|
||||
## Functions to call the plugin from the IDE |
||||
|
||||
### Add a repository: |
||||
|
||||
``` |
||||
addRepository(repoName, branch) |
||||
``` |
||||
|
||||
### Start a tutorial |
||||
|
||||
``` |
||||
startTutorial(repoName,branch,id) |
||||
``` |
||||
|
||||
You don't need to add a separate addRepository before calling startTutorial, this call will also add the repo. |
||||
|
||||
_Parameters_ |
||||
|
||||
id: this can be two things: |
||||
|
||||
- type of number, it specifies the n-th tutorial in the list |
||||
- type of string, this refers to the ID parameter in the config.yml file in the tutorial |
||||
for example: |
||||
|
||||
``` |
||||
--- |
||||
id: basics |
||||
name: 1 Basics of Solidity |
||||
summary: Some basic functions explained |
||||
level: 4 |
||||
tags: |
||||
- solidity |
||||
``` |
||||
|
||||
### How to call these functions in the REMIX IDE |
||||
|
||||
``` |
||||
(function () { |
||||
try { |
||||
// You don't need to add a separate addRepository before calling startTutorial, this is just an example |
||||
remix.call('LearnEth', 'addRepository', "ethereum/remix-workshops", "master") |
||||
remix.call('LearnEth', 'startTutorial', "ethereum/remix-workshops", "master", "basics") |
||||
remix.call('LearnEth', 'startTutorial', "ethereum/remix-workshops", "master", 2) |
||||
} catch (e) { |
||||
console.log(e.message) |
||||
} |
||||
})() |
||||
``` |
||||
|
||||
Then call this in the REMIX console |
||||
|
||||
``` |
||||
remix.exeCurrent() |
||||
``` |
@ -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.1" |
||||
} |
||||
} |
@ -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,146 @@ |
||||
/* 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: rgba(0, 0, 0, 0.3); |
||||
|
||||
&:active { |
||||
background-color: rgba(255, 70, 70, 0.9); |
||||
} |
||||
|
||||
svg { |
||||
fill: #fff; |
||||
} |
||||
} |
||||
|
||||
.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(--action-background, rgba(0, 0, 0, 0.05)); |
||||
|
||||
svg { |
||||
fill: #6f7b88; |
||||
} |
||||
} |
||||
} |
||||
|
||||
svg { |
||||
overflow: visible; |
||||
fill: #919eab; |
||||
} |
||||
|
||||
&:active { |
||||
background-color: var(--background, rgba(0, 0, 0, 0.05)); |
||||
|
||||
svg { |
||||
fill: var(--fill, #788491); |
||||
} |
||||
} |
||||
|
||||
&:focus-visible { |
||||
outline: none; |
||||
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0), |
||||
0 0px 0px 2px #4c9ffe; |
||||
} |
||||
} |
||||
|
||||
.container { |
||||
flex-direction: column; |
||||
|
||||
&.placeholder { |
||||
justify-content: center; |
||||
align-items: center; |
||||
cursor: pointer; |
||||
} |
||||
|
||||
&:focus-visible { |
||||
border-color: transparent; |
||||
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0), 0 0px 0px 2px #4c9ffe; |
||||
} |
||||
} |
||||
|
||||
|
||||
.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; |
||||
color: #dfe1ea !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,56 @@ |
||||
import React, { useEffect, useReducer } from 'react'; |
||||
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 [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); |
||||
}); |
||||
}); |
||||
}, []); |
||||
return ( |
||||
<AppContext.Provider |
||||
value={{ |
||||
dispatch, |
||||
appState, |
||||
}} |
||||
> |
||||
{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 /> |
||||
</AppContext.Provider> |
||||
); |
||||
} |
||||
|
||||
export default App; |
@ -0,0 +1,320 @@ |
||||
import axios from 'axios'; |
||||
import { omitBy } from 'lodash'; |
||||
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({ |
||||
proxy: 'https://vercel-proxy-bice-six.vercel.app', |
||||
onError: (err: Error) => { |
||||
console.log(err); |
||||
}, |
||||
}); |
||||
|
||||
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(); |
||||
|
||||
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( |
||||
'https://remix-dapp.pages.dev/manifest.json' |
||||
); |
||||
const { src, file, css, assets } = data['index.html']; |
||||
const paths = [src, file, ...css, ...assets]; |
||||
|
||||
const instance = state.instance; |
||||
|
||||
const files: Record<string, string> = { |
||||
'dir/instance.json': JSON.stringify({ |
||||
...instance, |
||||
shortname: payload.shortname, |
||||
shareTo: payload.shareTo, |
||||
}), |
||||
}; |
||||
|
||||
// console.log(
|
||||
// JSON.stringify({
|
||||
// ...instance,
|
||||
// shareTo: payload.shareTo,
|
||||
// })
|
||||
// );
|
||||
|
||||
for (let index = 0; index < paths.length; index++) { |
||||
const path = paths[index]; |
||||
const resp = await axios.get(`https://remix-dapp.pages.dev/${path}`); |
||||
files[`dir/${path}`] = resp.data; |
||||
} |
||||
|
||||
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 (error) { |
||||
callback({ code: 'ERROR', error: 'this domain belongs to someone else' }); |
||||
return; |
||||
} |
||||
|
||||
callback({ code: 'SUCCESS', error: '' }); |
||||
return; |
||||
}; |
||||
|
||||
export const initInstance = async ({ |
||||
methodIdentifiers, |
||||
devdoc, |
||||
...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 = {}; |
||||
payload.abi.forEach((item: any) => { |
||||
if (item.type === 'function') { |
||||
item.id = encodeFunctionId(item); |
||||
abi[item.id] = 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 }; |
||||
await dispatch({ |
||||
type: 'SET_INSTANCE', |
||||
payload: { |
||||
...payload, |
||||
abi, |
||||
items, |
||||
containers: Object.keys(items), |
||||
natSpec, |
||||
}, |
||||
}); |
||||
}; |
||||
|
||||
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,128 @@ |
||||
import React, { useEffect, useState } from 'react'; |
||||
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 }) { |
||||
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 |
||||
className="form-control" |
||||
placeholder="Title of function" |
||||
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 |
||||
className="form-control" |
||||
placeholder="Instructions for function" |
||||
value={props.funcABI.details} |
||||
onChange={({ target: { value } }) => { |
||||
saveDetails({ id: props.funcABI.id, details: value }); |
||||
}} |
||||
/> |
||||
</div> |
||||
</div> |
||||
); |
||||
} |
@ -0,0 +1,100 @@ |
||||
import React, { useState } from 'react'; |
||||
import { Alert, Button, Form } from 'react-bootstrap'; |
||||
import { initInstance } from '../../actions'; |
||||
|
||||
const CreateInstance: React.FC = () => { |
||||
const [formVal, setFormVal] = useState({ |
||||
address: '', |
||||
abi: [], |
||||
name: '', |
||||
network: '', |
||||
}); |
||||
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">address</Form.Label> |
||||
<Form.Control |
||||
type="address" |
||||
placeholder="Enter address" |
||||
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="Enter abi" |
||||
value={formVal.abi.length > 0 ? JSON.stringify(formVal.abi) : ''} |
||||
onChange={(e) => { |
||||
let abi = []; |
||||
try { |
||||
abi = JSON.parse(e.target.value); |
||||
} catch (error) { |
||||
/* empty */ |
||||
} |
||||
setFormVal({ ...formVal, abi }); |
||||
}} |
||||
/> |
||||
</Form.Group> |
||||
|
||||
<Form.Group className="mb-2" controlId="formName"> |
||||
<Form.Label className="text-uppercase mb-0">name</Form.Label> |
||||
<Form.Control |
||||
type="name" |
||||
placeholder="Enter name" |
||||
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">network</Form.Label> |
||||
<Form.Control |
||||
type="network" |
||||
placeholder="Enter network" |
||||
value={formVal.network} |
||||
onChange={(e) => { |
||||
setFormVal({ ...formVal, network: e.target.value }); |
||||
}} |
||||
/> |
||||
</Form.Group> |
||||
<Button |
||||
variant="primary" |
||||
type="submit" |
||||
className="mt-2" |
||||
disabled={ |
||||
!formVal.address || |
||||
!formVal.name || |
||||
!formVal.network || |
||||
!formVal.abi.length |
||||
} |
||||
> |
||||
Submit |
||||
</Button> |
||||
<Alert className="mt-4" variant="info"> |
||||
Dapp Draft only work for Injected Provider currently. More providers |
||||
will be adapted in further iterations. |
||||
<br /> |
||||
Click the edit icon in a deployed contract will input the parameters |
||||
automatically. |
||||
</Alert> |
||||
<img src='./assets/edit-dapp.png' /> |
||||
</Form> |
||||
); |
||||
}; |
||||
|
||||
export default CreateInstance; |
@ -0,0 +1,258 @@ |
||||
import React, { useContext, useState } from 'react'; |
||||
import { Form, Button, Alert, InputGroup } from 'react-bootstrap'; |
||||
import { |
||||
deploy, |
||||
emptyInstance, |
||||
resetInstance, |
||||
getInfoFromNatSpec, |
||||
} from '../../actions'; |
||||
import { ThemeUI } from './theme'; |
||||
import { CustomTooltip } from '@remix-ui/helper'; |
||||
import { AppContext } from '../../contexts'; |
||||
|
||||
function DeployPanel(): JSX.Element { |
||||
const { appState, dispatch } = useContext(AppContext); |
||||
const { verified, natSpec } = 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, |
||||
}); |
||||
return ( |
||||
<div className="col-3 d-inline-block"> |
||||
<h3 className="mb-3">QuickDapp Admin</h3> |
||||
<Button |
||||
onClick={() => { |
||||
resetInstance(); |
||||
}} |
||||
> |
||||
Reset Functions |
||||
</Button> |
||||
<Button |
||||
className="ml-3" |
||||
onClick={() => { |
||||
emptyInstance(); |
||||
}} |
||||
> |
||||
Delete Dapp |
||||
</Button> |
||||
<Alert variant="info" className="my-2"> |
||||
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 target="_blank" href="https://surge.sh/help/"> |
||||
surge.sh |
||||
</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">Email</Form.Label> |
||||
<Form.Control |
||||
type="email" |
||||
placeholder="Surge email" |
||||
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">Password</Form.Label> |
||||
<Form.Control |
||||
type="password" |
||||
placeholder="Surge password" |
||||
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">Subdomain</Form.Label> |
||||
<InputGroup> |
||||
<InputGroup.Text>https://</InputGroup.Text>
|
||||
<Form.Control |
||||
type="subdomain" |
||||
placeholder="Unique subdomain name" |
||||
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"> |
||||
Share To (Optional) |
||||
</Form.Label> |
||||
<br /> |
||||
<div className="d-inline-flex align-items-center custom-control custom-checkbox"> |
||||
<input |
||||
id="inline-checkbox-1" |
||||
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="inline-checkbox-1" |
||||
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="inline-checkbox-2" |
||||
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="inline-checkbox-2" |
||||
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"> |
||||
Use NatSpec (Optional) |
||||
</Form.Label> |
||||
<br /> |
||||
<span |
||||
data-id="remix_ai_switch" |
||||
id="remix_ai_switch" |
||||
className="btn ai-switch pl-0 py-0" |
||||
onClick={async () => { |
||||
getInfoFromNatSpec(!natSpec.checked); |
||||
}} |
||||
> |
||||
<CustomTooltip |
||||
placement="top" |
||||
tooltipText="Retrieve info from the contract's NatSpec" |
||||
> |
||||
<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"> |
||||
Verified by Etherscan (Optional) |
||||
</Form.Label> |
||||
<div className="d-flex py-1 align-items-center custom-control custom-checkbox"> |
||||
<input |
||||
id="inline-checkbox-3" |
||||
className="form-check-input custom-control-input" |
||||
type="checkbox" |
||||
onChange={(e) => { |
||||
dispatch({ |
||||
type: 'SET_INSTANCE', |
||||
payload: { verified: e.target.checked }, |
||||
}); |
||||
}} |
||||
checked={verified} |
||||
/> |
||||
|
||||
<label |
||||
htmlFor="inline-checkbox-3" |
||||
className="m-0 form-check-label custom-control-label" |
||||
style={{ paddingTop: 1 }} |
||||
> |
||||
Verified |
||||
</label> |
||||
</div> |
||||
</Form.Group> |
||||
<ThemeUI /> |
||||
<Button |
||||
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> |
||||
)} |
||||
Deploy |
||||
</Button> |
||||
{deployState.code === 'SUCCESS' && ( |
||||
<Alert variant="success" className="mt-4"> |
||||
Deployed successfully! <br /> Click the link below to view your dapp |
||||
<br /> |
||||
<a |
||||
target="_blank" |
||||
href={`https://${formVal.subdomain}.surge.sh`} |
||||
>{`https://${formVal.subdomain}.surge.sh`}</a> |
||||
</Alert> |
||||
)} |
||||
{deployState.error && ( |
||||
<Alert variant="danger" className="mt-4"> |
||||
{deployState.error} |
||||
</Alert> |
||||
)} |
||||
</Form> |
||||
</div> |
||||
); |
||||
} |
||||
|
||||
export default DeployPanel; |
@ -0,0 +1,157 @@ |
||||
import { Ref, useContext, useEffect } from 'react'; |
||||
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); |
||||
}} |
||||
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">Themes</label> |
||||
<Dropdown className="w-100"> |
||||
<Dropdown.Toggle |
||||
as={CustomToggle} |
||||
id="dropdown-custom-components" |
||||
className="btn btn-light btn-block w-100 d-inline-block border border-dark form-control" |
||||
icon={''} |
||||
> |
||||
{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,76 @@ |
||||
import { useContext } from 'react'; |
||||
import { omitBy } from 'lodash'; |
||||
import { MultipleContainers } from '../MultipleContainers'; |
||||
import { AppContext } from '../../contexts'; |
||||
|
||||
function EditInstance(): JSX.Element { |
||||
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="mx-4 my-2 p-3 w-75 bg-light"> |
||||
<input |
||||
className="form-control" |
||||
placeholder="Dapp title" |
||||
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="mx-4 my-2 p-3 w-75 bg-light"> |
||||
<textarea |
||||
className="form-control" |
||||
placeholder="Dapp instructions" |
||||
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> |
||||
<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,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 opacity-100 z-3" |
||||
style={{ |
||||
top: 0, |
||||
backgroundColor: 'rgba(51, 51, 51, 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,69 @@ |
||||
import React, { forwardRef } from 'react'; |
||||
import { Handle, Remove } from '../Item'; |
||||
|
||||
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} /> |
||||
<Handle {...handleProps} /> |
||||
</div> |
||||
</div> |
||||
) : null} |
||||
{placeholder ? ( |
||||
children |
||||
) : ( |
||||
<ul className="p-0 m-0 list-unstyled" style={{ overflowY: 'auto' }}> |
||||
{children} |
||||
</ul> |
||||
)} |
||||
</div> |
||||
); |
||||
} |
||||
); |
@ -0,0 +1,2 @@ |
||||
export { Container } from './Container' |
||||
export type { Props as ContainerProps } from './Container' |
@ -0,0 +1,120 @@ |
||||
import React, { useEffect } from 'react'; |
||||
import type { DraggableSyntheticListeners } from '@dnd-kit/core'; |
||||
import type { Transform } from '@dnd-kit/utilities'; |
||||
import { Handle } from './components/Handle'; |
||||
|
||||
const removeIcon = ( |
||||
<svg |
||||
width="10" |
||||
height="10" |
||||
viewBox="0 0 22 22" |
||||
xmlns="http://www.w3.org/2000/svg" |
||||
> |
||||
<path d="M2.99998 -0.000206962C2.7441 -0.000206962 2.48794 0.0972617 2.29294 0.292762L0.292945 2.29276C-0.0980552 2.68376 -0.0980552 3.31682 0.292945 3.70682L7.58591 10.9998L0.292945 18.2928C-0.0980552 18.6838 -0.0980552 19.3168 0.292945 19.7068L2.29294 21.7068C2.68394 22.0978 3.31701 22.0978 3.70701 21.7068L11 14.4139L18.2929 21.7068C18.6829 22.0978 19.317 22.0978 19.707 21.7068L21.707 19.7068C22.098 19.3158 22.098 18.6828 21.707 18.2928L14.414 10.9998L21.707 3.70682C22.098 3.31682 22.098 2.68276 21.707 2.29276L19.707 0.292762C19.316 -0.0982383 18.6829 -0.0982383 18.2929 0.292762L11 7.58573L3.70701 0.292762C3.51151 0.0972617 3.25585 -0.000206962 2.99998 -0.000206962Z" /> |
||||
</svg> |
||||
); |
||||
|
||||
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; |
||||
} |
||||
|
||||
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, |
||||
...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} /> |
||||
</div> |
||||
<button |
||||
className={`d-flex justify-content-center align-items-center position-absolute border-0 rounded-circle item-remove`} |
||||
onClick={onRemove} |
||||
> |
||||
{removeIcon} |
||||
</button> |
||||
</div> |
||||
</li> |
||||
); |
||||
} |
||||
) |
||||
); |
@ -0,0 +1,31 @@ |
||||
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, |
||||
'--fill': active?.fill, |
||||
'--background': active?.background, |
||||
width: 12, |
||||
} as CSSProperties |
||||
} |
||||
/> |
||||
); |
||||
} |
||||
); |
@ -0,0 +1,2 @@ |
||||
export { Action } from './Action'; |
||||
export type { Props as ActionProps } from './Action'; |
@ -0,0 +1,20 @@ |
||||
import React, { forwardRef } from 'react'; |
||||
|
||||
import { Action, ActionProps } from '../Action'; |
||||
|
||||
export const Handle = forwardRef<HTMLButtonElement, ActionProps>( |
||||
(props, ref) => { |
||||
return ( |
||||
<Action |
||||
ref={ref} |
||||
cursor="grab" |
||||
data-cypress="draggable-handle" |
||||
{...props} |
||||
> |
||||
<svg viewBox="0 0 20 20" width="12"> |
||||
<path d="M7 2a2 2 0 1 0 .001 4.001A2 2 0 0 0 7 2zm0 6a2 2 0 1 0 .001 4.001A2 2 0 0 0 7 8zm0 6a2 2 0 1 0 .001 4.001A2 2 0 0 0 7 14zm6-8a2 2 0 1 0-.001-4.001A2 2 0 0 0 13 6zm0 2a2 2 0 1 0 .001 4.001A2 2 0 0 0 13 8zm0 6a2 2 0 1 0 .001 4.001A2 2 0 0 0 13 14z"></path> |
||||
</svg> |
||||
</Action> |
||||
); |
||||
} |
||||
); |
@ -0,0 +1 @@ |
||||
export { Handle } from './Handle'; |
@ -0,0 +1,19 @@ |
||||
import React from 'react'; |
||||
|
||||
import { Action, ActionProps } from '../Action'; |
||||
|
||||
export function Remove(props: ActionProps) { |
||||
return ( |
||||
<Action |
||||
{...props} |
||||
active={{ |
||||
fill: 'rgba(255, 70, 70, 0.95)', |
||||
background: 'rgba(255, 70, 70, 0.1)', |
||||
}} |
||||
> |
||||
<svg width="8" viewBox="0 0 22 22" xmlns="http://www.w3.org/2000/svg"> |
||||
<path d="M2.99998 -0.000206962C2.7441 -0.000206962 2.48794 0.0972617 2.29294 0.292762L0.292945 2.29276C-0.0980552 2.68376 -0.0980552 3.31682 0.292945 3.70682L7.58591 10.9998L0.292945 18.2928C-0.0980552 18.6838 -0.0980552 19.3168 0.292945 19.7068L2.29294 21.7068C2.68394 22.0978 3.31701 22.0978 3.70701 21.7068L11 14.4139L18.2929 21.7068C18.6829 22.0978 19.317 22.0978 19.707 21.7068L21.707 19.7068C22.098 19.3158 22.098 18.6828 21.707 18.2928L14.414 10.9998L21.707 3.70682C22.098 3.31682 22.098 2.68276 21.707 2.29276L19.707 0.292762C19.316 -0.0982383 18.6829 -0.0982383 18.2929 0.292762L11 7.58573L3.70701 0.292762C3.51151 0.0972617 3.25585 -0.000206962 2.99998 -0.000206962Z" /> |
||||
</svg> |
||||
</Action> |
||||
); |
||||
} |
@ -0,0 +1 @@ |
||||
export { Remove } from './Remove'; |
@ -0,0 +1,3 @@ |
||||
export { Action } from './Action'; |
||||
export { Handle } from './Handle'; |
||||
export { Remove } from './Remove'; |
@ -0,0 +1,2 @@ |
||||
export { Item } from './Item'; |
||||
export { Action, Handle, Remove } from './components'; |
@ -0,0 +1,3 @@ |
||||
export { Container } from './Container'; |
||||
export type { ContainerProps } from './Container'; |
||||
export { Item, Action, Handle, Remove } from './Item'; |
@ -0,0 +1,653 @@ |
||||
import React, { useCallback, useEffect, useRef, useState } from 'react'; |
||||
import { createPortal, unstable_batchedUpdates } from 'react-dom'; |
||||
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', |
||||
}; |
||||
|
||||
const animateLayoutChanges: AnimateLayoutChanges = (args) => |
||||
defaultAnimateLayoutChanges({ ...args, wasDragging: true }); |
||||
|
||||
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 ( |
||||
<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> |
||||
); |
||||
} |
||||
|
||||
const dropAnimation: DropAnimation = { |
||||
sideEffects: defaultDropAnimationSideEffects({ |
||||
styles: { |
||||
active: { |
||||
opacity: '0.5', |
||||
}, |
||||
}, |
||||
}), |
||||
}; |
||||
|
||||
type Items = Record<UniqueIdentifier, UniqueIdentifier[]>; |
||||
|
||||
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[] = []; |
||||
|
||||
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 [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={`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 |
||||
> |
||||
+ Add column |
||||
</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 |
||||
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]} /> |
||||
</Item> |
||||
); |
||||
} |
||||
|
||||
function renderContainerDragOverlay(containerId: UniqueIdentifier) { |
||||
return ( |
||||
<Container label={`Column ${containerId}`} columns={columns}> |
||||
{items[containerId].map((item, index) => ( |
||||
<Item |
||||
key={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]} /> |
||||
</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; |
||||
} |
||||
|
||||
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 |
||||
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]} /> |
||||
</Item> |
||||
); |
||||
} |
||||
|
||||
function useMountStatus() { |
||||
const [isMounted, setIsMounted] = useState(false); |
||||
|
||||
useEffect(() => { |
||||
const timeout = setTimeout(() => setIsMounted(true), 500); |
||||
|
||||
return () => clearTimeout(timeout); |
||||
}, []); |
||||
|
||||
return isMounted; |
||||
} |
@ -0,0 +1,114 @@ |
||||
import { |
||||
closestCorners, |
||||
getFirstCollision, |
||||
KeyboardCode, |
||||
DroppableContainer, |
||||
KeyboardCoordinateGetter, |
||||
} from '@dnd-kit/core'; |
||||
|
||||
const directions: string[] = [ |
||||
KeyboardCode.Down, |
||||
KeyboardCode.Right, |
||||
KeyboardCode.Up, |
||||
KeyboardCode.Left, |
||||
]; |
||||
|
||||
export const coordinateGetter: KeyboardCoordinateGetter = ( |
||||
event, |
||||
{ context: { active, droppableRects, droppableContainers, collisionRect } } |
||||
) => { |
||||
if (directions.includes(event.code)) { |
||||
event.preventDefault(); |
||||
|
||||
if (!active || !collisionRect) { |
||||
return; |
||||
} |
||||
|
||||
const filteredContainers: DroppableContainer[] = []; |
||||
|
||||
droppableContainers.getEnabled().forEach((entry) => { |
||||
if (!entry || entry?.disabled) { |
||||
return; |
||||
} |
||||
|
||||
const rect = droppableRects.get(entry.id); |
||||
|
||||
if (!rect) { |
||||
return; |
||||
} |
||||
|
||||
const data = entry.data.current; |
||||
|
||||
if (data) { |
||||
const { type, children } = data; |
||||
|
||||
if (type === 'container' && children?.length > 0) { |
||||
if (active.data.current?.type !== 'container') { |
||||
return; |
||||
} |
||||
} |
||||
} |
||||
|
||||
switch (event.code) { |
||||
case KeyboardCode.Down: |
||||
if (collisionRect.top < rect.top) { |
||||
filteredContainers.push(entry); |
||||
} |
||||
break; |
||||
case KeyboardCode.Up: |
||||
if (collisionRect.top > rect.top) { |
||||
filteredContainers.push(entry); |
||||
} |
||||
break; |
||||
case KeyboardCode.Left: |
||||
if (collisionRect.left >= rect.left + rect.width) { |
||||
filteredContainers.push(entry); |
||||
} |
||||
break; |
||||
case KeyboardCode.Right: |
||||
if (collisionRect.left + collisionRect.width <= rect.left) { |
||||
filteredContainers.push(entry); |
||||
} |
||||
break; |
||||
} |
||||
}); |
||||
|
||||
const collisions = closestCorners({ |
||||
active, |
||||
collisionRect: collisionRect, |
||||
droppableRects, |
||||
droppableContainers: filteredContainers, |
||||
pointerCoordinates: null, |
||||
}); |
||||
const closestId = getFirstCollision(collisions, 'id'); |
||||
|
||||
if (closestId != null) { |
||||
const newDroppable = droppableContainers.get(closestId); |
||||
const newNode = newDroppable?.node.current; |
||||
const newRect = newDroppable?.rect.current; |
||||
|
||||
if (newNode && newRect) { |
||||
if (newDroppable.id === 'placeholder') { |
||||
return { |
||||
x: newRect.left + (newRect.width - collisionRect.width) / 2, |
||||
y: newRect.top + (newRect.height - collisionRect.height) / 2, |
||||
}; |
||||
} |
||||
|
||||
if (newDroppable.data.current?.type === 'container') { |
||||
return { |
||||
x: newRect.left + 20, |
||||
y: newRect.top + 74, |
||||
}; |
||||
} |
||||
|
||||
return { |
||||
x: newRect.left, |
||||
y: newRect.top, |
||||
}; |
||||
} |
||||
} |
||||
} |
||||
|
||||
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,32 @@ |
||||
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 }: any): void { |
||||
// console.log(
|
||||
// 'edit dapp',
|
||||
// address,
|
||||
// abi,
|
||||
// network,
|
||||
// name,
|
||||
// devdoc,
|
||||
// methodIdentifiers
|
||||
// );
|
||||
initInstance({ |
||||
address, |
||||
abi, |
||||
network, |
||||
name, |
||||
devdoc, |
||||
methodIdentifiers, |
||||
}); |
||||
} |
||||
} |
||||
|
||||
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.1": |
||||
version "1.1.1" |
||||
resolved "https://registry.npmjs.org/@drafish/surge-client/-/surge-client-1.1.1.tgz#c457f0988e2c11a6867fa42c0189d8f0fa3d3511" |
||||
integrity sha512-xTRCZfeAaoImYAV72YStozKcMjYs9RhKgLqEvXDx6QyHSI9udiSUyBxtYtt1ymwiF8qBxLwdVcjIUTI7pg+qnA== |
||||
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== |
After Width: | Height: | Size: 3.4 KiB |
Loading…
Reference in new issue