Files can be added to templates and managed in a dedicated "Files" view.
Mosaico integration in progress. The files seem to be working for Mosaico.
This commit is contained in:
parent
c85f2d4440
commit
b5cdf57f72
23 changed files with 506 additions and 164 deletions
|
@ -3,18 +3,19 @@
|
|||
import React, {Component} from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import {translate} from "react-i18next";
|
||||
import {requiresAuthenticatedUser} from "./lib/page";
|
||||
import {ACEEditor, Button, Form, FormSendMethod, withForm} from "./lib/form";
|
||||
import {withErrorHandling} from "./lib/error-handling";
|
||||
import {Table} from "./lib/table";
|
||||
import {requiresAuthenticatedUser} from "./page";
|
||||
import {withErrorHandling} from "./error-handling";
|
||||
import {Table} from "./table";
|
||||
import Dropzone from "react-dropzone";
|
||||
import {ModalDialog} from "./lib/modals";
|
||||
import {Icon} from "./lib/bootstrap-components";
|
||||
import {ModalDialog} from "./modals";
|
||||
import {Icon} from "./bootstrap-components";
|
||||
import axios from './axios';
|
||||
import styles from "./styles.scss";
|
||||
import {withPageHelpers} from "./page";
|
||||
|
||||
@translate()
|
||||
@withForm
|
||||
@withErrorHandling
|
||||
@withPageHelpers
|
||||
@requiresAuthenticatedUser
|
||||
export default class Files extends Component {
|
||||
constructor(props) {
|
||||
|
@ -26,16 +27,18 @@ export default class Files extends Component {
|
|||
};
|
||||
|
||||
const t = props.t;
|
||||
|
||||
this.initForm();
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
title: PropTypes.string,
|
||||
entity: PropTypes.object,
|
||||
entityTypeId: PropTypes.string
|
||||
entityTypeId: PropTypes.string,
|
||||
usePublicDownloadUrls: PropTypes.bool
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
usePublicDownloadUrls: true
|
||||
}
|
||||
|
||||
getFilesUploadedMessage(response){
|
||||
const t = this.props.t;
|
||||
|
@ -56,21 +59,21 @@ export default class Files extends Component {
|
|||
onDrop(files){
|
||||
const t = this.props.t;
|
||||
if (files.length > 0) {
|
||||
this.setFormStatusMessage('info', t('Uploading {{count}} file(s)', files.length));
|
||||
this.setFlashMessage('info', t('Uploading {{count}} file(s)', files.length));
|
||||
const data = new FormData();
|
||||
for (const file of files) {
|
||||
data.append('file', file)
|
||||
data.append('files[]', file)
|
||||
}
|
||||
axios.put(`/rest/files/${this.props.entityTypeId}, ${this.props.entity.id}`, data)
|
||||
axios.post(`/rest/files/${this.props.entityTypeId}/${this.props.entity.id}`, data)
|
||||
.then(res => {
|
||||
this.filesTable.refresh();
|
||||
const message = this.getFilesUploadedMessage(res);
|
||||
this.setFormStatusMessage('info', message);
|
||||
this.setFlashMessage('info', message);
|
||||
})
|
||||
.catch(res => this.setFormStatusMessage('danger', t('File upload failed: ') + res.message));
|
||||
.catch(res => this.setFlashMessage('danger', t('File upload failed: ') + res.message));
|
||||
}
|
||||
else{
|
||||
this.setFormStatusMessage('info', t('No files to upload'));
|
||||
this.setFlashMessage('info', t('No files to upload'));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -88,16 +91,13 @@ export default class Files extends Component {
|
|||
await this.hideDeleteFile();
|
||||
|
||||
try {
|
||||
this.disableForm();
|
||||
this.setFormStatusMessage('info', t('Deleting file ...'));
|
||||
this.setFlashMessage('info', t('Deleting file ...'));
|
||||
await axios.delete(`/rest/files/${this.props.entityTypeId}/${fileToDeleteId}`);
|
||||
this.filesTable.refresh();
|
||||
this.setFormStatusMessage('info', t('File deleted'));
|
||||
this.enableForm();
|
||||
this.setFlashMessage('info', t('File deleted'));
|
||||
} catch (err) {
|
||||
this.filesTable.refresh();
|
||||
this.setFormStatusMessage('danger', t('Delete file failed: ') + err.message);
|
||||
this.enableForm();
|
||||
this.setFlashMessage('danger', t('Delete file failed: ') + err.message);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -106,14 +106,21 @@ export default class Files extends Component {
|
|||
|
||||
const columns = [
|
||||
{ data: 1, title: "Name" },
|
||||
{ data: 2, title: "Size" },
|
||||
{ data: 3, title: "Size" },
|
||||
{
|
||||
actions: data => {
|
||||
|
||||
let downloadUrl;
|
||||
if (this.props.usePublicDownloadUrls) {
|
||||
downloadUrl =`/files/${this.props.entityTypeId}/${this.props.entity.id}/${data[2]}`;
|
||||
} else {
|
||||
downloadUrl =`/rest/files/${this.props.entityTypeId}/${data[0]}`;
|
||||
}
|
||||
|
||||
const actions = [
|
||||
{
|
||||
label: <Icon icon="download" title={t('Download')}/>,
|
||||
href: `/rest/files/${this.props.entityTypeId}/${data[0]}`
|
||||
href: downloadUrl
|
||||
},
|
||||
{
|
||||
label: <Icon icon="remove" title={t('Delete')}/>,
|
||||
|
@ -138,10 +145,10 @@ export default class Files extends Component {
|
|||
]}>
|
||||
{t('Are you sure you want to delete file "{{name}}"?', {name: this.state.fileToDeleteName})}
|
||||
</ModalDialog>
|
||||
<Dropzone onDrop={::this.onDrop} className="dropZone" activeClassName="dropZoneActive">
|
||||
<Dropzone onDrop={::this.onDrop} className={styles.dropZone} activeClassName="dropZoneActive">
|
||||
{state => state.isDragActive ? t('Drop {{count}} file(s)', {count:state.draggedFiles.length}) : t('Drop files here')}
|
||||
</Dropzone>
|
||||
<Table withHeader ref={node => this.filesTable = node} dataUrl={`/rest/template-files-table/${this.props.entity.id}`} columns={columns} />
|
||||
<Table withHeader ref={node => this.filesTable = node} dataUrl={`/rest/files-table/${this.props.entityTypeId}/${this.props.entity.id}`} columns={columns} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -86,4 +86,17 @@
|
|||
|
||||
.secondaryNav > li > a {
|
||||
padding: 3px 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.dropZone {
|
||||
padding-top: 30px;
|
||||
padding-bottom: 30px;
|
||||
margin-bottom: 15px;
|
||||
margin-top: 8px;
|
||||
border: 2px solid #E6E9ED;
|
||||
border-radius: 5px;
|
||||
background-color: #FAFAD2;
|
||||
text-align: center;
|
||||
font-size: 20px;
|
||||
color: #808080;
|
||||
}
|
||||
|
|
|
@ -8,20 +8,41 @@ import {
|
|||
} from 'react-i18next';
|
||||
import i18n from '../lib/i18n';
|
||||
import PropTypes from "prop-types";
|
||||
import styles from "./styles.scss";
|
||||
|
||||
const ResourceType = {
|
||||
TEMPLATE: 'template',
|
||||
CAMPAIGN: 'campaign'
|
||||
}
|
||||
|
||||
@translate()
|
||||
class MosaicoEditor extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.viewModel = null;
|
||||
this.state = {
|
||||
entityTypeId: ResourceType.TEMPLATE, // FIXME
|
||||
entityId: 13 // FIXME
|
||||
}
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
//structure: PropTypes.oneOfType([PropTypes.object, PropTypes.func]).isRequired,
|
||||
}
|
||||
|
||||
async onClose(evt) {
|
||||
const t = this.props.t;
|
||||
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
|
||||
if (confirm(t('Unsaved changes will be lost. Close now?'))) {
|
||||
window.location.href = `/${this.state.entityTypeId}s/${this.state.entityId}/edit`;
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const basePath = '/public/mosaico';
|
||||
const publicPath = '/public/mosaico';
|
||||
|
||||
if (!Mosaico.isCompatible()) {
|
||||
alert('Update your browser!');
|
||||
|
@ -30,35 +51,57 @@ class MosaicoEditor extends Component {
|
|||
|
||||
const plugins = window.mosaicoPlugins;
|
||||
|
||||
plugins.push(viewModel => {
|
||||
this.viewModel = viewModel;
|
||||
});
|
||||
|
||||
// (Custom) HTML postRenderers
|
||||
plugins.push(viewModel => {
|
||||
viewModel.originalExportHTML = viewModel.exportHTML;
|
||||
viewModel.exportHTML = () => {
|
||||
let html = viewModel.originalExportHTML();
|
||||
for (const portRender of window.mosaicoHTMLPostRenderers) {
|
||||
html = postRender(html);
|
||||
}
|
||||
return html;
|
||||
};
|
||||
});
|
||||
|
||||
plugins.unshift(vm => {
|
||||
// This is a fix for the use of hardcoded path in Mosaico
|
||||
vm.logoPath = basePath + '/img/mosaico32.png'
|
||||
vm.logoPath = publicPath + '/img/mosaico32.png'
|
||||
});
|
||||
|
||||
const config = {
|
||||
imgProcessorBackend: basePath+'/img/',
|
||||
emailProcessorBackend: basePath+'/dl/',
|
||||
imgProcessorBackend: `/mosaico/img/${this.state.entityTypeId}/${this.state.entityId}`,
|
||||
emailProcessorBackend: '/mosaico/dl/',
|
||||
titleToken: "MOSAICO Responsive Email Designer",
|
||||
fileuploadConfig: {
|
||||
url: basePath+'/upload/'
|
||||
url: `/mosaico/upload/${this.state.entityTypeId}/${this.state.entityId}`
|
||||
},
|
||||
strings: window.mosaicoLanguageStrings
|
||||
};
|
||||
|
||||
const metadata = undefined;
|
||||
const model = undefined;
|
||||
const template = basePath + '/templates/versafix-1/index.html';
|
||||
const template = publicPath + '/templates/versafix-1/index.html';
|
||||
|
||||
Mosaico.start(config, template, metadata, model, plugins);
|
||||
const allPlugins = plugins.concat(window.mosaicoPlugins);
|
||||
|
||||
Mosaico.start(config, template, metadata, model, allPlugins);
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
|
||||
}
|
||||
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={styles.navbar}>
|
||||
<img className={styles.logo} src="/public/mailtrain-header.png"/>
|
||||
<a className={styles.btn} onClick={::this.onClose}>{t('CLOSE')}</a>
|
||||
<a className={styles.btn}><span></span></a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
35
client/src/mosaico/styles.scss
Normal file
35
client/src/mosaico/styles.scss
Normal file
|
@ -0,0 +1,35 @@
|
|||
:global .mo-standalone {
|
||||
top: 34px;
|
||||
bottom: 0px;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.navbar {
|
||||
background: #DE4320;
|
||||
overflow: hidden;
|
||||
height: 34px;
|
||||
top: -34px;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: 24px;
|
||||
padding: 5px 0 5px 10px;
|
||||
filter: brightness(0) invert(1);
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: block;
|
||||
float: right;
|
||||
width: 150px;
|
||||
line-height: 34px;
|
||||
text-align: center;
|
||||
color: white;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
font-family: sans-serif;
|
||||
cursor: pointer;
|
||||
border-left: 1px solid #972E15;
|
||||
}
|
|
@ -64,6 +64,13 @@ export default class List extends Component {
|
|||
});
|
||||
}
|
||||
|
||||
if (perms.includes('manageFiles')) {
|
||||
actions.push({
|
||||
label: <Icon icon="hdd" title={t('Files')}/>,
|
||||
link: `/templates/${data[0]}/files`
|
||||
});
|
||||
}
|
||||
|
||||
if (perms.includes('share')) {
|
||||
actions.push({
|
||||
label: <Icon icon="share-alt" title={t('Share')}/>,
|
||||
|
|
|
@ -9,6 +9,7 @@ import { Section } from '../lib/page';
|
|||
import TemplatesCUD from './CUD';
|
||||
import TemplatesList from './List';
|
||||
import Share from '../shares/Share';
|
||||
import Files from "../lib/files";
|
||||
|
||||
|
||||
function getMenus(t) {
|
||||
|
@ -31,6 +32,12 @@ function getMenus(t) {
|
|||
visible: resolved => resolved.template.permissions.includes('edit'),
|
||||
panelRender: props => <TemplatesCUD action={props.match.params.action} entity={props.resolved.template} />
|
||||
},
|
||||
files: {
|
||||
title: t('Files'),
|
||||
link: params => `/templates/${params.templateId}/files`,
|
||||
visible: resolved => resolved.template.permissions.includes('edit'),
|
||||
panelRender: props => <Files title={t('Files')} entity={props.resolved.template} entityTypeId="template" />
|
||||
},
|
||||
share: {
|
||||
title: t('Share'),
|
||||
link: params => `/templates/${params.templateId}/share`,
|
||||
|
|
|
@ -15,7 +15,7 @@ module.exports = {
|
|||
rules: [
|
||||
{
|
||||
test: /\.(js|jsx)$/,
|
||||
exclude: /(disposables|react-dnd-touch-backend)/ /* https://github.com/react-dnd/react-dnd/issues/407 */,
|
||||
exclude: /(disposables|react-dnd-touch-backend|attr-accept)/ /* https://github.com/react-dnd/react-dnd/issues/407 */,
|
||||
use: [ 'babel-loader' ]
|
||||
},
|
||||
{
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue