Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
B
baas-ide
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
JIRA
JIRA
Merge Requests
1
Merge Requests
1
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Registry
Registry
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
guxukai
baas-ide
Commits
18691860
Commit
18691860
authored
Apr 09, 2019
by
Grandschtroumpf
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Remove plugin folder
parent
61029b8f
Hide whitespace changes
Inline
Side-by-side
Showing
7 changed files
with
0 additions
and
640 deletions
+0
-640
bundle.js
src/app/plugin/bundle.js
+0
-94
index.js
src/app/plugin/index.js
+0
-55
package.json
src/app/plugin/package.json
+0
-71
plugin.md
src/app/plugin/plugin.md
+0
-53
pluginAPI.js
src/app/plugin/pluginAPI.js
+0
-150
pluginManager.js
src/app/plugin/pluginManager.js
+0
-179
plugins.js
src/app/plugin/plugins.js
+0
-38
No files found.
src/app/plugin/bundle.js
deleted
100644 → 0
View file @
61029b8f
(
function
e
(
t
,
n
,
r
){
function
s
(
o
,
u
){
if
(
!
n
[
o
]){
if
(
!
t
[
o
]){
var
a
=
typeof
require
==
"function"
&&
require
;
if
(
!
u
&&
a
)
return
a
(
o
,
!
0
);
if
(
i
)
return
i
(
o
,
!
0
);
var
f
=
new
Error
(
"Cannot find module '"
+
o
+
"'"
);
throw
f
.
code
=
"MODULE_NOT_FOUND"
,
f
}
var
l
=
n
[
o
]
=
{
exports
:{}};
t
[
o
][
0
].
call
(
l
.
exports
,
function
(
e
){
var
n
=
t
[
o
][
1
][
e
];
return
s
(
n
?
n
:
e
)},
l
,
l
.
exports
,
e
,
t
,
n
,
r
)}
return
n
[
o
].
exports
}
var
i
=
typeof
require
==
"function"
&&
require
;
for
(
var
o
=
0
;
o
<
r
.
length
;
o
++
)
s
(
r
[
o
]);
return
s
})({
1
:[
function
(
require
,
module
,
exports
){
'use strict'
;
var
_createClass
=
function
()
{
function
defineProperties
(
target
,
props
)
{
for
(
var
i
=
0
;
i
<
props
.
length
;
i
++
)
{
var
descriptor
=
props
[
i
];
descriptor
.
enumerable
=
descriptor
.
enumerable
||
false
;
descriptor
.
configurable
=
true
;
if
(
"value"
in
descriptor
)
descriptor
.
writable
=
true
;
Object
.
defineProperty
(
target
,
descriptor
.
key
,
descriptor
);
}
}
return
function
(
Constructor
,
protoProps
,
staticProps
)
{
if
(
protoProps
)
defineProperties
(
Constructor
.
prototype
,
protoProps
);
if
(
staticProps
)
defineProperties
(
Constructor
,
staticProps
);
return
Constructor
;
};
}();
function
_classCallCheck
(
instance
,
Constructor
)
{
if
(
!
(
instance
instanceof
Constructor
))
{
throw
new
TypeError
(
"Cannot call a class as a function"
);
}
}
var
RemixExtension
=
function
()
{
function
RemixExtension
()
{
var
_this
=
this
;
_classCallCheck
(
this
,
RemixExtension
);
this
.
_notifications
=
{};
this
.
_pendingRequests
=
{};
this
.
_id
=
0
;
window
.
addEventListener
(
'message'
,
function
(
event
)
{
return
_this
.
_newMessage
(
event
);
},
false
);
}
_createClass
(
RemixExtension
,
[{
key
:
'listen'
,
value
:
function
listen
(
key
,
type
,
callback
)
{
if
(
!
this
.
_notifications
[
key
])
this
.
_notifications
[
key
]
=
{};
this
.
_notifications
[
key
][
type
]
=
callback
;
}
},
{
key
:
'call'
,
value
:
function
call
(
key
,
type
,
params
,
callback
)
{
this
.
_id
++
;
this
.
_pendingRequests
[
this
.
_id
]
=
callback
;
window
.
parent
.
postMessage
(
JSON
.
stringify
({
action
:
'request'
,
key
:
key
,
type
:
type
,
value
:
params
,
id
:
this
.
_id
}),
'*'
);
}
},
{
key
:
'_newMessage'
,
value
:
function
_newMessage
(
event
)
{
if
(
!
event
.
data
)
return
;
if
(
typeof
event
.
data
!==
'string'
)
return
;
var
msg
;
try
{
msg
=
JSON
.
parse
(
event
.
data
);
}
catch
(
e
)
{
return
console
.
log
(
'unable to parse data'
);
}
var
_msg
=
msg
,
action
=
_msg
.
action
,
key
=
_msg
.
key
,
type
=
_msg
.
type
,
value
=
_msg
.
value
;
if
(
action
===
'notification'
)
{
if
(
this
.
_notifications
[
key
]
&&
this
.
_notifications
[
key
][
type
])
{
this
.
_notifications
[
key
][
type
](
value
);
}
}
else
if
(
action
===
'response'
)
{
var
_msg2
=
msg
,
id
=
_msg2
.
id
,
error
=
_msg2
.
error
;
if
(
this
.
_pendingRequests
[
id
])
{
this
.
_pendingRequests
[
id
](
error
,
value
);
delete
this
.
_pendingRequests
[
id
];
}
}
}
}]);
return
RemixExtension
;
}();
if
(
window
)
window
.
RemixExtension
=
RemixExtension
;
if
(
module
&&
module
.
exports
)
module
.
exports
=
RemixExtension
;
},{}]},{},[
1
]);
src/app/plugin/index.js
deleted
100644 → 0
View file @
61029b8f
'use strict'
class
RemixExtension
{
constructor
()
{
this
.
_notifications
=
{}
this
.
_pendingRequests
=
{}
this
.
_id
=
0
window
.
addEventListener
(
'message'
,
(
event
)
=>
this
.
_newMessage
(
event
),
false
)
}
listen
(
key
,
type
,
callback
)
{
if
(
!
this
.
_notifications
[
key
])
this
.
_notifications
[
key
]
=
{}
this
.
_notifications
[
key
][
type
]
=
callback
}
call
(
key
,
type
,
params
,
callback
)
{
this
.
_id
++
this
.
_pendingRequests
[
this
.
_id
]
=
callback
window
.
parent
.
postMessage
(
JSON
.
stringify
({
action
:
'request'
,
key
,
type
,
value
:
params
,
id
:
this
.
_id
}),
'*'
)
}
_newMessage
(
event
)
{
if
(
!
event
.
data
)
return
if
(
typeof
event
.
data
!==
'string'
)
return
var
msg
try
{
msg
=
JSON
.
parse
(
event
.
data
)
}
catch
(
e
)
{
return
console
.
log
(
'unable to parse data'
)
}
const
{
action
,
key
,
type
,
value
}
=
msg
if
(
action
===
'notification'
)
{
if
(
this
.
_notifications
[
key
]
&&
this
.
_notifications
[
key
][
type
])
{
this
.
_notifications
[
key
][
type
](
value
)
}
}
else
if
(
action
===
'response'
)
{
const
{
id
,
error
}
=
msg
if
(
this
.
_pendingRequests
[
id
])
{
this
.
_pendingRequests
[
id
](
error
,
value
)
delete
this
.
_pendingRequests
[
id
]
}
}
}
}
if
(
window
)
window
.
RemixExtension
=
RemixExtension
if
(
module
&&
module
.
exports
)
module
.
exports
=
RemixExtension
src/app/plugin/package.json
deleted
100644 → 0
View file @
61029b8f
{
"name"
:
"remix-extension"
,
"version"
:
"0.0.1"
,
"description"
:
"Ethereum IDE and tools for the web"
,
"contributors"
:
[
{
"name"
:
"Yann Levreau"
,
"email"
:
"yann@ethdev.com"
}
],
"main"
:
"./index.js"
,
"dependencies"
:
{
"babel-eslint"
:
"^7.1.1"
,
"babel-plugin-transform-object-assign"
:
"^6.22.0"
,
"babel-preset-es2015"
:
"^6.24.0"
,
"babelify"
:
"^7.3.0"
,
"standard"
:
"^7.0.1"
,
"tape"
:
"^4.6.0"
},
"scripts"
:
{
"browserify"
:
"browserify index.js -o bundle.js"
},
"standard"
:
{
"ignore"
:
[
"node_modules/*"
],
"parser"
:
"babel-eslint"
},
"repository"
:
{
"type"
:
"git"
,
"url"
:
"git+https://github.com/ethereum/remix-ide.git"
},
"author"
:
"cpp-ethereum team"
,
"license"
:
"MIT"
,
"bugs"
:
{
"url"
:
"https://github.com/ethereum/remix-ide/issues"
},
"homepage"
:
"https://github.com/ethereum/remix-ide#readme"
,
"browserify"
:
{
"transform"
:
[
[
"babelify"
,
{
"plugins"
:
[
[
"fast-async"
,
{
"runtimePatten"
:
null
,
"compiler"
:
{
"promises"
:
true
,
"es7"
:
true
,
"noRuntime"
:
true
,
"wrapAwait"
:
true
}
}
],
"transform-object-assign"
]
}
],
[
"babelify"
,
{
"presets"
:
[
"es2015"
]
}
]
]
}
}
src/app/plugin/plugin.md
deleted
100644 → 0
View file @
61029b8f
plugin api
# current APIs:
## 1) notifications
### app (key: app)
-
unfocus
`[]`
-
focus
`[]`
### compiler (key: compiler)
-
compilationFinished
`[success (bool), data (obj), source (obj)]`
-
compilationData
`[compilationResult]`
### transaction listener (key: txlistener)
-
newTransaction
`tx (obj)`
## 2) interactions
### app
-
getExecutionContextProvider
`@return {String} provider (injected | web3 | vm)`
-
getProviderEndpoint
`@return {String} url`
-
updateTitle
`@param {String} title`
-
detectNetWork
`@return {Object} {name, id}`
-
addProvider
`@param {String} name, @param {String} url`
-
removeProvider
`@return {String} name`
### config
-
setConfig
`@param {String} path, @param {String} content`
-
getConfig
`@param {String} path`
-
removeConfig
`@param {String} path`
### compiler
-
getCompilationResult
`@return {Object} compilation result`
### udapp (only VM)
-
runTx
`@param {Object} tx`
-
getAccounts
`@return {Array} acccounts`
-
createVMAccount
`@param {String} privateKey, @param {String} balance (hex)`
### editor
-
getFilesFromPath
`@param {Array} [path]`
-
getCurrentFile
`@return {String} path`
-
getFile
`@param {String} path`
-
setFile
`@param {String} path, @param {String} content`
-
highlight
`@param {Object} lineColumnPos {start: {line, column}, end: {line, column}}, @param {String} path, @param {String} hexColor`
-
discardHighlight
src/app/plugin/pluginAPI.js
deleted
100644 → 0
View file @
61029b8f
'use strict'
var
executionContext
=
require
(
'../../execution-context'
)
var
SourceHighlighter
=
require
(
'../editor/sourceHighlighter'
)
/*
Defines available API. `key` / `type`
*/
module
.
exports
=
(
pluginManager
,
fileProviders
,
fileManager
,
compilesrArtefacts
,
udapp
)
=>
{
let
highlighters
=
{}
return
{
app
:
{
getExecutionContextProvider
:
(
mod
,
cb
)
=>
{
cb
(
null
,
executionContext
.
getProvider
())
},
getProviderEndpoint
:
(
mod
,
cb
)
=>
{
if
(
executionContext
.
getProvider
()
===
'web3'
)
{
cb
(
null
,
executionContext
.
web3
().
currentProvider
.
host
)
}
else
{
cb
(
'no endpoint: current provider is either injected or vm'
)
}
},
updateTitle
:
(
mod
,
title
,
cb
)
=>
{
pluginManager
.
plugins
[
mod
].
modal
.
setTitle
(
title
)
if
(
cb
)
cb
()
},
detectNetWork
:
(
mod
,
cb
)
=>
{
executionContext
.
detectNetwork
((
error
,
network
)
=>
{
cb
(
error
,
network
)
})
},
addProvider
:
(
mod
,
name
,
url
,
cb
)
=>
{
executionContext
.
addProvider
({
name
,
url
})
cb
()
},
removeProvider
:
(
mod
,
name
,
cb
)
=>
{
executionContext
.
removeProvider
(
name
)
cb
()
}
},
config
:
{
setConfig
:
(
mod
,
path
,
content
,
cb
)
=>
{
fileProviders
[
'config'
].
set
(
mod
+
'/'
+
path
,
content
)
cb
()
},
getConfig
:
(
mod
,
path
,
cb
)
=>
{
cb
(
null
,
fileProviders
[
'config'
].
get
(
mod
+
'/'
+
path
))
},
removeConfig
:
(
mod
,
path
,
cb
)
=>
{
cb
(
null
,
fileProviders
[
'config'
].
remove
(
mod
+
'/'
+
path
))
if
(
cb
)
cb
()
}
},
compiler
:
{
getCompilationResult
:
(
mod
,
cb
)
=>
{
cb
(
null
,
compilesrArtefacts
[
'__last'
])
},
sendCompilationResult
:
(
mod
,
file
,
source
,
languageVersion
,
data
,
cb
)
=>
{
pluginManager
.
receivedDataFrom
(
'sendCompilationResult'
,
mod
,
[
file
,
source
,
languageVersion
,
data
])
}
},
udapp
:
{
runTx
:
(
mod
,
tx
,
cb
)
=>
{
executionContext
.
detectNetwork
((
error
,
network
)
=>
{
if
(
error
)
return
cb
(
error
)
if
(
network
.
name
===
'Main'
&&
network
.
id
===
'1'
)
{
return
cb
(
'It is not allowed to make this action against mainnet'
)
}
udapp
.
silentRunTx
(
tx
,
(
error
,
result
)
=>
{
if
(
error
)
return
cb
(
error
)
cb
(
null
,
{
transactionHash
:
result
.
transactionHash
,
status
:
result
.
result
.
status
,
gasUsed
:
'0x'
+
result
.
result
.
gasUsed
.
toString
(
'hex'
),
error
:
result
.
result
.
vm
.
exceptionError
,
return
:
result
.
result
.
vm
.
return
?
'0x'
+
result
.
result
.
vm
.
return
.
toString
(
'hex'
)
:
'0x'
,
createdAddress
:
result
.
result
.
createdAddress
?
'0x'
+
result
.
result
.
createdAddress
.
toString
(
'hex'
)
:
undefined
})
})
})
},
getAccounts
:
(
mod
,
cb
)
=>
{
executionContext
.
detectNetwork
((
error
,
network
)
=>
{
if
(
error
)
return
cb
(
error
)
if
(
network
.
name
===
'Main'
&&
network
.
id
===
'1'
)
{
return
cb
(
'It is not allowed to make this action against mainnet'
)
}
udapp
.
getAccounts
(
cb
)
})
},
createVMAccount
:
(
mod
,
privateKey
,
balance
,
cb
)
=>
{
if
(
executionContext
.
getProvider
()
!==
'vm'
)
return
cb
(
'plugin API does not allow creating a new account through web3 connection. Only vm mode is allowed'
)
udapp
.
createVMAccount
(
privateKey
,
balance
,
(
error
,
address
)
=>
{
cb
(
error
,
address
)
})
}
},
editor
:
{
getFilesFromPath
:
(
mod
,
path
,
cb
)
=>
{
fileManager
.
filesFromPath
(
path
,
cb
)
},
getCurrentFile
:
(
mod
,
cb
)
=>
{
var
path
=
fileManager
.
currentFile
()
if
(
!
path
)
{
cb
(
'no file selected'
)
}
else
{
cb
(
null
,
path
)
}
},
getFile
:
(
mod
,
path
,
cb
)
=>
{
var
provider
=
fileManager
.
fileProviderOf
(
path
)
if
(
provider
)
{
// TODO add approval to user for external plugin to get the content of the given `path`
provider
.
get
(
path
,
(
error
,
content
)
=>
{
cb
(
error
,
content
)
})
}
else
{
cb
(
path
+
' not available'
)
}
},
setFile
:
(
mod
,
path
,
content
,
cb
)
=>
{
var
provider
=
fileManager
.
fileProviderOf
(
path
)
if
(
provider
)
{
// TODO add approval to user for external plugin to set the content of the given `path`
provider
.
set
(
path
,
content
,
(
error
)
=>
{
if
(
error
)
return
cb
(
error
)
fileManager
.
syncEditor
(
path
)
cb
()
})
}
else
{
cb
(
path
+
' not available'
)
}
},
highlight
:
(
mod
,
lineColumnPos
,
filePath
,
hexColor
,
cb
)
=>
{
var
position
try
{
position
=
JSON
.
parse
(
lineColumnPos
)
}
catch
(
e
)
{
return
cb
(
e
.
message
)
}
if
(
!
highlighters
[
mod
])
highlighters
[
mod
]
=
new
SourceHighlighter
()
highlighters
[
mod
].
currentSourceLocation
(
null
)
highlighters
[
mod
].
currentSourceLocationFromfileName
(
position
,
filePath
,
hexColor
)
cb
()
},
discardHighlight
:
(
mod
,
cb
)
=>
{
if
(
highlighters
[
mod
])
highlighters
[
mod
].
currentSourceLocation
(
null
)
cb
()
}
}
}
}
src/app/plugin/pluginManager.js
deleted
100644 → 0
View file @
61029b8f
'use strict'
var
remixLib
=
require
(
'remix-lib'
)
var
EventManager
=
remixLib
.
EventManager
const
PluginAPI
=
require
(
'./pluginAPI'
)
/**
* Register and Manage plugin:
*
* Plugin registration is done in the settings tab,
* using the following format:
* {
* "title": "<plugin name>",
* "url": "<plugin url>"
* }
*
* structure of messages:
*
* - Notification sent by Remix:
*{
* action: 'notification',
* key: <string>,
* type: <string>,
* value: <array>
*}
*
* - Request sent by the plugin:
*{
* id: <number>,
* action: 'request',
* key: <string>,
* type: <string>,
* value: <array>
*}
*
* - Response sent by Remix and receive by the plugin:
*{
* id: <number>,
* action: 'response',
* key: <string>,
* type: <string>,
* value: <array>,
* error: (see below)
*}
* => The `error` property is `undefined` if no error happened.
* => In case of error (due to permission, system error, API error, etc...):
* error: { code, msg (optional), data (optional), stack (optional)
* => possible error code are still to be defined, but the generic one would be 500.
*
* Plugin receive 4 types of message:
* - focus (when he get focus)
* - unfocus (when he loose focus - is hidden)
* - compilationData (that is triggered just after a focus - and send the current compilation data or null)
* - compilationFinished (that is only sent to the plugin that has focus)
*
* Plugin can emit messages and receive response.
*
* CONFIG:
* - getConfig(filename). The data to send should be formatted like:
* {
* id: <requestid>,
* action: 'request',
* key: 'config',
* type: 'getConfig',
* value: ['filename.ext']
* }
* the plugin will reveice a response like:
* {
* id: <requestid>,
* action: 'response',
* key: 'config',
* type: 'getConfig',
* error,
* value: ['content of filename.ext']
* }
* same apply for the other call
* - setConfig(filename, content)
* - removeConfig
*
* See index.html and remix.js in test-browser folder for sample
*
*/
module
.
exports
=
class
PluginManager
{
constructor
(
app
,
compilersArtefacts
,
txlistener
,
fileProviders
,
fileManager
,
udapp
)
{
const
self
=
this
self
.
event
=
new
EventManager
()
var
pluginAPI
=
new
PluginAPI
(
this
,
fileProviders
,
fileManager
,
compilersArtefacts
,
udapp
)
self
.
_components
=
{
pluginAPI
}
self
.
plugins
=
{}
self
.
origins
=
{}
self
.
inFocus
fileManager
.
events
.
on
(
'currentFileChanged'
,
(
file
)
=>
{
self
.
broadcast
(
JSON
.
stringify
({
action
:
'notification'
,
key
:
'editor'
,
type
:
'currentFileChanged'
,
value
:
[
file
]
}))
})
txlistener
.
event
.
register
(
'newTransaction'
,
(
tx
)
=>
{
self
.
broadcast
(
JSON
.
stringify
({
action
:
'notification'
,
key
:
'txlistener'
,
type
:
'newTransaction'
,
value
:
[
tx
]
}))
})
window
.
addEventListener
(
'message'
,
(
event
)
=>
{
if
(
event
.
type
!==
'message'
)
return
var
extension
=
self
.
origins
[
event
.
origin
]
if
(
!
extension
)
return
function
response
(
key
,
type
,
callid
,
error
,
result
)
{
self
.
postToOrigin
(
event
.
origin
,
JSON
.
stringify
({
id
:
callid
,
action
:
'response'
,
key
:
key
,
type
:
type
,
error
:
error
,
value
:
[
result
]
}))
}
var
data
=
JSON
.
parse
(
event
.
data
)
data
.
value
.
unshift
(
extension
)
data
.
value
.
push
((
error
,
result
)
=>
{
response
(
data
.
key
,
data
.
type
,
data
.
id
,
error
,
result
)
})
if
(
pluginAPI
[
data
.
key
]
&&
pluginAPI
[
data
.
key
][
data
.
type
])
{
pluginAPI
[
data
.
key
][
data
.
type
].
apply
({},
data
.
value
)
}
else
{
response
(
data
.
key
,
data
.
type
,
data
.
id
,
`Endpoint
${
data
.
key
}
/
${
data
.
type
}
not present`
,
null
)
}
},
false
)
}
unregister
(
desc
)
{
const
self
=
this
self
.
_components
.
pluginAPI
.
editor
.
discardHighlight
(
desc
.
title
,
()
=>
{})
delete
self
.
plugins
[
desc
.
title
]
delete
self
.
origins
[
desc
.
url
]
}
register
(
desc
,
modal
,
content
)
{
const
self
=
this
self
.
plugins
[
desc
.
title
]
=
{
content
,
modal
,
origin
:
desc
.
url
}
self
.
origins
[
desc
.
url
]
=
desc
.
title
}
broadcast
(
value
)
{
for
(
var
plugin
in
this
.
plugins
)
{
this
.
post
(
plugin
,
value
)
}
}
postToOrigin
(
origin
,
value
)
{
if
(
this
.
origins
[
origin
])
{
this
.
post
(
this
.
origins
[
origin
],
value
)
}
}
receivedDataFrom
(
methodName
,
mod
,
argumentsArray
)
{
// TODO check whether 'mod' as right to do that
console
.
log
(
argumentsArray
)
this
.
event
.
trigger
(
methodName
,
argumentsArray
)
// forward to internal modules
this
.
broadcast
(
JSON
.
stringify
({
// forward to plugins
action
:
'notification'
,
key
:
mod
,
type
:
methodName
,
value
:
argumentsArray
}))
}
post
(
name
,
value
)
{
const
self
=
this
if
(
self
.
plugins
[
name
])
{
self
.
plugins
[
name
].
content
.
querySelector
(
'iframe'
).
contentWindow
.
postMessage
(
value
,
self
.
plugins
[
name
].
origin
)
}
}
}
src/app/plugin/plugins.js
deleted
100644 → 0
View file @
61029b8f
'use strict'
module
.
exports
=
{
'oraclize'
:
{
url
:
'https://remix-plugin.oraclize.it'
,
title
:
'Oraclize'
},
'solium'
:
{
url
:
'https://two-water.surge.sh'
,
title
:
'Solium'
},
'ethdoc'
:
{
url
:
'https://30400.swarm-gateways.net/bzz:/ethdoc.remixide.eth'
,
title
:
'Ethdoc'
},
'openzeppelin snippet'
:
{
url
:
'https://left-edge.surge.sh'
,
title
:
'Openzeppelin snippet'
},
'vyper'
:
{
url
:
'https://plugin.vyper.live'
,
title
:
'Vyper'
},
'slither/mythril'
:
{
url
:
'http://jittery-space.surge.sh'
,
title
:
'Slither/Mythril'
},
'pipeline'
:
{
url
:
'https://pipeline.pipeos.one'
,
title
:
'Pipeline'
}
/*
'etherscan-general': {
url: 'http://127.0.0.1:8081',
title: 'Etherscan-general'
}
*/
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment