email templates editing, syntax help and preview modals and actually rendering on submit.
This commit is contained in:
parent
0a2f727a3a
commit
ef0a3ddd67
|
@ -202,17 +202,23 @@ def custom_template_set(hashid):
|
|||
if not form.controlled_by(current_user):
|
||||
return jsonerror(401, {'error': "You do not control this form."})
|
||||
|
||||
# TODO catch render exception before deploying
|
||||
template = form.template
|
||||
if not template:
|
||||
template = EmailTemplate(form_id=form.id)
|
||||
|
||||
template.from_name = request.get_json()['from_name']
|
||||
template.subject = request.get_json()['subject']
|
||||
template.style = request.get_json()['style']
|
||||
template.body = request.get_json()['body']
|
||||
|
||||
try:
|
||||
pass
|
||||
except:
|
||||
template.sample()
|
||||
except Exception as e:
|
||||
print(e)
|
||||
return jsonerror(406, {'error': "Failed to render. The template has errors."})
|
||||
|
||||
print(form.template)
|
||||
|
||||
DB.session.add(form)
|
||||
DB.session.add(template)
|
||||
DB.session.commit()
|
||||
|
||||
return jsonify({'ok': True})
|
||||
|
||||
|
||||
|
@ -221,22 +227,14 @@ def custom_template_preview_render():
|
|||
if not current_user.has_feature('whitelabel'):
|
||||
return jsonerror(402, {'error': "Please upgrade your account."})
|
||||
|
||||
template = EmailTemplate.temporary(
|
||||
body, _ = EmailTemplate.make_sample(
|
||||
from_name=request.get_json()['from_name'],
|
||||
subject=request.get_json()['subject'],
|
||||
style=request.get_json()['style'],
|
||||
body=request.get_json()['body']
|
||||
body=request.get_json()['body'],
|
||||
)
|
||||
|
||||
return template.render_body(
|
||||
data={
|
||||
'name': 'Irwin Jones',
|
||||
'_replyto': 'i.jones@example.com',
|
||||
'message': 'Hello!\n\nThis is a preview message!'
|
||||
},
|
||||
host='example.com/',
|
||||
keys=['name', '_replyto', 'message'],
|
||||
now=datetime.datetime.utcnow().strftime('%I:%M %p UTC - %d %B %Y'),
|
||||
unconfirm_url='#'
|
||||
)
|
||||
return body
|
||||
|
||||
|
||||
@login_required
|
||||
|
|
|
@ -139,7 +139,7 @@ class Form(DB.Model):
|
|||
'counter': self.counter,
|
||||
'email': self.email,
|
||||
'host': self.host,
|
||||
'template': self.template,
|
||||
'template': self.template.serialize() if self.template else None,
|
||||
'features': {f: True for f in self.features},
|
||||
'confirm_sent': self.confirm_sent,
|
||||
'confirmed': self.confirmed,
|
||||
|
@ -193,6 +193,7 @@ class Form(DB.Model):
|
|||
next = next_url(referrer, data.get('_next'))
|
||||
spam = data.get('_gotcha', None)
|
||||
format = data.get('_format', None)
|
||||
fromname = None
|
||||
|
||||
# turn cc emails into array
|
||||
if cc:
|
||||
|
@ -291,10 +292,11 @@ class Form(DB.Model):
|
|||
unconfirm_url=unconfirm)
|
||||
|
||||
# if there's a custom email template we should use it
|
||||
if self.template and self.owner.has_feature('whitelabel'):
|
||||
html = self.template.render_body(
|
||||
if self.owner.has_feature('whitelabel') and self.template:
|
||||
html, subject = self.template.render_body_and_subject(
|
||||
data=data, host=self.host, keys=keys, now=now,
|
||||
unconfirm_url=unconfirm)
|
||||
fromname = self.template.from_name
|
||||
|
||||
# check if the user wants a new or old version of the email
|
||||
if format == 'plain':
|
||||
|
@ -330,6 +332,7 @@ class Form(DB.Model):
|
|||
text=text,
|
||||
html=html,
|
||||
sender=settings.DEFAULT_SENDER,
|
||||
fromname=fromname,
|
||||
reply_to=reply_to,
|
||||
cc=cc,
|
||||
headers={
|
||||
|
@ -532,26 +535,49 @@ class EmailTemplate(DB.Model):
|
|||
(self.id or 'with an id to be assigned', self.form_id)
|
||||
|
||||
@classmethod
|
||||
def temporary(cls, style, body):
|
||||
def make_sample(cls, style, body,
|
||||
from_name='Formspree Team',
|
||||
subject='New submission from {{ _host }}'):
|
||||
t = cls(0)
|
||||
t.from_name = from_name
|
||||
t.subject = subject
|
||||
t.style = style
|
||||
t.body = body
|
||||
return t
|
||||
return t.sample()
|
||||
|
||||
def render_subject(self, data):
|
||||
return pystache.render(self.subject, data)
|
||||
def sample(self):
|
||||
return self.render_body_and_subject(
|
||||
data={
|
||||
'name': 'Irwin Jones',
|
||||
'_replyto': 'i.jones@example.com',
|
||||
'message': 'Hello!\n\nThis is a preview message!'
|
||||
},
|
||||
host='example.com/',
|
||||
keys=['name', '_replyto', 'message'],
|
||||
now=datetime.datetime.utcnow().strftime('%I:%M %p UTC - %d %B %Y'),
|
||||
unconfirm_url='#'
|
||||
)
|
||||
|
||||
def render_body(self, data, host, keys, now, unconfirm_url):
|
||||
def serialize(self):
|
||||
return {
|
||||
'subject': self.subject,
|
||||
'from_name': self.from_name,
|
||||
'style': self.style,
|
||||
'body': self.body
|
||||
}
|
||||
|
||||
def render_body_and_subject(self, data, host, keys, now, unconfirm_url):
|
||||
data.update({
|
||||
'_fields': [{'field_name': f, 'field_value': data[f]} for f in keys],
|
||||
'_fields': [{'_name': f, '_value': data[f]} for f in keys],
|
||||
'_time': now,
|
||||
'_host': host
|
||||
})
|
||||
html = pystache.render(self.body, data)
|
||||
styled = '<style>' + self.style + '</style>' + html
|
||||
inlined = transform(styled)
|
||||
subject = pystache.render(self.subject, data)
|
||||
html = pystache.render('<style>' + self.style + '</style>' + self.body, data)
|
||||
print(html)
|
||||
inlined = transform(html)
|
||||
suffixed = inlined + '''<table width="100%"><tr><td>You are receiving this because you confirmed this email address on <a href="{service_url}">{service_name}</a>. If you don't remember doing that, or no longer wish to receive these emails, please remove the form on {host} or <a href="{unconfirm_url}">click here to unsubscribe</a> from this endpoint.</td></tr></table>'''.format(service_url=settings.SERVICE_URL, service_name=settings.SERVICE_NAME, host=host, unconfirm_url=unconfirm_url)
|
||||
return suffixed
|
||||
return suffixed, subject
|
||||
|
||||
|
||||
class Submission(DB.Model):
|
||||
|
|
|
@ -4,6 +4,7 @@ const toastr = window.toastr
|
|||
const fetch = window.fetch
|
||||
const React = require('react')
|
||||
const CodeMirror = require('react-codemirror2')
|
||||
const Modal = require('react-modal')
|
||||
require('codemirror/mode/xml/xml')
|
||||
require('codemirror/mode/css/css')
|
||||
|
||||
|
@ -13,48 +14,53 @@ module.exports = class FormSettings extends React.Component {
|
|||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
this.changeFrom = this.changeFrom.bind(this)
|
||||
this.changeFromName = this.changeFromName.bind(this)
|
||||
this.changeSubject = this.changeSubject.bind(this)
|
||||
this.changeStyle = this.changeStyle.bind(this)
|
||||
this.changeBody = this.changeBody.bind(this)
|
||||
this.preview = this.preview.bind(this)
|
||||
this.closePreview = this.closePreview.bind(this)
|
||||
this.showSyntax = this.showSyntax.bind(this)
|
||||
this.closeModal = this.closeModal.bind(this)
|
||||
this.revert = this.revert.bind(this)
|
||||
this.deploy = this.deploy.bind(this)
|
||||
|
||||
this.defaultValues = {
|
||||
from: 'Team Formspree',
|
||||
from_name: 'Team Formspree',
|
||||
subject: 'New submission from {{ host }}',
|
||||
style: `h1 {
|
||||
color: black;
|
||||
}
|
||||
.content {
|
||||
font-size: 20pt;
|
||||
font-size: 15pt;
|
||||
}`,
|
||||
body: `<div class="content">
|
||||
<h1>You've got a new submissions from {{ _replyto }}</h1>
|
||||
<h1>You've got a new submission from {{ _replyto }} on {{ _host }}</h1>
|
||||
|
||||
<table>
|
||||
{{# _fields }}
|
||||
<tr>
|
||||
<th>{{ field_name }}</th>
|
||||
<td>{{ field_value }}</td>
|
||||
<th>{{ _name }}</th>
|
||||
<td>{{ _value }}</td>
|
||||
</tr>
|
||||
{{/ _fields }}
|
||||
</table>
|
||||
</div>`
|
||||
</div>
|
||||
<p>This submission was sent on {{ _time }}.</p>
|
||||
<br>
|
||||
<hr>`
|
||||
}
|
||||
|
||||
this.state = {
|
||||
previewing: null,
|
||||
changes: {}
|
||||
changes: {},
|
||||
modal: null,
|
||||
previewHTML: null
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
let {form} = this.props
|
||||
|
||||
let {from, subject, style, body} = {
|
||||
let {from_name, subject, style, body} = {
|
||||
...this.defaultValues,
|
||||
...form.template,
|
||||
...this.state.changes
|
||||
|
@ -66,10 +72,14 @@ module.exports = class FormSettings extends React.Component {
|
|||
<FormDescription prefix="Whitelabel settings for" form={form} />
|
||||
<div className="container">
|
||||
<div className="col-1-6">
|
||||
<label htmlFor="from">From</label>
|
||||
<label htmlFor="from_name">From</label>
|
||||
</div>
|
||||
<div className="col-5-6">
|
||||
<input id="from" onChange={this.changeFrom} value={from} />
|
||||
<input
|
||||
id="from_name"
|
||||
onChange={this.changeFromName}
|
||||
value={from_name}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="container">
|
||||
|
@ -88,9 +98,9 @@ module.exports = class FormSettings extends React.Component {
|
|||
</div>
|
||||
</div>
|
||||
<div className="container">
|
||||
<div className="col-1-1">
|
||||
<label>
|
||||
Style
|
||||
<label className="row">
|
||||
<div className="col-1-1">Style</div>
|
||||
<div className="col-1-1">
|
||||
<CodeMirror.Controlled
|
||||
value={style}
|
||||
options={{
|
||||
|
@ -100,13 +110,20 @@ module.exports = class FormSettings extends React.Component {
|
|||
}}
|
||||
onBeforeChange={this.changeStyle}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<div className="container">
|
||||
<div className="col-1-1">
|
||||
<label>
|
||||
Body
|
||||
<label className="row">
|
||||
<div className="row">
|
||||
<div className="col-1-2">Body</div>
|
||||
<div className="col-1-2 right">
|
||||
<a href="#" onClick={this.showSyntax}>
|
||||
syntax quick reference
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-1-1">
|
||||
<CodeMirror.Controlled
|
||||
value={body}
|
||||
options={{
|
||||
|
@ -116,15 +133,14 @@ module.exports = class FormSettings extends React.Component {
|
|||
}}
|
||||
onBeforeChange={this.changeBody}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<div className="container">
|
||||
<div className="col-1-6">
|
||||
<button onClick={this.preview}>Preview</button>
|
||||
</div>
|
||||
<div className="col-2-6" />
|
||||
<div className="col-1-6">
|
||||
<div className="col-2-3 right">
|
||||
{Object.keys(this.state.changes).length > 0
|
||||
? 'changes pending'
|
||||
: null}
|
||||
|
@ -132,7 +148,7 @@ module.exports = class FormSettings extends React.Component {
|
|||
<div className="col-1-6">
|
||||
<button
|
||||
onClick={this.revert}
|
||||
disabled={Object.keys(this.state.changes).length > 0}
|
||||
disabled={Object.keys(this.state.changes).length === 0}
|
||||
>
|
||||
Revert
|
||||
</button>
|
||||
|
@ -140,21 +156,86 @@ module.exports = class FormSettings extends React.Component {
|
|||
<div className="col-1-6">
|
||||
<button
|
||||
onClick={this.deploy}
|
||||
disabled={Object.keys(this.state.changes).length > 0}
|
||||
disabled={Object.keys(this.state.changes).length === 0}
|
||||
>
|
||||
Deploy
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Modal
|
||||
contentLabel="Preview"
|
||||
isOpen={this.state.modal === 'preview'}
|
||||
onRequestClose={this.closeModal}
|
||||
className="dummy"
|
||||
overlayClassName="dummy"
|
||||
>
|
||||
<div id="whitelabel-preview-modal">
|
||||
<div
|
||||
className="container preview"
|
||||
dangerouslySetInnerHTML={{__html: this.state.previewHTML}}
|
||||
/>
|
||||
<div className="container right">
|
||||
<button onClick={this.closeModal}>OK</button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
<Modal
|
||||
contentLabel="Email Syntax"
|
||||
isOpen={this.state.modal === 'syntax'}
|
||||
onRequestClose={this.closeModal}
|
||||
className="dummy"
|
||||
overlayClassName="dummy"
|
||||
>
|
||||
<div>
|
||||
<div>
|
||||
<h2>Email Syntax</h2>
|
||||
<p>
|
||||
the email body can contain simple HTML that's valid in an email.
|
||||
No <span className="code"><script></span> or{' '}
|
||||
<span className="code"><style></span>
|
||||
tags can be included. For a list of recommended HTML tags see{' '}
|
||||
<a href="" target="_blank">
|
||||
the ContantContact guide to HTML in email
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
<p>
|
||||
The following special variables are recognized by Formspree,
|
||||
using the{' '}
|
||||
<a
|
||||
href="https://mustache.github.io/mustache.5.html"
|
||||
target="_blank"
|
||||
>
|
||||
mustache
|
||||
</a>{' '}
|
||||
template language.
|
||||
</p>
|
||||
<pre>
|
||||
{`
|
||||
{{ _time }} The formatted date and time of the submission.
|
||||
{{ _host }} The URL (without "https://") of where the form was submitted.
|
||||
{{ <fieldname> }} Any named input value in your form will be displayed.
|
||||
{{# _fields }} A list of all fields can be included.
|
||||
{{ _name }} Within the _fields block you can access the current field name…
|
||||
{{ _value }} … and field value.
|
||||
{{/ _fields }} Closes the _fields block.
|
||||
`.trim()}
|
||||
</pre>
|
||||
</div>
|
||||
<div className="container right">
|
||||
<button onClick={this.closeModal}>OK</button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
changeFrom(e) {
|
||||
changeFromName(e) {
|
||||
let value = e.target.value
|
||||
this.setState(state => {
|
||||
state.changes.from = value
|
||||
state.changes.from_name = value
|
||||
return state
|
||||
})
|
||||
}
|
||||
|
@ -181,11 +262,15 @@ module.exports = class FormSettings extends React.Component {
|
|||
})
|
||||
}
|
||||
|
||||
closePreview() {
|
||||
this.setState({previewing: null})
|
||||
closeModal() {
|
||||
this.setState({modal: null})
|
||||
}
|
||||
|
||||
async preview() {
|
||||
async preview(e) {
|
||||
e.preventDefault()
|
||||
|
||||
this.setState({modal: 'preview'})
|
||||
|
||||
let template = {
|
||||
...this.defaultValues,
|
||||
...this.props.form.template,
|
||||
|
@ -204,7 +289,7 @@ module.exports = class FormSettings extends React.Component {
|
|||
})
|
||||
let html = await resp.text()
|
||||
|
||||
this.setState({previewing: html})
|
||||
this.setState({previewHTML: html})
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
toastr.error(
|
||||
|
@ -213,6 +298,57 @@ module.exports = class FormSettings extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
revert() {}
|
||||
deploy() {}
|
||||
revert(e) {
|
||||
e.preventDefault()
|
||||
|
||||
this.setState({changes: {}})
|
||||
}
|
||||
|
||||
showSyntax(e) {
|
||||
e.preventDefault()
|
||||
this.setState({modal: 'syntax'})
|
||||
}
|
||||
|
||||
async deploy(e) {
|
||||
e.preventDefault()
|
||||
|
||||
try {
|
||||
let resp = await fetch(
|
||||
`/api-int/forms/${this.props.form.hashid}/whitelabel`,
|
||||
{
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({
|
||||
...this.defaultValues,
|
||||
...this.props.form.template,
|
||||
...this.state.changes
|
||||
}),
|
||||
credentials: 'same-origin',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}
|
||||
)
|
||||
let r = await resp.json()
|
||||
|
||||
if (!resp.ok || r.error) {
|
||||
toastr.warning(
|
||||
r.error
|
||||
? `Failed to save custom template: ${r.error}`
|
||||
: 'Failed to save custom template.'
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
toastr.success('Custom template saved.')
|
||||
this.props.onUpdate().then(() => {
|
||||
this.setState({changes: {}})
|
||||
})
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
toastr.error(
|
||||
'Failed to save custom template. See the console for more details.'
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,10 +2,14 @@
|
|||
|
||||
const render = require('react-dom').render
|
||||
const React = require('react') // eslint-disable-line no-unused-vars
|
||||
const Modal = require('react-modal')
|
||||
|
||||
const Dashboard = require('./Dashboard')
|
||||
|
||||
if (document.querySelector('body.forms.dashboard')) {
|
||||
let el = document.querySelector('.container.block')
|
||||
Modal.setAppElement(el)
|
||||
|
||||
document.querySelector('.menu .item:nth-child(2)').innerHTML = ''
|
||||
render(<Dashboard />, document.querySelector('.container.block'))
|
||||
render(<Dashboard />, el)
|
||||
}
|
||||
|
|
|
@ -50,6 +50,7 @@ def configure_routes(app):
|
|||
app.add_url_rule('/api-int/forms/<hashid>', view_func=fa.delete, methods=['DELETE'])
|
||||
app.add_url_rule('/api-int/forms/sitewide-check', view_func=fa.sitewide_check, methods=['POST'])
|
||||
app.add_url_rule('/api-int/forms/<hashid>/submissions/<submissionid>', view_func=fa.submission_delete, methods=['DELETE'])
|
||||
app.add_url_rule('/api-int/forms/<hashid>/whitelabel', view_func=fa.custom_template_set, methods=['PUT'])
|
||||
app.add_url_rule('/api-int/forms/whitelabel/preview', view_func=fa.custom_template_preview_render, methods=['POST'])
|
||||
|
||||
# Webhooks
|
||||
|
|
|
@ -190,6 +190,13 @@
|
|||
overflow-x: auto;
|
||||
}
|
||||
|
||||
a.button.export {
|
||||
width: 11em;
|
||||
text-align: center;
|
||||
display: inline-block;
|
||||
margin: 0 0 20px 20px;
|
||||
}
|
||||
|
||||
table {
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
|
@ -375,14 +382,8 @@
|
|||
top: -100%;
|
||||
z-index: 11;
|
||||
width: 770px;
|
||||
-webkit-transform: translate(0, -500%);
|
||||
-ms-transform: translate(0, -500%);
|
||||
transform: translate(0, -500%);
|
||||
-webkit-transition: -webkit-transform 0.3s ease-out;
|
||||
-moz-transition: -moz-transform 0.3s ease-out;
|
||||
-o-transition: -o-transform 0.3s ease-out;
|
||||
transition: transform 0.3s ease-out;
|
||||
|
||||
transition: transform 0.5s ease-out;
|
||||
padding: 0.3em 1.3em 1.3em 1.3em;
|
||||
|
||||
.x a {
|
||||
|
@ -500,20 +501,50 @@
|
|||
align-items: baseline;
|
||||
}
|
||||
|
||||
#from::after {
|
||||
#from_name::after {
|
||||
content: ' submissions@formspree.io';
|
||||
color: grey;
|
||||
}
|
||||
}
|
||||
|
||||
a.button.export {
|
||||
width: 11em;
|
||||
text-align: center;
|
||||
display: inline-block;
|
||||
margin: 0 0 20px 20px;
|
||||
#whitelabel-preview-modal {
|
||||
.preview {
|
||||
padding: 12px;
|
||||
margin: 12px;
|
||||
border: 5px dotted #444;
|
||||
}
|
||||
}
|
||||
|
||||
.ReactModal__Overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(255, 255, 255, 0.5);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
.ReactModal__Content {
|
||||
z-index: 11;
|
||||
position: absolute;
|
||||
top: 80px;
|
||||
background: #fefefe;
|
||||
border: #333333 solid 1px;
|
||||
border-radius: 5px;
|
||||
padding: 1.4em 1.4em 0.5em 1.4em;
|
||||
max-width: 900px;
|
||||
transform: translate(0, -500%);
|
||||
transition: transform 0.6s ease-out;
|
||||
}
|
||||
|
||||
&.ReactModal__Overlay--after-open .ReactModal__Content {
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
.CodeMirror {
|
||||
z-index: 0;
|
||||
padding: 10px;
|
||||
height: auto;
|
||||
font-size: 90%;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import os
|
||||
from premailer import transform
|
||||
from premailer import Premailer
|
||||
|
||||
TEMPLATES_DIR = 'formspree/templates/email/pre_inline_style/'
|
||||
|
||||
|
@ -8,7 +8,8 @@ def generate_templates():
|
|||
for filename in os.listdir(TEMPLATES_DIR):
|
||||
if filename.endswith('.html'):
|
||||
with open(os.path.join(TEMPLATES_DIR, filename), 'r') as html:
|
||||
transformed_template = transform(html.read())
|
||||
p = Premailer(html.read(), remove_classes=True)
|
||||
transformed_template = p.transform()
|
||||
|
||||
# weird issue with jinja templates beforehand so we use this hack
|
||||
# see https://github.com/peterbe/premailer/issues/72
|
||||
|
|
|
@ -118,7 +118,8 @@ def next_url(referrer=None, next=None):
|
|||
|
||||
|
||||
def send_email(to=None, subject=None, text=None, html=None,
|
||||
sender=None, cc=None, reply_to=None, headers=None):
|
||||
sender=None, cc=None, reply_to=None, headers=None,
|
||||
fromname=None):
|
||||
g.log = g.log.new(to=to, sender=sender)
|
||||
|
||||
if None in [to, subject, text, sender]:
|
||||
|
@ -142,6 +143,9 @@ def send_email(to=None, subject=None, text=None, html=None,
|
|||
except ValueError:
|
||||
data.update({'from': sender})
|
||||
|
||||
if fromname:
|
||||
data.update({'fromname': fromname})
|
||||
|
||||
if headers:
|
||||
data.update({'headers': json.dumps(headers)})
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
"react": "^16.4.2",
|
||||
"react-codemirror2": "^5.1.0",
|
||||
"react-dom": "^16.4.2",
|
||||
"react-modal": "^3.5.1",
|
||||
"react-router-dom": "^4.3.1",
|
||||
"url": "^0.11.0",
|
||||
"valid-url": "^1.0.9"
|
||||
|
|
Loading…
Reference in New Issue