Update to add dynamic settings to email plugin

Email plugin now functions more like the upload plugin. Users can go to the plugins page, click on the settings cog for the email, and switch between providers or change settings. The default provider is strapi-email-sendmail. Extra providers can be added by installing strapi-email-mailgun or strapi-email-sendgrid.
This commit is contained in:
Austin Bratcher 2018-05-31 14:29:00 -05:00
parent cbb97f22eb
commit ddc01eb27b
58 changed files with 2973 additions and 69 deletions

View File

@ -32,14 +32,15 @@ class Row extends React.Component {
}
render() {
const uploadPath = `/plugins/upload/configurations/${this.context.currentEnvironment}`;
const icons = this.props.name === 'upload' ? [
// const uploadPath = `/plugins/upload/configurations/${this.context.currentEnvironment}`;
const settingsPath = `/plugins/${this.props.name}/configurations/${this.context.currentEnvironment}`;
const icons = this.props.name === 'upload' || this.props.name === 'email' ? [
{
icoType: 'cog',
onClick: (e) => {
e.preventDefault();
e.stopPropagation();
this.context.router.history.push(uploadPath);
this.context.router.history.push(settingsPath);
},
},
{

View File

@ -0,0 +1,16 @@
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[{package.json,*.yml}]
indent_style = space
indent_size = 2
[*.md]
trim_trailing_whitespace = false

View File

@ -0,0 +1,95 @@
############################
# OS X
############################
.DS_Store
.AppleDouble
.LSOverride
Icon
.Spotlight-V100
.Trashes
._*
############################
# Linux
############################
*~
############################
# Windows
############################
Thumbs.db
ehthumbs.db
Desktop.ini
$RECYCLE.BIN/
*.cab
*.msi
*.msm
*.msp
############################
# Packages
############################
*.7z
*.csv
*.dat
*.dmg
*.gz
*.iso
*.jar
*.rar
*.tar
*.zip
*.com
*.class
*.dll
*.exe
*.o
*.seed
*.so
*.swo
*.swp
*.swn
*.swm
*.out
*.pid
############################
# Logs and databases
############################
.tmp
*.log
*.sql
*.sqlite
############################
# Misc.
############################
*#
.idea
nbproject
############################
# Node.js
############################
lib-cov
lcov.info
pids
logs
results
build
node_modules
.node_history
package-lock.json

View File

@ -0,0 +1,104 @@
############################
# OS X
############################
.DS_Store
.AppleDouble
.LSOverride
Icon
.Spotlight-V100
.Trashes
._*
############################
# Linux
############################
*~
############################
# Windows
############################
Thumbs.db
ehthumbs.db
Desktop.ini
$RECYCLE.BIN/
*.cab
*.msi
*.msm
*.msp
############################
# Packages
############################
*.7z
*.csv
*.dat
*.dmg
*.gz
*.iso
*.jar
*.rar
*.tar
*.zip
*.com
*.class
*.dll
*.exe
*.o
*.seed
*.so
*.swo
*.swp
*.swn
*.swm
*.out
*.pid
############################
# Logs and databases
############################
.tmp
*.log
*.sql
*.sqlite
############################
# Misc.
############################
*#
ssl
.editorconfig
.gitattributes
.idea
nbproject
############################
# Node.js
############################
lib-cov
lcov.info
pids
logs
results
build
node_modules
.node_history
############################
# Tests
############################
test

View File

@ -0,0 +1,7 @@
Copyright (c) 2015-2018 Strapi Solutions.
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -0,0 +1,11 @@
# strapi-email-sendmail
## Resources
- [MIT License](LICENSE.md)
## Links
- [Strapi website](http://strapi.io/)
- [Strapi community on Slack](http://slack.strapi.io)
- [Strapi news on Twitter](https://twitter.com/strapijs)

View File

@ -0,0 +1,70 @@
'use strict';
/**
* Module dependencies
*/
// Public node modules.
const _ = require('lodash');
const mailgunFactory = require('mailgun-js');
/* eslint-disable no-unused-vars */
module.exports = {
provider: 'mailgun',
name: 'Mailgun',
auth: {
mailgun_default_from: {
label: 'Mailgun Default From',
type: 'text'
},
mailgun_default_replyto: {
label: 'Mailgun Default Reply-To',
type: 'text'
},
mailgun_api_key: {
label: 'Mailgun API Key',
type: 'text'
},
mailgun_domain: {
label: 'Mailgun Domain',
type: 'text'
}
},
init: (config) => {
const mailgun = mailgunFactory({
apiKey: config.mailgun_api_key,
domain: config.mailgun_domain,
mute: false
});
return {
send: (options, cb) => {
return new Promise((resolve, reject) => {
// Default values.
options = _.isObject(options) ? options : {};
options.from = options.from || config.mailgun_default_from;
options.replyTo = options.replyTo || config.mailgun_default_replyto;
options.text = options.text || options.html;
options.html = options.html || options.text;
let msg = {
from: options.from,
to: options.to,
subject: options.subject,
text: options.text,
html: options.html
};
msg['h:Reply-To'] = options.replyTo;
mailgun.messages().send(msg, function (err) {
if (err) {
reject([{ messages: [{ id: 'Auth.form.error.email.invalid' }] }]);
} else {
resolve();
}
});
});
}
};
}
};

View File

@ -0,0 +1,45 @@
{
"name": "strapi-email-mailgun",
"version": "3.0.0-alpha.12.2",
"description": "Mailgun provider for strapi email plugin",
"homepage": "http://strapi.io",
"keywords": [
"email",
"strapi",
"mailgun"
],
"directories": {
"lib": "./lib"
},
"main": "./lib",
"dependencies": {
"mailgun-js": "0.18.0"
},
"strapi": {
"isProvider": true
},
"author": {
"email": "hi@strapi.io",
"name": "Strapi team",
"url": "http://strapi.io"
},
"maintainers": [
{
"name": "Strapi team",
"email": "hi@strapi.io",
"url": "http://strapi.io"
}
],
"repository": {
"type": "git",
"url": "git://github.com/strapi/strapi.git"
},
"bugs": {
"url": "https://github.com/strapi/strapi/issues"
},
"engines": {
"node": ">= 9.0.0",
"npm": ">= 5.3.0"
},
"license": "MIT"
}

View File

@ -0,0 +1,16 @@
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[{package.json,*.yml}]
indent_style = space
indent_size = 2
[*.md]
trim_trailing_whitespace = false

View File

@ -0,0 +1,95 @@
############################
# OS X
############################
.DS_Store
.AppleDouble
.LSOverride
Icon
.Spotlight-V100
.Trashes
._*
############################
# Linux
############################
*~
############################
# Windows
############################
Thumbs.db
ehthumbs.db
Desktop.ini
$RECYCLE.BIN/
*.cab
*.msi
*.msm
*.msp
############################
# Packages
############################
*.7z
*.csv
*.dat
*.dmg
*.gz
*.iso
*.jar
*.rar
*.tar
*.zip
*.com
*.class
*.dll
*.exe
*.o
*.seed
*.so
*.swo
*.swp
*.swn
*.swm
*.out
*.pid
############################
# Logs and databases
############################
.tmp
*.log
*.sql
*.sqlite
############################
# Misc.
############################
*#
.idea
nbproject
############################
# Node.js
############################
lib-cov
lcov.info
pids
logs
results
build
node_modules
.node_history
package-lock.json

View File

@ -0,0 +1,104 @@
############################
# OS X
############################
.DS_Store
.AppleDouble
.LSOverride
Icon
.Spotlight-V100
.Trashes
._*
############################
# Linux
############################
*~
############################
# Windows
############################
Thumbs.db
ehthumbs.db
Desktop.ini
$RECYCLE.BIN/
*.cab
*.msi
*.msm
*.msp
############################
# Packages
############################
*.7z
*.csv
*.dat
*.dmg
*.gz
*.iso
*.jar
*.rar
*.tar
*.zip
*.com
*.class
*.dll
*.exe
*.o
*.seed
*.so
*.swo
*.swp
*.swn
*.swm
*.out
*.pid
############################
# Logs and databases
############################
.tmp
*.log
*.sql
*.sqlite
############################
# Misc.
############################
*#
ssl
.editorconfig
.gitattributes
.idea
nbproject
############################
# Node.js
############################
lib-cov
lcov.info
pids
logs
results
build
node_modules
.node_history
############################
# Tests
############################
test

View File

@ -0,0 +1,7 @@
Copyright (c) 2015-2018 Strapi Solutions.
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -0,0 +1,11 @@
# strapi-email-sendmail
## Resources
- [MIT License](LICENSE.md)
## Links
- [Strapi website](http://strapi.io/)
- [Strapi community on Slack](http://slack.strapi.io)
- [Strapi news on Twitter](https://twitter.com/strapijs)

View File

@ -0,0 +1,62 @@
'use strict';
/**
* Module dependencies
*/
// Public node modules.
const _ = require('lodash');
const sendgrid = require('@sendgrid/mail');
/* eslint-disable no-unused-vars */
module.exports = {
provider: 'sendgrid',
name: 'Sendgrid',
auth: {
sendgrid_default_from: {
label: 'Sendgrid Default From',
type: 'text'
},
sendgrid_default_replyto: {
label: 'Sendgrid Default Reply-To',
type: 'text'
},
sendgrid_api_key: {
label: 'Sendgrid API Key',
type: 'text'
}
},
init: (config) => {
sendgrid.setApiKey(config.sendgrid_api_key);
return {
send: (options, cb) => {
return new Promise((resolve, reject) => {
// Default values.
options = _.isObject(options) ? options : {};
options.from = options.from || config.sendgrid_default_from;
options.replyTo = options.replyTo || config.sendgrid_default_replyto;
options.text = options.text || options.html;
options.html = options.html || options.text;
let msg = {
from: options.from,
to: options.to,
reply_to: options.replyTo,
subject: options.subject,
text: options.text,
html: options.html
};
sendgrid.send(msg, function (err) {
if (err) {
reject([{ messages: [{ id: 'Auth.form.error.email.invalid' }] }]);
} else {
resolve();
}
});
});
}
};
}
};

View File

@ -0,0 +1,45 @@
{
"name": "strapi-email-sendgrid",
"version": "3.0.0-alpha.12.2",
"description": "Sendgrid provider for strapi email",
"homepage": "http://strapi.io",
"keywords": [
"email",
"strapi",
"sendgrid"
],
"directories": {
"lib": "./lib"
},
"main": "./lib",
"dependencies": {
"@sendgrid/mail": "6.2.1"
},
"strapi": {
"isProvider": true
},
"author": {
"email": "hi@strapi.io",
"name": "Strapi team",
"url": "http://strapi.io"
},
"maintainers": [
{
"name": "Strapi team",
"email": "hi@strapi.io",
"url": "http://strapi.io"
}
],
"repository": {
"type": "git",
"url": "git://github.com/strapi/strapi.git"
},
"bugs": {
"url": "https://github.com/strapi/strapi/issues"
},
"engines": {
"node": ">= 9.0.0",
"npm": ">= 5.3.0"
},
"license": "MIT"
}

View File

@ -0,0 +1,16 @@
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[{package.json,*.yml}]
indent_style = space
indent_size = 2
[*.md]
trim_trailing_whitespace = false

View File

@ -0,0 +1,95 @@
############################
# OS X
############################
.DS_Store
.AppleDouble
.LSOverride
Icon
.Spotlight-V100
.Trashes
._*
############################
# Linux
############################
*~
############################
# Windows
############################
Thumbs.db
ehthumbs.db
Desktop.ini
$RECYCLE.BIN/
*.cab
*.msi
*.msm
*.msp
############################
# Packages
############################
*.7z
*.csv
*.dat
*.dmg
*.gz
*.iso
*.jar
*.rar
*.tar
*.zip
*.com
*.class
*.dll
*.exe
*.o
*.seed
*.so
*.swo
*.swp
*.swn
*.swm
*.out
*.pid
############################
# Logs and databases
############################
.tmp
*.log
*.sql
*.sqlite
############################
# Misc.
############################
*#
.idea
nbproject
############################
# Node.js
############################
lib-cov
lcov.info
pids
logs
results
build
node_modules
.node_history
package-lock.json

View File

@ -0,0 +1,104 @@
############################
# OS X
############################
.DS_Store
.AppleDouble
.LSOverride
Icon
.Spotlight-V100
.Trashes
._*
############################
# Linux
############################
*~
############################
# Windows
############################
Thumbs.db
ehthumbs.db
Desktop.ini
$RECYCLE.BIN/
*.cab
*.msi
*.msm
*.msp
############################
# Packages
############################
*.7z
*.csv
*.dat
*.dmg
*.gz
*.iso
*.jar
*.rar
*.tar
*.zip
*.com
*.class
*.dll
*.exe
*.o
*.seed
*.so
*.swo
*.swp
*.swn
*.swm
*.out
*.pid
############################
# Logs and databases
############################
.tmp
*.log
*.sql
*.sqlite
############################
# Misc.
############################
*#
ssl
.editorconfig
.gitattributes
.idea
nbproject
############################
# Node.js
############################
lib-cov
lcov.info
pids
logs
results
build
node_modules
.node_history
############################
# Tests
############################
test

View File

@ -0,0 +1,7 @@
Copyright (c) 2015-2018 Strapi Solutions.
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -0,0 +1,11 @@
# strapi-email-sendmail
## Resources
- [MIT License](LICENSE.md)
## Links
- [Strapi website](http://strapi.io/)
- [Strapi community on Slack](http://slack.strapi.io)
- [Strapi news on Twitter](https://twitter.com/strapijs)

View File

@ -0,0 +1,56 @@
'use strict';
/**
* Module dependencies
*/
// Public node modules.
const _ = require('lodash');
const sendmail = require('sendmail')({
silent: true
});
/* eslint-disable no-unused-vars */
module.exports = {
provider: 'sendmail',
name: 'Sendmail',
auth: {
sendmail_default_from: {
label: 'Sendmail Default From',
type: 'text'
},
sendmail_default_replyto: {
label: 'Sendmail Default Reply-To',
type: 'text'
}
},
init: (config) => {
return {
send: (options, cb) => {
return new Promise((resolve, reject) => {
// Default values.
options = _.isObject(options) ? options : {};
options.from = options.from || config.sendmail_default_from;
options.replyTo = options.replyTo || config.sendmail_default_replyto;
options.text = options.text || options.html;
options.html = options.html || options.text;
sendmail({
from: options.from,
to: options.to,
replyTo: options.replyTo,
subject: options.subject,
text: options.text,
html: options.html
}, function (err) {
if (err) {
reject([{ messages: [{ id: 'Auth.form.error.email.invalid' }] }]);
} else {
resolve();
}
});
});
}
};
}
};

View File

@ -0,0 +1,44 @@
{
"name": "strapi-email-sendmail",
"version": "3.0.0-alpha.12.2",
"description": "Sendmail provider for strapi email",
"homepage": "http://strapi.io",
"keywords": [
"email",
"strapi"
],
"directories": {
"lib": "./lib"
},
"main": "./lib",
"dependencies": {
"sendmail": "^1.2.0"
},
"strapi": {
"isProvider": true
},
"author": {
"email": "hi@strapi.io",
"name": "Strapi team",
"url": "http://strapi.io"
},
"maintainers": [
{
"name": "Strapi team",
"email": "hi@strapi.io",
"url": "http://strapi.io"
}
],
"repository": {
"type": "git",
"url": "git://github.com/strapi/strapi.git"
},
"bugs": {
"url": "https://github.com/strapi/strapi/issues"
},
"engines": {
"node": ">= 9.0.0",
"npm": ">= 5.3.0"
},
"license": "MIT"
}

View File

@ -0,0 +1,87 @@
/**
*
* EditForm
*
*/
import React from 'react';
import { findIndex, get, isEmpty, map } from 'lodash';
import PropTypes from 'prop-types';
// You can find these components in either
// ./node_modules/strapi-helper-plugin/lib/src
// or strapi/packages/strapi-helper-plugin/lib/src
import Input from 'components/InputsIndex';
import styles from './styles.scss';
class EditForm extends React.Component {
getProviderForm = () => get(this.props.settings, ['providers', this.props.selectedProviderIndex, 'auth'], {});
generateSelectOptions = () => (
Object.keys(get(this.props.settings, 'providers', {})).reduce((acc, current) => {
const option = {
id: get(this.props.settings, ['providers', current, 'name']),
value: get(this.props.settings, ['providers', current, 'provider']),
};
acc.push(option);
return acc;
}, [])
)
render() {
return (
<div className={styles.editForm}>
<div className="row">
<Input
customBootstrapClass="col-md-6"
inputDescription={{ id: 'email.EditForm.Input.select.inputDescription' }}
inputClassName={styles.inputStyle}
label={{ id: 'email.EditForm.Input.select.label' }}
name="provider"
onChange={this.props.onChange}
selectOptions={this.generateSelectOptions()}
type="select"
value={get(this.props.modifiedData, 'provider')}
/>
</div>
{!isEmpty(this.getProviderForm()) && (
<div className={styles.subFormWrapper}>
<div className="row">
{map(this.getProviderForm(), (value, key) => (
<Input
didCheckErrors={this.props.didCheckErrors}
errors={get(this.props.formErrors, [findIndex(this.props.formErrors, ['name', key]), 'errors'])}
key={key}
label={{ id: value.label }}
name={key}
onChange={this.props.onChange}
selectOptions={value.values}
type={value.type === 'enum' ? 'select' : value.type}
validations={{ required: true }}
value={get(this.props.modifiedData, key, '')}
/>
))}
</div>
</div>
)}
</div>
);
}
}
EditForm.defaultProps = {
settings: {
providers: [],
},
};
EditForm.propTypes = {
didCheckErrors: PropTypes.bool.isRequired,
formErrors: PropTypes.array.isRequired,
modifiedData: PropTypes.object.isRequired,
onChange: PropTypes.func.isRequired,
selectedProviderIndex: PropTypes.number.isRequired,
settings: PropTypes.object,
};
export default EditForm;

View File

@ -0,0 +1,24 @@
.editForm {
background: #ffffff;
padding: 45px 30px 22px 30px;
border-radius: 2px;
box-shadow: 0 2px 4px #E3E9F3;
}
.inputStyle {
max-width: 358px;
}
.separator {
height: 1px;
width: 100%;
margin-bottom: 22px;
background: #F6F6F6;
box-sizing: border-box;
}
.subFormWrapper {
margin-bottom: 14px;
padding: 23px 30px 0 30px;
background-color: #FAFAFB;
}

View File

@ -0,0 +1,31 @@
/**
*
* EntriesNumber
*
*/
import React from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
import styles from './styles.scss';
function EntriesNumber({ number }) {
const id = number > 1 ? 'number.plural' : 'number';
return (
<div className={styles.entriesNumberContainer}>
<FormattedMessage id={`upload.EntriesNumber.${id}`} values={{ number }} />
</div>
);
}
EntriesNumber.defaultProps = {
number: 0,
};
EntriesNumber.propTypes = {
number: PropTypes.number,
};
export default EntriesNumber;

View File

@ -0,0 +1,4 @@
.entriesNumberContainer {
color: #787E8F;
font-size: 13px;
}

View File

@ -0,0 +1,9 @@
{
"archive": ["rar", "zip"],
"code": ["js", "json", "rb", "erb", "txt", "css", "scss", "html", "jsx", "svg"],
"img": ["jpg", "jpeg", "png", "gif", "ico"],
"pdf": ["pdf"],
"powerpoint": ["ppt", "key", "xls"],
"video": ["mov", "avi", "mpg", "mp4", "m4v"],
"word": ["doc", "pages"]
}

View File

@ -0,0 +1,62 @@
/**
*
*
* FileIcon
*/
import React from 'react';
import PropTypes from 'prop-types';
import cn from 'classnames';
import { trim } from 'lodash';
import ext from './extensions.json';
import styles from './styles.scss';
function FileIcon({ fileType }) {
const iconType = (() => {
switch (true) {
case ext.archive.includes(trim(fileType, '.')):
return 'file-archive-o';
case ext.code.includes(trim(fileType, '.')):
return 'file-code-o';
case ext.img.includes(trim(fileType, '.')):
return 'file-image-o';
case ext.pdf.includes(trim(fileType, '.')):
return 'file-pdf-o';
case ext.powerpoint.includes(trim(fileType, '.')):
return 'file-powerpoint-o';
case ext.video.includes(trim(fileType, '.')):
return 'file-video-o';
case ext.word.includes(trim(fileType, '.')):
return 'file-word-o';
default:
return 'file';
}
})();
return (
<div
className={(cn(
styles.fileIconContainer,
iconType === 'file-pdf-o' && styles.pdf,
iconType === 'file-archive-o' && styles.zip,
iconType === 'file-image-o' && styles.image,
iconType === 'file-video-o' && styles.video,
iconType === 'file-code-o' && styles.code,
))}
>
<i className={`fa fa-${iconType}`} />
</div>
);
}
FileIcon.defaultProps = {
fileType: 'zip',
};
FileIcon.propTypes = {
fileType: PropTypes.string,
};
export default FileIcon;

View File

@ -0,0 +1,24 @@
.fileIconContainer {
font-size: 20px;
color: #BDBFC2;
}
.image {
color: #8AA066;
}
.pdf {
color: #E26D6D;
}
.video {
color: #77C69E;
}
.zip {
color: #715A31;
}
.code {
color: #515A6D;
}

View File

@ -0,0 +1,167 @@
/**
*
* Li
*
*/
import React from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
import { CopyToClipboard } from 'react-copy-to-clipboard';
import cn from 'classnames';
import moment from 'moment';
import FileIcon from 'components/FileIcon';
import IcoContainer from 'components/IcoContainer';
import PopUpWarning from 'components/PopUpWarning';
import styles from './styles.scss';
/* eslint-disable react/no-string-refs */
class Li extends React.Component {
state = { isOpen: false, copied: false };
componentDidUpdate(prevProps, prevState) {
if (prevState.copied !== this.state.copied && this.state.copied) {
setTimeout(() => {
this.setState({ copied: false });
}, 3000);
}
}
getUnit = (value) => {
let unit;
let divider;
switch (true) {
case value > 10000:
unit = 'GB';
divider = 1000;
break;
case value < 1:
unit = 'B';
divider = 1;
break;
case value > 1000:
unit = 'MB';
divider = 1000;
break;
default:
unit = 'KB';
divider = 1;
}
return { divider, unit };
}
handleClick = (e) => {
e.preventDefault();
const aTag = document.getElementById('aTag');
aTag.click();
}
handleDelete = (e) => {
e.preventDefault();
this.context.deleteData(this.props.item);
}
renderLiCopied = () => (
<li className={cn(styles.liWrapper, styles.copied)}>
<div>
<div className={styles.checked}>
<div />
</div>
<div>
<FormattedMessage id="upload.Li.linkCopied" />
</div>
</div>
</li>
);
render() {
const { item } = this.props;
if (this.state.copied) {
return this.renderLiCopied();
}
const icons = [
// {
// icoType: item.private ? 'lock' : 'unlock',
// onClick: () => {},
// },
{
icoType: 'eye',
onClick: this.handleClick,
},
{
icoType: 'trash',
onClick: () => this.setState({ isOpen: true }),
},
];
return (
<CopyToClipboard text={item.url} onCopy={() => this.setState({copied: true})}>
<li className={styles.liWrapper}>
<a href={item.url} target="_blank" style={{ display: 'none' }} id="aTag">nothing</a>
<div className={styles.liContainer}>
<div>
<div />
<FileIcon fileType={item.ext} />
</div>
{['hash', 'name', 'updatedAt', 'size', 'relatedTo', ''].map((value, key) => {
if (value === 'updatedAt') {
return (
<div key={key} className={styles.truncate}>{moment(item[value]).format('YYYY/MM/DD - HH:mm')}</div>
);
}
if (value === 'size') {
const { divider, unit } = this.getUnit(item[value]);
const size = item[value]/divider;
return (
<div key={key} className={styles.truncate}>{Math.round(size * 100) / 100 }&nbsp;{unit}</div>
);
}
if (value !== '') {
return (
<div key={key} className={styles.truncate}>{item[value]}</div>
);
}
return <IcoContainer key={key} icons={icons} />;
})}
</div>
<PopUpWarning
isOpen={this.state.isOpen}
onConfirm={this.handleDelete}
toggleModal={() => this.setState({ isOpen: false })}
/>
</li>
</CopyToClipboard>
);
}
}
Li.contextTypes = {
deleteData: PropTypes.func.isRequired,
};
Li.defaultProps = {
item: {
type: 'pdf',
hash: '1234',
name: 'avatar.pdf',
updated: '20/11/2017 19:29:54',
size: '24 B',
relatedTo: 'John Doe',
},
};
Li.propTypes = {
item: PropTypes.object,
};
export default Li;

View File

@ -0,0 +1,108 @@
.liWrapper {
height: 54px;
background-color: #fff;
padding-top: 5px;
cursor: pointer;
}
.liContainer {
display: flex;
height: 100%;
padding-left: 12px;
padding-right: 12px;
margin-left: 20px;
margin-right: 20px;
line-height: 48px;
border-bottom: 1px solid rgba(14,22,34,0.04);
justify-content: space-between;
> div:first-child {
display: flex;
width: 133px;
> div:first-child {
width: 51px;
}
> div:last-child {
width: 82px;
}
}
> div:nth-child(2) {
width: calc(100% - 696px);
padding-right: 20px;
}
> div:nth-child(3) {
width: calc(100% - 596px);
}
> div:nth-child(4) {
width: 184px;
flex-shrink: 0;
> span {
&:after {
content: '\f0d8';
margin-left: 10px;
font-family: 'FontAwesome';
}
}
}
> div:nth-child(5) {
flex-shrink: 0;
width: 100px;
}
> div:nth-child(6) {
width: 147px;
flex-shrink: 0;
}
> div:nth-child(7) {
width: 116px;
flex-shrink: 0;
}
}
.copied {
background-color: #FAFAFB;
> div {
display: flex;
width: 100%;
justify-content: center;
padding-top: 1px;
text-align: center;
color: #868FA1;
font-size: 12px;
font-weight: 500;
line-height: 54px;
text-transform: uppercase;
letter-spacing: .05rem;
}
}
.checked {
padding-top: 20px;
line-height: 54px;
position: relative;
> div {
height: 14px;
width: 14px;
margin-right: 10px;
background-color: #2DD210;
border: 1px solid rgba(16,22,34,0.10);
border-radius: 3px;
&:after {
content: '\f00c';
position: absolute;
top: 0; left: 2px;
font-size: 10px;
font-family: 'FontAwesome';
font-weight: 100;
color: #fff;
transition: all .2s;
}
}
}
.truncate {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

View File

@ -0,0 +1,54 @@
/**
*
* List
*
*/
import React from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
import cn from 'classnames';
import Li from 'components/Li';
import ListHeader from 'components/ListHeader';
import styles from './styles.scss';
const EmptyLi = () => (
<li className={styles.emptyLiWrapper}>
<div>
<FormattedMessage id="upload.EmptyLi.message" />
</div>
</li>
);
function List(props) {
return (
<div className={cn('container-fluid', styles.listWrapper)}>
<div className="row">
<ul className={styles.ulList}>
<ListHeader changeSort={props.changeSort} sort={props.sort} />
{props.data.map((item, key) => (
<Li
key={item.hash || key}
item={item}
/>
))}
{props.data.length === 0 && <EmptyLi />}
</ul>
</div>
</div>
);
}
List.defaultProps = {
sort: 'id',
};
List.propTypes = {
changeSort: PropTypes.func.isRequired,
data: PropTypes.arrayOf(PropTypes.object).isRequired,
sort: PropTypes.string,
};
export default List;

View File

@ -0,0 +1,48 @@
.listWrapper {
margin-top: 9px;
padding: 0;
> div:first-child {
margin: 0;
}
}
.ulList {
width: 100%;
padding: 0 !important;
list-style: none;
> li:nth-child(2) {
height: 57px;
> div {
line-height: 54px !important;
}
}
> li {
width: 100%;
margin-top: 0;
}
> li:last-child {
> div {
border-bottom: 0 !important;
}
box-shadow: 0 2px 4px #E3E9F3;
}
}
.emptyLiWrapper {
height: 54px;
background-color: #fff;
padding-top: 5px;
cursor: pointer;
> div {
display: flex;
width: 100%;
justify-content: center;
padding-top: 1px;
text-align: center;
font-size: 12px;
line-height: 54px;
text-transform: uppercase;
}
}

View File

@ -0,0 +1,77 @@
/**
*
* ListHeader
*
*/
import React from 'react';
import { FormattedMessage } from 'react-intl';
import cn from 'classnames';
import PropTypes from 'prop-types';
// import InputCheckBox from 'components/InputCheckbox';
import styles from './styles.scss';
function ListHeader({ changeSort, sort }) {
const titles = [
'hash',
'name',
'updated',
'size',
// 'related',
'',
'',
];
const handleChangeSort = (name) => {
if (sort === name) {
changeSort(`-${name}`);
} else if (sort === `-${name}`) {
changeSort('hash');
} else if (name === 'updated' || name === 'related') {
changeSort('hash');
} else {
changeSort(name);
}
};
const shouldDisplaySort = (title) => sort === title && styles.icon || sort === `-${title}` && styles.iconDesc || '';
return (
<li className={styles.listheaderWrapper}>
<div className={cn(styles.listHeader)}>
<div>
<div />
<div className={shouldDisplaySort('type')} onClick={() => handleChangeSort('type')}>
<FormattedMessage id="upload.ListHeader.type" />
<span />
</div>
</div>
{titles.map((title, key) => {
if (title !== '') {
return (
<div key={key} className={shouldDisplaySort(title)} onClick={() => handleChangeSort(title)}>
<FormattedMessage id={`upload.ListHeader.${title}`} />
<span />
</div>
);
}
return <div key={key} />;
})}
</div>
</li>
);
}
ListHeader.defaultProps = {
changeSort: () => {},
};
ListHeader.propTypes = {
changeSort: PropTypes.func,
sort: PropTypes.string.isRequired,
};
export default ListHeader;

View File

@ -0,0 +1,72 @@
.listheaderWrapper {
height: 30px;
background-color: #F3F3F4;
color: #333740;
font-size: 13px;
font-weight: 600;
line-height: 30px;
}
.listHeader {
display: flex;
margin-left: 32px;
margin-right: 32px;
justify-content: space-between;
span {
letter-spacing: 0.3px;
}
> div:first-child {
display: flex;
width: 133px;
> div:first-child {
width: 51px;
}
> div:last-child {
width: 82px;
}
}
> div:nth-child(2) {
width: calc(100% - 496px - 221px);
}
> div:nth-child(3) {
width: calc(100% - 496px - 148px);
}
> div:nth-child(4) {
width: 184px;
flex-shrink: 0;
}
> div:nth-child(5) {
width: 100px;
flex-shrink: 0;
}
> div:nth-child(6) {
width: 147px;
flex-shrink: 0;
}
> div:nth-child(7) {
width: 116px;
flex-shrink: 0;
}
}
.icon {
> span:last-child {
&:after {
content: '\f0d8';
margin-left: 10px;
font-family: 'FontAwesome';
}
}
cursor: pointer;
}
.iconDesc {
> span:last-child {
&:after {
content: '\f0d7';
margin-left: 10px;
font-family: 'FontAwesome';
}
}
cursor: pointer;
}

View File

@ -0,0 +1,85 @@
/**
*
* PluginInputFile
*
*/
import React from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
import cn from 'classnames';
import styles from './styles.scss';
/* eslint-disable react/no-string-refs */
/* eslint-disable jsx-a11y/label-has-for */
/* eslint-disable react/jsx-tag-spacing */
class PluginInputFile extends React.PureComponent {
state = { isDraging: false };
handleChange = (e) => {
const dataTransfer = e.target;
this.props.onDrop({ dataTransfer });
}
handleDragEnter = () => this.setState({ isDraging: true });
handleDragLeave = () => this.setState({ isDraging: false });
handleDrop = (e) => {
e.preventDefault();
this.setState({ isDraging: false });
this.props.onDrop(e);
}
render() {
const {
name,
showLoader,
} = this.props;
const { isDraging } = this.state;
const link = (
<FormattedMessage id="upload.PluginInputFile.link">
{(message) => <span className={styles.underline}>{message}</span>}
</FormattedMessage>
);
return (
<label
className={cn(styles.pluginInputFile, isDraging && styles.pluginInputFileHover, showLoader && styles.quadrat)}
onDragEnter={this.handleDragEnter}
onDragOver={(e) => {
e.preventDefault();
e.stopPropagation();
}}
onDrop={this.handleDrop}
>
<svg className={styles.icon} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 104.40317 83.13328"><g><rect x="5.02914" y="8.63138" width="77.33334" height="62.29167" rx="4" ry="4" transform="translate(-7.45722 9.32921) rotate(-12)" fill="#fafafb"/><rect x="5.52914" y="9.13138" width="76.33334" height="61.29167" rx="4" ry="4" transform="translate(-7.45722 9.32921) rotate(-12)" fill="none" stroke="#979797"/><path d="M74.25543,36.05041l3.94166,18.54405L20.81242,66.79194l-1.68928-7.94745,10.2265-16.01791,7.92872,5.2368,16.3624-25.62865ZM71.974,6.07811,6.76414,19.93889a1.27175,1.27175,0,0,0-.83343.58815,1.31145,1.31145,0,0,0-.18922,1.01364L16.44028,71.87453a1.31145,1.31145,0,0,0,.58515.849,1.27176,1.27176,0,0,0,1.0006.19831L83.23586,59.06111a1.27177,1.27177,0,0,0,.83343-.58815,1.31146,1.31146,0,0,0,.18922-1.01364L73.55972,7.12547a1.31146,1.31146,0,0,0-.58514-.849A1.27177,1.27177,0,0,0,71.974,6.07811Zm6.80253-.0615L89.4753,56.35046A6.5712,6.5712,0,0,1,88.554,61.435a6.37055,6.37055,0,0,1-4.19192,2.92439L19.15221,78.22019a6.37056,6.37056,0,0,1-5.019-.96655,6.57121,6.57121,0,0,1-2.90975-4.27024L.5247,22.64955A6.57121,6.57121,0,0,1,1.446,17.565a6.37056,6.37056,0,0,1,4.19192-2.92439L70.84779.77981a6.37055,6.37055,0,0,1,5.019.96655A6.5712,6.5712,0,0,1,78.77651,6.01661Z" transform="translate(-0.14193 -0.62489)" fill="#333740"/><rect x="26.56627" y="4.48824" width="62.29167" height="77.33333" rx="4" ry="4" transform="translate(0.94874 87.10632) rotate(-75)" fill="#fafafb"/><rect x="27.06627" y="4.98824" width="61.29167" height="76.33333" rx="4" ry="4" transform="translate(0.94874 87.10632) rotate(-75)" fill="none" stroke="#979797"/><path d="M49.62583,26.96884A7.89786,7.89786,0,0,1,45.88245,31.924a7.96,7.96,0,0,1-10.94716-2.93328,7.89786,7.89786,0,0,1-.76427-6.163,7.89787,7.89787,0,0,1,3.74338-4.95519,7.96,7.96,0,0,1,10.94716,2.93328A7.89787,7.89787,0,0,1,49.62583,26.96884Zm37.007,26.73924L81.72608,72.02042,25.05843,56.83637l2.1029-7.84815L43.54519,39.3589l4.68708,8.26558L74.44644,32.21756ZM98.20721,25.96681,33.81216,8.71221a1.27175,1.27175,0,0,0-1.00961.14568,1.31145,1.31145,0,0,0-.62878.81726L18.85537,59.38007a1.31145,1.31145,0,0,0,.13591,1.02215,1.27176,1.27176,0,0,0,.80151.631l64.39506,17.2546a1.27177,1.27177,0,0,0,1.0096-.14567,1.31146,1.31146,0,0,0,.62877-.81726l13.3184-49.70493a1.31146,1.31146,0,0,0-.13591-1.02215A1.27177,1.27177,0,0,0,98.20721,25.96681Zm6.089,3.03348L90.97784,78.70523a6.5712,6.5712,0,0,1-3.12925,4.1121,6.37055,6.37055,0,0,1-5.06267.70256L18.39086,66.26529a6.37056,6.37056,0,0,1-4.03313-3.13977,6.57121,6.57121,0,0,1-.654-5.12581L27.02217,8.29477a6.57121,6.57121,0,0,1,3.12925-4.11211,6.37056,6.37056,0,0,1,5.06267-.70255l64.39506,17.2546a6.37055,6.37055,0,0,1,4.03312,3.13977A6.5712,6.5712,0,0,1,104.29623,29.0003Z" transform="translate(-0.14193 -0.62489)" fill="#333740"/></g></svg>
<p className={styles.textWrapper}>
{!showLoader && <FormattedMessage id="upload.PluginInputFile.text" values={{ link }} /> }
{showLoader && <FormattedMessage id="upload.PluginInputFile.loading" />}
</p>
<div
onDragLeave={this.handleDragLeave}
className={cn(isDraging && styles.isDraging)}
/>
<input
multiple
name={name}
onChange={this.handleChange}
type="file"
/>
</label>
);
}
}
PluginInputFile.defaultProps = {};
PluginInputFile.propTypes = {
name: PropTypes.string.isRequired,
onDrop: PropTypes.func.isRequired,
showLoader: PropTypes.bool.isRequired,
};
export default PluginInputFile;

View File

@ -0,0 +1,102 @@
.isDraging {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
.pluginInputFile {
position: relative;
height: 146px;
width: 100%;
padding-top: 28px;
border: 2px dashed #E3E9F3;
border-radius: 2px;
text-align: center;
> input {
display: none;
}
.icon{
width: 82px;
path{
fill: #CCD0DA;
transition: fill .3s ease;
}
}
&:hover{
cursor: pointer;
}
}
.textWrapper {
margin-top: 10px;
text-align: center;
font-size: 13px;
color: #9EA7B8;
u {
color: #1C5DE7;
}
}
.pluginInputFileHover {
background-color: rgba(28,93,231,0.01) !important;
border: 2px dashed rgba(28,93,231,0.10) !important;
}
.underline {
color: #1C5DE7;
text-decoration: underline;
cursor: pointer;
}
@mixin smoothBlink($firstColor, $secondColor) {
@-webkit-keyframes blink {
0% {
fill: $firstColor;
background-color: $firstColor;
}
26% {
fill: $secondColor;
background-color: $secondColor;
}
76% {
fill: $firstColor;
background-color: $firstColor;
}
}
@keyframes blink {
0% {
fill: $firstColor;
background-color: $firstColor;
}
26% {
fill: $secondColor;
background-color: $secondColor;
}
76% {
fill: $firstColor;
background-color: $firstColor;
}
}
-webkit-animation: blink 2s linear infinite;
-moz-animation: blink 2s linear infinite;
-o-animation: blink 2s linear infinite;
animation: blink 2s linear infinite;
}
.quadrat {
.icon{
path {
fill: #729BEF;
}
}
@include smoothBlink(transparent, rgba(28, 93, 231, 0.05));
}

View File

@ -0,0 +1,31 @@
/**
*
* This component is the skeleton around the actual pages, and should only
* contain code that should be seen on all pages. (e.g. navigation bar)
*
*/
import React from 'react';
import { Switch, Route } from 'react-router-dom';
// Utils
import { pluginId } from 'app';
// Containers
import ConfigPage from 'containers/ConfigPage';
import NotFoundPage from 'containers/NotFoundPage';
function App() {
return (
<div className={pluginId}>
<Switch>
<Route path={`/plugins/${pluginId}/configurations/:env`} component={ConfigPage} exact />
<Route path={`/plugins/${pluginId}/configurations/`} component={ConfigPage} exact />
<Route path={`/plugins/${pluginId}`} component={ConfigPage} exact />
<Route component={NotFoundPage} />
</Switch>
</div>
);
}
export default App;

View File

@ -0,0 +1,18 @@
/*
*
* App reducer
*
*/
import { fromJS } from 'immutable';
const initialState = fromJS({});
function appReducer(state = initialState, action) {
switch (action.type) {
default:
return state;
}
}
export default appReducer;

View File

@ -0,0 +1,9 @@
// import { createSelector } from 'reselect';
/**
* Direct selector to the list state domain
*/
// const selectGlobalDomain = () => state => state.get('global');
export {};

View File

@ -0,0 +1,77 @@
/**
*
*
* ConfigPage actions
*
*/
import {
GET_SETTINGS,
GET_SETTINGS_SUCCEEDED,
ON_CANCEL,
ON_CHANGE,
SET_ERRORS,
SUBMIT,
SUBMIT_ERROR,
SUBMIT_SUCCEEDED,
} from './constants';
export function getSettings(env) {
return {
type: GET_SETTINGS,
env,
};
}
export function getSettingsSucceeded(settings, appEnvironments) {
return {
type: GET_SETTINGS_SUCCEEDED,
appEnvironments,
settings,
initialData: settings.config,
};
}
export function onCancel() {
return {
type: ON_CANCEL,
};
}
export function onChange({ target }) {
const keys = ['modifiedData'].concat(target.name.split('.'));
const value = target.name === 'sizeLimit' ? parseInt(target.value, 10) * 1000 : target.value;
return {
type: ON_CHANGE,
keys,
value,
};
}
export function setErrors(errors) {
return {
type: SET_ERRORS,
errors,
};
}
export function submit() {
return {
type: SUBMIT,
};
}
export function submitError(errors) {
return {
type: SUBMIT_ERROR,
errors,
};
}
export function submitSucceeded(data) {
return {
type: SUBMIT_SUCCEEDED,
data,
};
}

View File

@ -0,0 +1,16 @@
/**
*
* ConfigPage constants
*
*/
export const GET_ENV = 'Email/ConfigPage/GET_ENV';
export const GET_ENV_SUCCEEDED = 'Email/ConfigPage/GET_ENV_SUCCEEDED';
export const GET_SETTINGS = 'Email/ConfigPage/GET_SETTINGS';
export const GET_SETTINGS_SUCCEEDED = 'Email/ConfigPage/GET_SETTINGS_SUCCEEDED';
export const ON_CANCEL = 'Email/ConfigPage/ON_CANCEL';
export const ON_CHANGE = 'Email/ConfigPage/ON_CHANGE';
export const SET_ERRORS = 'Email/ConfigPage/SET_ERRORS';
export const SUBMIT = 'Email/ConfigPage/SUBMIT';
export const SUBMIT_ERROR = 'Email/ConfigPage/SUBMIT_ERROR';
export const SUBMIT_SUCCEEDED = 'Email/ConfigPage/SUBMIT_SUCCEEDED';

View File

@ -0,0 +1,190 @@
/**
*
* ConfigPage
*
*/
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { bindActionCreators, compose } from 'redux';
import { findIndex, get, isEmpty } from 'lodash';
// You can find these components in either
// ./node_modules/strapi-helper-plugin/lib/src
// or strapi/packages/strapi-helper-plugin/lib/src
import ContainerFluid from 'components/ContainerFluid';
import HeaderNav from 'components/HeaderNav';
import PluginHeader from 'components/PluginHeader';
// Plugin's components
import EditForm from 'components/EditForm';
// You can find these utils in either
// ./node_modules/strapi-helper-plugin/lib/src
// or strapi/packages/strapi-helper-plugin/lib/src
import injectReducer from 'utils/injectReducer';
import injectSaga from 'utils/injectSaga';
import {
getSettings,
onCancel,
onChange,
setErrors,
submit,
} from './actions';
import reducer from './reducer';
import saga from './saga';
import selectConfigPage from './selectors';
class ConfigPage extends React.Component {
componentDidMount() {
this.getSettings(this.props);
}
componentWillReceiveProps(nextProps) {
// Get new settings on navigation change
if (nextProps.match.params.env !== this.props.match.params.env) {
this.getSettings(nextProps);
}
// Redirect the user to the email list after modifying is provider
if (nextProps.submitSuccess !== this.props.submitSuccess) {
this.props.history.push(`/plugins/email/configurations/${this.props.match.params.env}`);
}
}
getSelectedProviderIndex = () => findIndex(this.props.settings.providers, ['provider', get(this.props.modifiedData, 'provider')]);
/**
* Get Settings depending on the props
* @param {Object} props
* @return {Func} calls the saga that gets the current settings
*/
getSettings = (props) => {
const { match: { params: { env} } } = props;
this.props.getSettings(env);
}
generateLinks = () => {
const headerNavLinks = this.props.appEnvironments.reduce((acc, current) => {
const link = Object.assign(current, { to: `/plugins/email/configurations/${current.name}` });
acc.push(link);
return acc;
}, []).sort(link => link.name === 'production');
return headerNavLinks;
}
handleSubmit = (e) => {
e.preventDefault();
const formErrors = Object.keys(get(this.props.settings, ['providers', this.getSelectedProviderIndex(), 'auth'], {})).reduce((acc, current) => {
if (isEmpty(get(this.props.modifiedData, current, ''))) {
acc.push({
name: current,
errors: [{ id: 'components.Input.error.validation.required' }],
});
}
return acc;
}, []);
if (!isEmpty(formErrors)) {
return this.props.setErrors(formErrors);
}
return this.props.submit();
}
pluginHeaderActions = [
{
kind: 'secondary',
label: 'app.components.Button.cancel',
onClick: this.props.onCancel,
type: 'button',
},
{
kind: 'primary',
label: 'app.components.Button.save',
onClick: this.handleSubmit,
type: 'submit',
},
];
render() {
return (
<div>
<form onSubmit={this.handleSubmit}>
<ContainerFluid>
<PluginHeader
actions={this.pluginHeaderActions}
description={{ id: 'email.ConfigPage.description' }}
title={{ id: 'email.ConfigPage.title'}}
/>
<HeaderNav links={this.generateLinks()} />
<EditForm
didCheckErrors={this.props.didCheckErrors}
formErrors={this.props.formErrors}
modifiedData={this.props.modifiedData}
onChange={this.props.onChange}
selectedProviderIndex={this.getSelectedProviderIndex()}
settings={this.props.settings}
/>
</ContainerFluid>
</form>
</div>
);
}
}
ConfigPage.contextTypes = {};
ConfigPage.defaultProps = {
appEnvironments: [],
formErrors: [],
settings: {
providers: [],
},
};
ConfigPage.propTypes = {
appEnvironments: PropTypes.array,
didCheckErrors: PropTypes.bool.isRequired,
formErrors: PropTypes.array,
getSettings: PropTypes.func.isRequired,
history: PropTypes.object.isRequired,
match: PropTypes.object.isRequired,
modifiedData: PropTypes.object.isRequired,
onCancel: PropTypes.func.isRequired,
onChange: PropTypes.func.isRequired,
setErrors: PropTypes.func.isRequired,
settings: PropTypes.object,
submit: PropTypes.func.isRequired,
submitSuccess: PropTypes.bool.isRequired,
};
function mapDispatchToProps(dispatch) {
return bindActionCreators(
{
getSettings,
onCancel,
onChange,
setErrors,
submit,
},
dispatch,
);
}
const mapStateToProps = selectConfigPage();
const withConnect = connect(mapStateToProps, mapDispatchToProps);
const withReducer = injectReducer({ key: 'configPage', reducer });
const withSaga = injectSaga({ key: 'configPage', saga });
export default compose(
withReducer,
withSaga,
withConnect,
)(ConfigPage);

View File

@ -0,0 +1,67 @@
/**
*
* ConfigPage reducer
*
*/
import { fromJS, List, Map } from 'immutable';
import {
GET_SETTINGS,
GET_SETTINGS_SUCCEEDED,
ON_CANCEL,
ON_CHANGE,
SET_ERRORS,
SUBMIT_ERROR,
SUBMIT_SUCCEEDED,
} from './constants';
const initialState = fromJS({
appEnvironments: List([]),
didCheckErrors: false,
env: '',
formErrors: List([]),
initialData: Map({}),
modifiedData: Map({}),
settings: {},
submitSuccess: false,
});
function configPageReducer(state = initialState, action) {
switch (action.type) {
case GET_SETTINGS:
return state.update('env', () => action.env);
case GET_SETTINGS_SUCCEEDED:
return state
.update('appEnvironments', () => List(action.appEnvironments))
.update('didCheckErrors', (v) => v = !v)
.update('formErrors', () => List([]))
.update('initialData', () => Map(action.initialData))
.update('modifiedData', () => Map(action.initialData))
.update('settings', () => action.settings);
case ON_CANCEL:
return state
.update('didCheckErrors', (v) => v = !v)
.update('formErrors', () => List([]))
.update('modifiedData', () => state.get('initialData'));
case ON_CHANGE:
return state
.updateIn(action.keys, () => action.value);
case SET_ERRORS:
case SUBMIT_ERROR:
return state
.update('didCheckErrors', (v) => v = !v)
.update('formErrors', () => List(action.errors));
case SUBMIT_SUCCEEDED:
return state
.update('didCheckErrors', (v) => v = !v)
.update('formErrors', () => List([]))
.update('initialData', () => Map(action.data))
.update('modifiedData', () => Map(action.data))
.update('submitSuccess', (v) => v = !v);
default:
return state;
}
}
export default configPageReducer;

View File

@ -0,0 +1,61 @@
// import { LOCATION_CHANGE } from 'react-router-redux';
import { call, fork, put, select, takeLatest } from 'redux-saga/effects';
import request from 'utils/request';
import {
getSettingsSucceeded,
submitSucceeded,
} from './actions';
import {
GET_SETTINGS,
SUBMIT,
} from './constants';
import {
makeSelectEnv,
makeSelectModifiedData,
} from './selectors';
export function* settingsGet(action) {
try {
const requestURL = `/email/settings/${action.env}`;
const response = yield [
call(request, requestURL, { method: 'GET' }),
call(request, '/email/environments', { method: 'GET' }),
];
yield put(getSettingsSucceeded(response[0], response[1].environments));
} catch(err) {
strapi.notification.error('notification.error');
}
}
export function* submit() {
try {
const env = yield select(makeSelectEnv());
let body = yield select(makeSelectModifiedData());
if (body.provider === 'local') {
body = {
enabled: body.enabled,
provider: 'local',
sizeLimit: body.sizeLimit,
};
}
const requestURL = `/email/settings/${env}`;
yield call(request, requestURL, { method: 'PUT', body });
// Update reducer with optimisticResponse
strapi.notification.success('email.notification.config.success');
yield put(submitSucceeded(body));
} catch(err) {
strapi.notification.error('notification.error');
// TODO handle error PUT
}
}
function* defaultSaga() {
yield fork(takeLatest, GET_SETTINGS, settingsGet);
yield fork(takeLatest, SUBMIT, submit);
}
export default defaultSaga;

View File

@ -0,0 +1,31 @@
import { createSelector } from 'reselect';
/**
* Direct selector to the configPage state domain
*/
const selectConfigPageDomain = () => state => state.get('configPage');
/**
* Default selector used by ConfigPage
*/
const selectConfigPage = () => createSelector(
selectConfigPageDomain(),
(substate) => substate.toJS(),
);
const makeSelectEnv = () => createSelector(
selectConfigPageDomain(),
(substate) => substate.get('env'),
);
const makeSelectModifiedData = () => createSelector(
selectConfigPageDomain(),
(substate) => substate.get('modifiedData').toJS(),
);
export default selectConfigPage;
export {
makeSelectEnv,
makeSelectModifiedData,
};

View File

@ -0,0 +1,20 @@
/**
* NotFoundPage
*
* This is the page we show when the user visits a url that doesn't have a route
*
* NOTE: while this component should technically be a stateless functional
* component (SFC), hot reloading does not currently support SFCs. If hot
* reloading is not a neccessity for you then you can refactor it and remove
* the linting exception.
*/
import React from 'react';
import NotFound from 'components/NotFound';
export default class NotFoundPage extends React.Component {
render() {
return <NotFound {...this.props} />;
}
}

View File

@ -1,4 +1,16 @@
{
"ConfigPage.title": "Email - Settings",
"ConfigPage.description": "Configure the email plugin",
"EditForm.Input.number.label": "Maximum size allowed (in MB)",
"EditForm.Input.select.label": "Providers",
"EditForm.Input.select.inputDescription": "Emails can be sent with the default provider (Sendmail) or an external provider",
"EditForm.Input.toggle.label": "Enable email send",
"plugin.description.short": "Send emails.",
"plugin.description.long": "Send emails."
"plugin.description.long": "Send emails.",
"notification.config.success": "The settings has been updated"
}

View File

@ -0,0 +1,68 @@
'use strict';
/**
* An asynchronous bootstrap function that runs before
* your application gets started.
*
* This gives you an opportunity to set up your data model,
* run jobs, or perform some special logic.
*/
const path = require('path');
const fs = require('fs');
const _ = require('lodash');
module.exports = async cb => {
// set plugin store
const pluginStore = strapi.store({
environment: strapi.config.environment,
type: 'plugin',
name: 'email'
});
strapi.plugins.email.config.providers = [];
const loadProviders = (basePath, cb) => {
fs.readdir(path.join(basePath, 'node_modules'), async (err, node_modules) => {
// get all email providers
const emails = _.filter(node_modules, (node_module) => {
return _.startsWith(node_module, ('strapi-email'));
});
// mount all providers to get configs
_.forEach(emails, (node_module) => {
strapi.plugins.email.config.providers.push(
require(path.join(`${basePath}/node_modules/${node_module}`))
);
});
try {
// if provider config not exist set one by default
const config = await pluginStore.get({key: 'provider'});
if (!config) {
const provider = _.find(strapi.plugins.email.config.providers, {provider: 'sendmail'});
const value = _.assign({}, provider, {
// TODO: set other default settings here
});
await pluginStore.set({key: 'provider', value});
}
} catch (err) {
strapi.log.error(`Can't load ${config.provider} email provider.`);
strapi.log.warn(`Please install strapi-email-${config.provider} --save in ${path.join(strapi.config.appPath, 'plugins', 'email')} folder.`);
strapi.stop();
}
cb();
});
};
// Load providers from the plugins' node_modules.
loadProviders(path.join(strapi.config.appPath, 'plugins', 'email'), () => {
// Load providers from the root node_modules.
loadProviders(path.join(strapi.config.appPath), cb);
});
};

View File

@ -0,0 +1,134 @@
const _ = require('lodash');
module.exports = {
find: async function (params = {}, populate) {
const records = await this.query(function(qb) {
_.forEach(params.where, (where, key) => {
qb.where(key, where[0].symbol, where[0].value);
});
if (params.sort) {
qb.orderBy(params.sort.key, params.sort.order);
}
if (params.start) {
qb.offset(params.start);
}
if (params.limit) {
qb.limit(params.limit);
}
}).fetchAll({
withRelated: populate || _.keys(_.groupBy(_.reject(this.associations, { autoPopulate: false }), 'alias'))
});
return records ? records.toJSON() : records;
},
count: async function (params = {}) {
return await this
.where(params)
.count();
},
findOne: async function (params, populate) {
const primaryKey = params[this.primaryKey] || params.id;
if (primaryKey) {
params = {
[this.primaryKey]: primaryKey
};
}
const record = await this
.forge(params)
.fetch({
withRelated: populate || this.associations.map(x => x.alias)
});
return record ? record.toJSON() : record;
},
create: async function (params) {
return this
.forge()
.save(Object.keys(params).reduce((acc, current) => {
if (_.get(this._attributes, [current, 'type']) || _.get(this._attributes, [current, 'model'])) {
acc[current] = params[current];
}
return acc;
}, {}))
.catch((err) => {
if (err.detail) {
const field = _.last(_.words(err.detail.split('=')[0]));
err = { message: `This ${field} is already taken`, field };
}
throw err;
});
},
update: async function (search, params = {}) {
if (_.isEmpty(params)) {
params = search;
}
const primaryKey = search[this.primaryKey] || search.id;
if (primaryKey) {
search = {
[this.primaryKey]: primaryKey
};
} else {
const entry = await module.exports.findOne.call(this, search);
search = {
[this.primaryKey]: entry[this.primaryKey] || entry.id
};
}
return this.forge(search)
.save(params, {
patch: true
})
.catch((err) => {
const field = _.last(_.words(err.detail.split('=')[0]));
const error = { message: `This ${field} is already taken`, field };
throw error;
});
},
delete: async function (params) {
return await this
.forge({
[this.primaryKey]: params[this.primaryKey] || params.id
})
.destroy();
},
search: async function (params) {
return this
.query(function(qb) {
qb
.whereRaw(`LOWER(hash) LIKE ?`, [`%${params.id}%`])
.orWhereRaw(`LOWER(name) LIKE ?`, [`%${params.id}%`]);
})
.fetchAll();
},
addPermission: async function (params) {
return this
.forge(params)
.save();
},
removePermission: async function (params) {
return this
.forge({
[this.primaryKey]: params[this.primaryKey] || params.id
})
.destroy();
}
};

View File

@ -0,0 +1,111 @@
const _ = require('lodash');
module.exports = {
find: async function (params = {}, populate) {
return this
.find(params.where)
.limit(Number(params.limit))
.sort(params.sort)
.skip(Number(params.start))
.populate(populate || this.associations.map(x => x.alias).join(' '))
.lean();
},
count: async function (params = {}) {
return Number(await this
.count(params));
},
findOne: async function (params, populate) {
const primaryKey = params[this.primaryKey] || params.id;
if (primaryKey) {
params = {
[this.primaryKey]: primaryKey
};
}
return this
.findOne(params)
.populate(populate || this.associations.map(x => x.alias).join(' '))
.lean();
},
create: async function (params) {
// Exclude relationships.
const values = Object.keys(params).reduce((acc, current) => {
if (_.get(this._attributes, [current, 'type']) || _.get(this._attributes, [current, 'model'])) {
acc[current] = params[current];
}
return acc;
}, {});
return this.create(values)
.catch((err) => {
if (err.message.indexOf('index:') !== -1) {
const message = err.message.split('index:');
const field = _.words(_.last(message).split('_')[0]);
const error = { message: `This ${field} is already taken`, field };
throw error;
}
throw err;
});
},
update: async function (search, params = {}) {
if (_.isEmpty(params)) {
params = search;
}
const primaryKey = search[this.primaryKey] || search.id;
if (primaryKey) {
search = {
[this.primaryKey]: primaryKey
};
}
return this.update(search, params, {
strict: false
})
.catch((error) => {
const field = _.last(_.words(error.message.split('_')[0]));
const err = { message: `This ${field} is already taken`, field };
throw err;
});
},
delete: async function (params) {
// Delete entry.
return this
.remove({
[this.primaryKey]: params[this.primaryKey] || params.id
});
},
search: async function (params) {
const re = new RegExp(params.id, 'i');
return this
.find({
'$or': [
{ hash: re },
{ name: re }
]
});
},
addPermission: async function (params) {
return this
.create(params);
},
removePermission: async function (params) {
return this
.remove(params);
}
};

View File

@ -1,3 +1,36 @@
{
"routes": []
"routes": [
{
"method": "POST",
"path": "/",
"handler": "Email.send",
"config": {
"policies": []
}
},
{
"method": "GET",
"path": "/environments",
"handler": "Email.getEnvironments",
"config": {
"policies": []
}
},
{
"method": "GET",
"path": "/settings/:environment",
"handler": "Email.getSettings",
"config": {
"policies": []
}
},
{
"method": "PUT",
"path": "/settings/:environment",
"handler": "Email.updateSettings",
"config": {
"policies": []
}
}
]
}

View File

@ -1,6 +0,0 @@
{
"EMAIL_METHOD": "",
"MAILGUN_API_KEY": "",
"MAILGUN_DOMAIN": "",
"SENDGRID_API_KEY": ""
}

View File

@ -0,0 +1,68 @@
'use strict';
/**
* Email.js controller
*
* @description: A set of functions called "actions" of the `email` plugin.
*/
const _ = require('lodash');
module.exports = {
send: async (ctx) => {
// Retrieve provider configuration.
const config = await strapi.store({
environment: strapi.config.environment,
type: 'plugin',
name: 'email'
}).get({ key: 'provider' });
// Verify if the file email is enable.
if (config.enabled === false) {
strapi.log.error('Email is disabled');
return ctx.badRequest(null, ctx.request.admin ? [{ messages: [{ id: 'Email.status.disabled' }] }] : 'Emailis disabled');
}
// Something is wrong
if (ctx.status === 400) {
return;
}
let options = ctx.request.body;
await strapi.plugins.email.services.send(options, config);
// Send 200 `ok`
ctx.send({});
},
getEnvironments: async (ctx) => {
const environments = _.map(_.keys(strapi.config.environments), environment => {
return {
name: environment,
active: (strapi.config.environment === environment)
};
});
ctx.send({ environments });
},
getSettings: async (ctx) => {
let config = await strapi.plugins.email.services.email.getProviderConfig(ctx.params.environment);
ctx.send({
providers: strapi.plugins.email.config.providers,
config
});
},
updateSettings: async (ctx) => {
await strapi.store({
environment: ctx.params.environment,
type: 'plugin',
name: 'email'
}).set({key: 'provider', value: ctx.request.body});
ctx.send({ok: true});
},
};

View File

@ -21,11 +21,10 @@
"test": "echo \"Error: no test specified\""
},
"dependencies": {
"@sendgrid/mail": "6.2.1",
"mailgun-js": "0.18.0",
"sendmail": "^1.2.0"
"strapi-email-sendmail": "3.0.0-alpha.12.2"
},
"devDependencies": {
"react-copy-to-clipboard": "5.0.1",
"strapi-helper-plugin": "3.0.0-alpha.12.2"
},
"author": {

View File

@ -7,68 +7,56 @@
*/
const _ = require('lodash');
const config = require('../config/settings.json');
let mailer;
if(config.EMAIL_METHOD === 'mailgun') {
const mailgun = require('mailgun-js')({
apiKey: config.MAILGUN_API_KEY,
domain: config.MAILGUN_DOMAIN,
mute: false
const createDefaultEnvConfig = async (env) => {
const pluginStore = strapi.store({
environment: env,
type: 'plugin',
name: 'email'
});
mailer = (msg, mailerCallback) => {
// change reply to format for Mailgun
msg['h:Reply-To'] = msg.replyTo;
mailgun.messages().send(msg, mailerCallback);
};
}
else if(config.EMAIL_METHOD === 'sendgrid') {
const sendgrid = require('@sendgrid/mail');
sendgrid.setApiKey(config.SENDGRID_API_KEY);
const provider = _.find(strapi.plugins.email.config.providers, {provider: 'sendmail'});
const value = _.assign({}, provider, {});
await pluginStore.set({key: 'provider', value});
return await strapi.store({
environment: env,
type: 'plugin',
name: 'email'
}).get({key: 'provider'});
};
mailer = (msg, mailerCallback) => {
// change capitalization for SendGrid
msg.reply_to = msg.replyTo;
sendgrid.send(msg, mailerCallback);
};
}
else {
// Fallback to default email method
const sendmail = require('sendmail')({
silent: true
});
const getProviderConfig = async (env) => {
let config = await strapi.store({
environment: env,
type: 'plugin',
name: 'email'
}).get({key: 'provider'});
mailer = (msg, mailerCallback) => {
sendmail(msg, mailerCallback);
};
}
if(!config) {
config = await createDefaultEnvConfig(env);
}
return config;
};
module.exports = {
send: (options, cb) => { // eslint-disable-line no-unused-vars
return new Promise((resolve, reject) => {
// Default values.
options = _.isObject(options) ? options : {};
options.from = options.from || '"Administration Panel" <no-reply@strapi.io>';
options.replyTo = options.replyTo || '"Administration Panel" <no-reply@strapi.io>';
options.text = options.text || options.html;
options.html = options.html || options.text;
getProviderConfig,
send: async (options, config, cb) => {
// Get email provider settings to configure the provider to use.
if(!config) {
config = await getProviderConfig(strapi.config.environment);
}
mailer({
from: options.from,
to: options.to,
replyTo: options.replyTo,
subject: options.subject,
text: options.text,
html: options.html
}, function (err) {
if (err) {
reject([{ messages: [{ id: 'Auth.form.error.email.invalid' }] }]);
} else {
resolve();
}
});
});
const provider = _.find(strapi.plugins.email.config.providers, { provider: config.provider });
if (!provider) {
throw new Error(`The provider package isn't installed. Please run \`npm install strapi-email-${config.provider}\``);
}
const actions = provider.init(config);
// Execute email function of the provider for all files.
return actions.send(options, cb);
}
};

View File

@ -24,6 +24,7 @@ module.exports = function() {
'./node_modules/strapi-helper-*',
'./node_modules/strapi-middleware-*',
'./node_modules/strapi-upload-*',
'./node_modules/strapi-email-*',
'./node_modules/strapi-lint'
]
}, (err, files) => {