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:
Tomas Bures 2018-03-24 23:55:50 +01:00
parent c85f2d4440
commit b5cdf57f72
23 changed files with 506 additions and 164 deletions

View file

@ -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>
);
}

View file

@ -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;
}

View file

@ -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>
);
}

View 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;
}

View file

@ -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')}/>,

View file

@ -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`,

View file

@ -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' ]
},
{