Fixed sandbox. Multiple tabs work now.
WiP on selectable mosaico templates. TODO: Make files always point to trusted URL, such that we don't have to rebase them. They are public anyway. The same goes for mosaico endpoints: /mosaico/templates and /mosaico/img
This commit is contained in:
parent
a4ee1534cc
commit
7788b0bc67
79 changed files with 724 additions and 390 deletions
|
@ -153,10 +153,6 @@ function createApp(trusted) {
|
|||
}));
|
||||
|
||||
app.use(cookieParser());
|
||||
useWith404Fallback('/public', express.static(path.join(__dirname, 'client', 'public')));
|
||||
useWith404Fallback('/mailtrain', express.static(path.join(__dirname, 'client', 'dist')));
|
||||
useWith404Fallback('/locales', express.static(path.join(__dirname, 'client', 'locales')));
|
||||
|
||||
app.use(session({
|
||||
store: config.redis.enabled ? new RedisStore(config.redis) : false,
|
||||
secret: config.www.secret,
|
||||
|
@ -185,6 +181,10 @@ function createApp(trusted) {
|
|||
app.use(passport.tryAuthByRestrictedAccessToken);
|
||||
}
|
||||
|
||||
useWith404Fallback('/public', express.static(path.join(__dirname, 'client', 'public')));
|
||||
useWith404Fallback('/mailtrain', express.static(path.join(__dirname, 'client', 'dist')));
|
||||
useWith404Fallback('/locales', express.static(path.join(__dirname, 'client', 'locales')));
|
||||
|
||||
/* FIXME - can we remove this???
|
||||
|
||||
// make sure flash messages are available
|
||||
|
|
|
@ -24,7 +24,7 @@ export default class API extends Component {
|
|||
|
||||
@withAsyncErrorHandler
|
||||
async loadAccessToken() {
|
||||
const response = await axios.get('/rest/access-token');
|
||||
const response = await axios.get(getUrl('rest/access-token'));
|
||||
this.setState({
|
||||
accessToken: response.data
|
||||
});
|
||||
|
@ -35,7 +35,7 @@ export default class API extends Component {
|
|||
}
|
||||
|
||||
async resetAccessToken() {
|
||||
const response = await axios.post('/rest/access-token-reset');
|
||||
const response = await axios.post(getUrl('rest/access-token-reset'));
|
||||
this.setState({
|
||||
accessToken: response.data
|
||||
});
|
||||
|
|
|
@ -26,7 +26,7 @@ export default class Account extends Component {
|
|||
|
||||
this.initForm({
|
||||
serverValidation: {
|
||||
url: '/rest/account-validate',
|
||||
url: 'rest/account-validate',
|
||||
changed: ['email', 'currentPassword']
|
||||
}
|
||||
});
|
||||
|
@ -34,7 +34,7 @@ export default class Account extends Component {
|
|||
|
||||
@withAsyncErrorHandler
|
||||
async loadFormValues() {
|
||||
await this.getFormValuesFromURL('/rest/account', data => {
|
||||
await this.getFormValuesFromURL('rest/account', data => {
|
||||
data.password = '';
|
||||
data.password2 = '';
|
||||
data.currentPassword = '';
|
||||
|
@ -113,7 +113,7 @@ export default class Account extends Component {
|
|||
this.disableForm();
|
||||
this.setFormStatusMessage('info', t('Updating user profile ...'));
|
||||
|
||||
const submitSuccessful = await this.validateAndSendFormValuesToURL(FormSendMethod.POST, '/rest/account', data => {
|
||||
const submitSuccessful = await this.validateAndSendFormValuesToURL(FormSendMethod.POST, 'rest/account', data => {
|
||||
delete data.password2;
|
||||
});
|
||||
|
||||
|
|
|
@ -44,7 +44,7 @@ export default class Forget extends Component {
|
|||
this.disableForm();
|
||||
this.setFormStatusMessage('info', t('Processing ...'));
|
||||
|
||||
const submitSuccessful = await this.validateAndSendFormValuesToURL(FormSendMethod.POST, '/rest/password-reset-send');
|
||||
const submitSuccessful = await this.validateAndSendFormValuesToURL(FormSendMethod.POST, 'rest/password-reset-send');
|
||||
|
||||
if (submitSuccessful) {
|
||||
this.navigateToWithFlashMessage('/account/login', 'success', t('If the username / email exists in the system, password reset link will be sent to the registered email.'));
|
||||
|
|
|
@ -59,7 +59,7 @@ export default class Login extends Component {
|
|||
this.disableForm();
|
||||
this.setFormStatusMessage('info', t('Verifying credentials ...'));
|
||||
|
||||
const submitSuccessful = await this.validateAndSendFormValuesToURL(FormSendMethod.POST, '/rest/login');
|
||||
const submitSuccessful = await this.validateAndSendFormValuesToURL(FormSendMethod.POST, 'rest/login');
|
||||
|
||||
if (submitSuccessful) {
|
||||
const nextUrl = qs.parse(this.props.location.search).next || getUrl();
|
||||
|
|
|
@ -11,6 +11,7 @@ import { withErrorHandling, withAsyncErrorHandler } from '../lib/error-handling'
|
|||
import passwordValidator from '../../../shared/password-validator';
|
||||
import axios from '../lib/axios';
|
||||
import interoperableErrors from '../../../shared/interoperable-errors';
|
||||
import {getUrl} from "../lib/urls";
|
||||
|
||||
const ResetTokenValidationState = {
|
||||
PENDING: 0,
|
||||
|
@ -39,7 +40,7 @@ export default class Account extends Component {
|
|||
async validateResetToken() {
|
||||
const params = this.props.match.params;
|
||||
|
||||
const response = await axios.post('/rest/password-reset-validate', {
|
||||
const response = await axios.post(getUrl('rest/password-reset-validate'), {
|
||||
username: params.username,
|
||||
resetToken: params.resetToken
|
||||
});
|
||||
|
@ -90,7 +91,7 @@ export default class Account extends Component {
|
|||
this.disableForm();
|
||||
this.setFormStatusMessage('info', t('Resetting password ...'));
|
||||
|
||||
const submitSuccessful = await this.validateAndSendFormValuesToURL(FormSendMethod.POST, '/rest/password-reset', data => {
|
||||
const submitSuccessful = await this.validateAndSendFormValuesToURL(FormSendMethod.POST, 'rest/password-reset', data => {
|
||||
delete data.password2;
|
||||
});
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ import {Table} from "../lib/table";
|
|||
import {ButtonRow, Form, InputField, withForm, FormSendMethod} from "../lib/form";
|
||||
import {Button, Icon} from "../lib/bootstrap-components";
|
||||
import axios from "../lib/axios";
|
||||
import {getUrl} from "../lib/urls";
|
||||
|
||||
@translate()
|
||||
@withForm
|
||||
|
@ -24,7 +25,7 @@ export default class List extends Component {
|
|||
|
||||
this.initForm({
|
||||
serverValidation: {
|
||||
url: '/rest/blacklist-validate',
|
||||
url: 'rest/blacklist-validate',
|
||||
changed: ['email']
|
||||
}
|
||||
});
|
||||
|
@ -64,7 +65,7 @@ export default class List extends Component {
|
|||
this.disableForm();
|
||||
this.setFormStatusMessage('info', t('Saving ...'));
|
||||
|
||||
const submitSuccessful = await this.validateAndSendFormValuesToURL(FormSendMethod.POST, '/rest/blacklist');
|
||||
const submitSuccessful = await this.validateAndSendFormValuesToURL(FormSendMethod.POST, 'rest/blacklist');
|
||||
|
||||
if (submitSuccessful) {
|
||||
this.hideFormValidation();
|
||||
|
@ -86,7 +87,7 @@ export default class List extends Component {
|
|||
|
||||
@withAsyncErrorHandler
|
||||
async deleteBlacklisted(email) {
|
||||
await axios.delete(`/rest/blacklist/${email}`);
|
||||
await axios.delete(getUrl(`rest/blacklist/${email}`));
|
||||
this.blacklistTable.refresh();
|
||||
}
|
||||
|
||||
|
@ -122,7 +123,7 @@ export default class List extends Component {
|
|||
|
||||
<h3 className="legend">{t('Blacklisted Emails')}</h3>
|
||||
|
||||
<Table ref={node => this.blacklistTable = node} withHeader dataUrl="/rest/blacklist-table" columns={columns} />
|
||||
<Table ref={node => this.blacklistTable = node} withHeader dataUrl="rest/blacklist-table" columns={columns} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -9,10 +9,10 @@ const axiosInst = axios.create({
|
|||
});
|
||||
|
||||
const axiosWrapper = {
|
||||
get: (...args) => axiosInst.get(...args).catch(error => { throw interoperableErrors.deserialize(error.response.data) || error }),
|
||||
put: (...args) => axiosInst.put(...args).catch(error => { throw interoperableErrors.deserialize(error.response.data) || error }),
|
||||
post: (...args) => axiosInst.post(...args).catch(error => { throw interoperableErrors.deserialize(error.response.data) || error }),
|
||||
delete: (...args) => axiosInst.delete(...args).catch(error => { throw interoperableErrors.deserialize(error.response.data) || error })
|
||||
get: (...args) => axiosInst.get(...args).catch(error => { throw (error.response && interoperableErrors.deserialize(error.response.data)) || error }),
|
||||
put: (...args) => axiosInst.put(...args).catch(error => { throw (error.response && interoperableErrors.deserialize(error.response.data)) || error }),
|
||||
post: (...args) => axiosInst.post(...args).catch(error => { throw (error.response && interoperableErrors.deserialize(error.response.data)) || error }),
|
||||
delete: (...args) => axiosInst.delete(...args).catch(error => { throw (error.response && interoperableErrors.deserialize(error.response.data)) || error })
|
||||
};
|
||||
|
||||
const HTTPMethod = {
|
||||
|
|
|
@ -12,6 +12,7 @@ import {Icon} from "./bootstrap-components";
|
|||
import axios from './axios';
|
||||
import styles from "./styles.scss";
|
||||
import {withPageHelpers} from "./page";
|
||||
import {getUrl} from "./urls";
|
||||
|
||||
@translate()
|
||||
@withErrorHandling
|
||||
|
@ -64,7 +65,7 @@ export default class Files extends Component {
|
|||
for (const file of files) {
|
||||
data.append('files[]', file)
|
||||
}
|
||||
axios.post(`/rest/files/${this.props.entityTypeId}/${this.props.entity.id}`, data)
|
||||
axios.post(getUrl(`rest/files/${this.props.entityTypeId}/${this.props.entity.id}`), data)
|
||||
.then(res => {
|
||||
this.filesTable.refresh();
|
||||
const message = this.getFilesUploadedMessage(res);
|
||||
|
@ -92,7 +93,7 @@ export default class Files extends Component {
|
|||
|
||||
try {
|
||||
this.setFlashMessage('info', t('Deleting file ...'));
|
||||
await axios.delete(`/rest/files/${this.props.entityTypeId}/${fileToDeleteId}`);
|
||||
await axios.delete(getUrl(`rest/files/${this.props.entityTypeId}/${fileToDeleteId}`));
|
||||
this.filesTable.refresh();
|
||||
this.setFlashMessage('info', t('File deleted'));
|
||||
} catch (err) {
|
||||
|
@ -114,7 +115,7 @@ export default class Files extends Component {
|
|||
if (this.props.usePublicDownloadUrls) {
|
||||
downloadUrl =`/files/${this.props.entityTypeId}/${this.props.entity.id}/${data[2]}`;
|
||||
} else {
|
||||
downloadUrl =`/rest/files/${this.props.entityTypeId}/${data[0]}`;
|
||||
downloadUrl =`rest/files/${this.props.entityTypeId}/${data[0]}`;
|
||||
}
|
||||
|
||||
const actions = [
|
||||
|
@ -148,7 +149,7 @@ export default class Files extends Component {
|
|||
<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/files-table/${this.props.entityTypeId}/${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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ import {Button, Icon} from "./bootstrap-components";
|
|||
import brace from 'brace';
|
||||
import ACEEditorRaw from 'react-ace';
|
||||
import 'brace/theme/github';
|
||||
import 'brace/ext/searchbox';
|
||||
|
||||
import CKEditorRaw from "react-ckeditor-component";
|
||||
|
||||
|
@ -24,6 +25,7 @@ import { parseDate, parseBirthday, formatDate, formatBirthday, DateFormat, birth
|
|||
|
||||
import styles from "./styles.scss";
|
||||
import moment from "moment";
|
||||
import {getUrl} from "./urls";
|
||||
|
||||
const FormState = {
|
||||
Loading: 0,
|
||||
|
@ -711,7 +713,8 @@ class TableSelect extends Component {
|
|||
id: PropTypes.string.isRequired,
|
||||
label: PropTypes.string.isRequired,
|
||||
help: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
|
||||
format: PropTypes.string
|
||||
format: PropTypes.string,
|
||||
disabled: PropTypes.bool
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
|
@ -772,11 +775,13 @@ class TableSelect extends Component {
|
|||
if (props.dropdown) {
|
||||
return wrapInput(id, htmlId, owner, props.format, '', props.label, props.help,
|
||||
<div>
|
||||
<div className={`input-group ${styles.tableSelectDropdown}`}>
|
||||
<input type="text" className="form-control" value={this.state.selectedLabel} readOnly onClick={::this.toggleOpen}/>
|
||||
<div className={(props.disabled ? '' : 'input-group ') + styles.tableSelectDropdown}>
|
||||
<input type="text" className="form-control" value={this.state.selectedLabel} onClick={::this.toggleOpen} readOnly={!props.disabled} disabled={props.disabled}/>
|
||||
{!props.disabled &&
|
||||
<span className="input-group-btn">
|
||||
<Button label={t('Select')} className="btn-default" onClickAsync={::this.toggleOpen}/>
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
<div className={styles.tableSelectTable + (this.state.open ? '' : ' ' + styles.tableSelectTableHidden)}>
|
||||
<Table ref={node => this.table = node} data={props.data} dataUrl={props.dataUrl} columns={props.columns} selectMode={props.selectMode} selectionAsArray={this.props.selectionAsArray} withHeader={props.withHeader} selectionKeyIndex={props.selectionKeyIndex} selection={owner.getFormValue(id)} onSelectionDataAsync={::this.onSelectionDataAsync} onSelectionChangedAsync={::this.onSelectionChangedAsync}/>
|
||||
|
@ -926,7 +931,7 @@ function withForm(target) {
|
|||
if (payloadNotEmpty) {
|
||||
mutState.set('isServerValidationRunning', true);
|
||||
|
||||
axios.post(settings.serverValidation.url, payload)
|
||||
axios.post(getUrl(settings.serverValidation.url), payload)
|
||||
.then(response => {
|
||||
|
||||
self.setState(previousState => ({
|
||||
|
@ -1011,7 +1016,7 @@ function withForm(target) {
|
|||
});
|
||||
}, 500);
|
||||
|
||||
const response = await axios.get(url);
|
||||
const response = await axios.get(getUrl(url));
|
||||
|
||||
const data = response.data;
|
||||
|
||||
|
@ -1037,7 +1042,7 @@ function withForm(target) {
|
|||
mutator(data);
|
||||
}
|
||||
|
||||
const response = await axios.method(method, url, data);
|
||||
const response = await axios.method(method, getUrl(url), data);
|
||||
|
||||
return response.data || true;
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@ import i18n from 'i18next';
|
|||
import XHR from 'i18next-xhr-backend';
|
||||
// import Cache from 'i18next-localstorage-cache';
|
||||
import mailtrainConfig from 'mailtrainConfig';
|
||||
import {getUrl} from "./urls";
|
||||
|
||||
i18n
|
||||
.use(XHR)
|
||||
|
@ -23,6 +24,10 @@ i18n
|
|||
|
||||
interpolation: {
|
||||
escapeValue: false // not needed for react
|
||||
},
|
||||
|
||||
backend: {
|
||||
loadPath: getUrl('locales/{{lng}}/{{ns}}.json')
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ import axios, { HTTPMethod } from './axios';
|
|||
import { translate } from 'react-i18next';
|
||||
import PropTypes from 'prop-types';
|
||||
import {ModalDialog} from "./bootstrap-components";
|
||||
import {getUrl} from "./urls";
|
||||
|
||||
@translate()
|
||||
class RestActionModalDialog extends Component {
|
||||
|
@ -35,7 +36,7 @@ class RestActionModalDialog extends Component {
|
|||
try {
|
||||
owner.disableForm();
|
||||
owner.setFormStatusMessage('info', this.props.actionInProgressMsg);
|
||||
await axios.method(this.props.actionMethod, this.props.actionUrl);
|
||||
await axios.method(this.props.actionMethod, getUrl(this.props.actionUrl));
|
||||
|
||||
owner.navigateToWithFlashMessage(this.props.successUrl, 'success', this.props.actionDoneMsg);
|
||||
} catch (err) {
|
||||
|
|
|
@ -1,14 +1,13 @@
|
|||
'use strict';
|
||||
|
||||
import './public-path';
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import {
|
||||
I18nextProvider,
|
||||
} from 'react-i18next';
|
||||
import {I18nextProvider,} from 'react-i18next';
|
||||
import i18n from './i18n';
|
||||
import styles from "./mosaico.scss";
|
||||
import { MosaicoSandbox } from './mosaico';
|
||||
import { UntrustedContentRoot } from './untrusted';
|
||||
import {MosaicoSandbox} from './mosaico';
|
||||
import {UntrustedContentRoot} from './untrusted';
|
||||
|
||||
export default function() {
|
||||
ReactDOM.render(
|
||||
|
|
|
@ -1,21 +1,15 @@
|
|||
'use strict';
|
||||
|
||||
import React, {Component} from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import {
|
||||
I18nextProvider,
|
||||
translate
|
||||
} from 'react-i18next';
|
||||
import i18n from './i18n';
|
||||
import {translate} from 'react-i18next';
|
||||
import PropTypes from "prop-types";
|
||||
import styles from "./mosaico.scss";
|
||||
import mailtrainConfig from 'mailtrainConfig';
|
||||
|
||||
import { UntrustedContentHost } from './untrusted';
|
||||
import {
|
||||
Button,
|
||||
Icon
|
||||
} from "./bootstrap-components";
|
||||
import {UntrustedContentHost} from './untrusted';
|
||||
import {Icon} from "./bootstrap-components";
|
||||
import {getUrl} from "./urls";
|
||||
import {base, unbase} from "../../../shared/templates";
|
||||
|
||||
|
||||
export const ResourceType = {
|
||||
TEMPLATE: 'template',
|
||||
|
@ -36,7 +30,10 @@ export class MosaicoEditor extends Component {
|
|||
entityTypeId: PropTypes.string,
|
||||
entity: PropTypes.object,
|
||||
title: PropTypes.string,
|
||||
onFullscreenAsync: PropTypes.func
|
||||
onFullscreenAsync: PropTypes.func,
|
||||
templateId: PropTypes.number,
|
||||
initialModel: PropTypes.string,
|
||||
initialMetadata: PropTypes.string
|
||||
}
|
||||
|
||||
async toggleFullscreenAsync() {
|
||||
|
@ -57,18 +54,19 @@ export class MosaicoEditor extends Component {
|
|||
const mosaicoData = {
|
||||
entityTypeId: this.props.entityTypeId,
|
||||
entityId: this.props.entity.id,
|
||||
model: this.props.entity.data.model,
|
||||
metadata: this.props.entity.data.metadata
|
||||
templateId: this.props.templateId,
|
||||
initialModel: this.props.initialModel,
|
||||
initialMetadata: this.props.initialMetadata
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={this.state.fullscreen ? styles.editorFullscreen : styles.editor}>
|
||||
<div className={styles.navbar}>
|
||||
{this.state.fullscreen && <img className={styles.logo} src="/public/mailtrain-notext.png"/>}
|
||||
{this.state.fullscreen && <img className={styles.logo} src={getUrl('public/mailtrain-notext.png')}/>}
|
||||
<div className={styles.title}>{this.props.title}</div>
|
||||
<a className={styles.btn} onClick={::this.toggleFullscreenAsync}><Icon icon="fullscreen"/></a>
|
||||
</div>
|
||||
<UntrustedContentHost ref={node => this.contentNode = node} className={styles.host} contentProps={mosaicoData} contentSrc="mosaico/editor" tokenMethod="mosaico" tokenParams={mosaicoData}/>
|
||||
<UntrustedContentHost ref={node => this.contentNode = node} className={styles.host} singleToken={true} contentProps={mosaicoData} contentSrc="mosaico/editor" tokenMethod="mosaico" tokenParams={mosaicoData}/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -92,12 +90,13 @@ export class MosaicoSandbox extends Component {
|
|||
static propTypes = {
|
||||
entityTypeId: PropTypes.string,
|
||||
entityId: PropTypes.number,
|
||||
model: PropTypes.object,
|
||||
metadata: PropTypes.object
|
||||
templateId: PropTypes.number,
|
||||
initialModel: PropTypes.string,
|
||||
initialMetadata: PropTypes.string
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const publicPath = '/public/mosaico';
|
||||
const publicPath = 'public/mosaico';
|
||||
|
||||
if (!Mosaico.isCompatible()) {
|
||||
alert('Update your browser!');
|
||||
|
@ -124,22 +123,23 @@ export class MosaicoSandbox extends Component {
|
|||
|
||||
plugins.unshift(vm => {
|
||||
// This is an override of the default paths in Mosaico
|
||||
vm.logoPath = publicPath + '/img/mosaico32.png';
|
||||
vm.logoPath = getUrl(publicPath + '/img/mosaico32.png');
|
||||
vm.logoUrl = '#';
|
||||
});
|
||||
|
||||
const config = {
|
||||
imgProcessorBackend: `/mosaico/img/${this.props.entityTypeId}/${this.props.entityId}`,
|
||||
emailProcessorBackend: '/mosaico/dl/',
|
||||
imgProcessorBackend: getUrl(`mosaico/img/${this.props.entityTypeId}/${this.props.entityId}`),
|
||||
emailProcessorBackend: getUrl('mosaico/dl/'),
|
||||
fileuploadConfig: {
|
||||
url: `/mosaico/upload/${this.props.entityTypeId}/${this.props.entityId}`
|
||||
url: getUrl(`mosaico/upload/${this.props.entityTypeId}/${this.props.entityId}`)
|
||||
},
|
||||
strings: window.mosaicoLanguageStrings
|
||||
};
|
||||
|
||||
const metadata = this.props.metadata;
|
||||
const model = this.props.model;
|
||||
const template = publicPath + '/templates/versafix-1/index.html';
|
||||
const urlBase = getUrl();
|
||||
const metadata = this.props.initialMetadata && JSON.parse(base(this.props.initialMetadata, urlBase));
|
||||
const model = this.props.initialModel && JSON.parse(base(this.props.initialModel, urlBase));
|
||||
const template = getUrl(`mosaico/templates/${this.props.templateId}/index.html`);
|
||||
|
||||
const allPlugins = plugins.concat(window.mosaicoPlugins);
|
||||
|
||||
|
@ -148,10 +148,11 @@ export class MosaicoSandbox extends Component {
|
|||
|
||||
async onMethodAsync(method, params) {
|
||||
if (method === 'exportState') {
|
||||
const urlBase = getUrl();
|
||||
return {
|
||||
html: this.viewModel.exportHTML(),
|
||||
model: this.viewModel.exportJS(),
|
||||
metadata: this.viewModel.metadata
|
||||
html: unbase(this.viewModel.exportHTML(), urlBase),
|
||||
model: unbase(this.viewModel.exportJSON(), urlBase),
|
||||
metadata: unbase(this.viewModel.exportMetadata(), urlBase)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@ class NamespaceSelect extends Component {
|
|||
const t = this.props.t;
|
||||
|
||||
return (
|
||||
<TreeTableSelect id="namespace" label={t('Namespace')} dataUrl="/rest/namespaces-tree"/>
|
||||
<TreeTableSelect id="namespace" label={t('Namespace')} dataUrl="rest/namespaces-tree"/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import PropTypes from "prop-types";
|
|||
import {withRouter} from "react-router";
|
||||
import {withErrorHandling} from "./error-handling";
|
||||
import axios from "../lib/axios";
|
||||
import {getUrl} from "./urls";
|
||||
|
||||
function needsResolve(route, nextRoute, match, nextMatch) {
|
||||
const resolve = route.resolve;
|
||||
|
@ -30,7 +31,7 @@ async function resolve(route, match) {
|
|||
const promises = keys.map(key => {
|
||||
const url = route.resolve[key](match.params);
|
||||
if (url) {
|
||||
return axios.get(url);
|
||||
return axios.get(getUrl(url));
|
||||
} else {
|
||||
return Promise.resolve({data: null});
|
||||
}
|
||||
|
|
12
client/src/lib/permissions.js
Normal file
12
client/src/lib/permissions.js
Normal file
|
@ -0,0 +1,12 @@
|
|||
'use strict';
|
||||
|
||||
import {getUrl} from "./urls";
|
||||
import axios from "./axios";
|
||||
|
||||
async function checkPermissions(request) {
|
||||
return await axios.post(getUrl('rest/permissions-check'), request);
|
||||
}
|
||||
|
||||
export {
|
||||
checkPermissions
|
||||
}
|
5
client/src/lib/public-path.js
Normal file
5
client/src/lib/public-path.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
'use strict';
|
||||
|
||||
import {getUrl} from "./urls";
|
||||
|
||||
__webpack_public_path__ = getUrl();
|
|
@ -42,6 +42,12 @@
|
|||
opacity: 1;
|
||||
}
|
||||
|
||||
:global .form-control[disabled] {
|
||||
cursor: default;
|
||||
background-color: #eeeeee;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
:global .ace_editor {
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@ import axios from './axios';
|
|||
import { withPageHelpers } from './page'
|
||||
import { withErrorHandling, withAsyncErrorHandler } from './error-handling';
|
||||
import styles from "./styles.scss";
|
||||
import {getUrl} from "./urls";
|
||||
|
||||
//dtFactory();
|
||||
//dtSelectFactory();
|
||||
|
@ -122,7 +123,7 @@ class Table extends Component {
|
|||
@withAsyncErrorHandler
|
||||
async fetchData(data, callback) {
|
||||
// This custom ajax fetch function allows us to properly handle the case when the user is not authenticated.
|
||||
const response = await axios.post(this.props.dataUrl, data);
|
||||
const response = await axios.post(getUrl(this.props.dataUrl), data);
|
||||
callback(response.data);
|
||||
}
|
||||
|
||||
|
@ -138,7 +139,7 @@ class Table extends Component {
|
|||
}
|
||||
|
||||
if (keysToFetch.length > 0) {
|
||||
const response = await axios.post(this.props.dataUrl, {
|
||||
const response = await axios.post(getUrl(this.props.dataUrl), {
|
||||
operation: 'getBy',
|
||||
column: this.props.selectionKeyIndex,
|
||||
values: keysToFetch
|
||||
|
|
|
@ -15,6 +15,7 @@ import axios from './axios';
|
|||
import { withPageHelpers } from './page'
|
||||
import { withErrorHandling, withAsyncErrorHandler } from './error-handling';
|
||||
import styles from "./styles.scss";
|
||||
import {getUrl} from "./urls";
|
||||
|
||||
const TreeSelectMode = {
|
||||
NONE: 0,
|
||||
|
@ -47,7 +48,7 @@ class TreeTable extends Component {
|
|||
|
||||
@withAsyncErrorHandler
|
||||
async loadData(dataUrl) {
|
||||
const response = await axios.get(dataUrl);
|
||||
const response = await axios.get(getUrl(dataUrl));
|
||||
const treeData = response.data;
|
||||
|
||||
for (const root of treeData) {
|
||||
|
|
|
@ -3,12 +3,22 @@
|
|||
import React, {Component} from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import {translate} from "react-i18next";
|
||||
import {requiresAuthenticatedUser, withPageHelpers} from "./page";
|
||||
import {withAsyncErrorHandler, withErrorHandling} from "./error-handling";
|
||||
import {
|
||||
requiresAuthenticatedUser,
|
||||
withPageHelpers
|
||||
} from "./page";
|
||||
import {
|
||||
withAsyncErrorHandler,
|
||||
withErrorHandling
|
||||
} from "./error-handling";
|
||||
import axios from "./axios";
|
||||
import styles from "./styles.scss";
|
||||
import {getTrustedUrl, getSandboxUrl} from "./urls";
|
||||
import {Table} from "./table";
|
||||
import {
|
||||
getSandboxUrl,
|
||||
getTrustedUrl,
|
||||
getUrl,
|
||||
setRestrictedAccessToken
|
||||
} from "./urls";
|
||||
|
||||
@translate(null, { withRef: true })
|
||||
@withPageHelpers
|
||||
|
@ -37,7 +47,8 @@ export class UntrustedContentHost extends Component {
|
|||
contentProps: PropTypes.object,
|
||||
tokenMethod: PropTypes.string,
|
||||
tokenParams: PropTypes.object,
|
||||
className: PropTypes.string
|
||||
className: PropTypes.string,
|
||||
singleToken: PropTypes.bool
|
||||
}
|
||||
|
||||
isInitialized() {
|
||||
|
@ -73,6 +84,7 @@ export class UntrustedContentHost extends Component {
|
|||
const msgId = this.rpcCounter;
|
||||
|
||||
this.sendMessage('rpcRequest', {
|
||||
method,
|
||||
params,
|
||||
msgId
|
||||
});
|
||||
|
@ -85,20 +97,26 @@ export class UntrustedContentHost extends Component {
|
|||
|
||||
@withAsyncErrorHandler
|
||||
async refreshAccessToken() {
|
||||
const result = await axios.post(getTrustedUrl('rest/restricted-access-token'), {
|
||||
method: this.props.tokenMethod,
|
||||
params: this.props.tokenParams
|
||||
});
|
||||
if (this.props.singleToken && this.accessToken) {
|
||||
await axios.put(getUrl('rest/restricted-access-token'), {
|
||||
token: this.accessToken
|
||||
});
|
||||
} else {
|
||||
const result = await axios.post(getUrl('rest/restricted-access-token'), {
|
||||
method: this.props.tokenMethod,
|
||||
params: this.props.tokenParams
|
||||
});
|
||||
|
||||
this.accessToken = result.data;
|
||||
this.accessToken = result.data;
|
||||
|
||||
if (!this.state.hasAccessToken) {
|
||||
this.setState({
|
||||
hasAccessToken: true
|
||||
})
|
||||
if (!this.state.hasAccessToken) {
|
||||
this.setState({
|
||||
hasAccessToken: true
|
||||
})
|
||||
}
|
||||
|
||||
this.sendMessage('accessToken', this.accessToken);
|
||||
}
|
||||
|
||||
this.sendMessage('accessToken', this.accessToken);
|
||||
}
|
||||
|
||||
scheduleRefreshAccessToken() {
|
||||
|
@ -169,11 +187,6 @@ export class UntrustedContentRoot extends Component {
|
|||
}
|
||||
|
||||
|
||||
setAccessTokenCookie(token) {
|
||||
document.cookie = 'restricted_access_token=' + token + '; expires=' + (new Date(Date.now()+60000)).toUTCString();
|
||||
console.log(document.cookie);
|
||||
}
|
||||
|
||||
async receiveMessage(evt) {
|
||||
const msg = evt.data;
|
||||
console.log(msg);
|
||||
|
@ -182,14 +195,14 @@ export class UntrustedContentRoot extends Component {
|
|||
this.sendMessage('initNeeded');
|
||||
|
||||
} else if (msg.type === 'init' && !this.state.initialized) {
|
||||
this.setAccessTokenCookie(msg.data.accessToken);
|
||||
setRestrictedAccessToken(msg.data.accessToken);
|
||||
this.setState({
|
||||
initialized: true,
|
||||
contentProps: msg.data.contentProps
|
||||
});
|
||||
|
||||
} else if (msg.type === 'accessToken') {
|
||||
this.setAccessTokenCookie(msg.data);
|
||||
setRestrictedAccessToken(msg.data);
|
||||
} else if (msg.type === 'rpcRequest') {
|
||||
const ret = await this.contentNode.onMethodAsync(msg.data.method, msg.data.params);
|
||||
this.sendMessage('rpcResponse', {msgId: msg.data.msgId, ret});
|
||||
|
|
|
@ -2,12 +2,18 @@
|
|||
|
||||
import mailtrainConfig from "mailtrainConfig";
|
||||
|
||||
let restrictedAccessToken = 'ANONYMOUS';
|
||||
|
||||
function setRestrictedAccessToken(token) {
|
||||
restrictedAccessToken = token;
|
||||
}
|
||||
|
||||
function getTrustedUrl(path) {
|
||||
return mailtrainConfig.trustedUrlBase + (path || '');
|
||||
}
|
||||
|
||||
function getSandboxUrl(path) {
|
||||
return mailtrainConfig.sandboxUrlBase + (path || '');
|
||||
return mailtrainConfig.sandboxUrlBase + restrictedAccessToken + '/' + (path || '');
|
||||
}
|
||||
|
||||
function getUrl(path) {
|
||||
|
@ -22,7 +28,7 @@ function getBaseDir() {
|
|||
if (mailtrainConfig.trusted) {
|
||||
return mailtrainConfig.trustedUrlBaseDir;
|
||||
} else {
|
||||
return mailtrainConfig.sandboxUrlBaseDir;
|
||||
return mailtrainConfig.sandboxUrlBaseDir + 'ANONYMOUS';
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -30,5 +36,6 @@ export {
|
|||
getTrustedUrl,
|
||||
getSandboxUrl,
|
||||
getUrl,
|
||||
getBaseDir
|
||||
getBaseDir,
|
||||
setRestrictedAccessToken
|
||||
}
|
|
@ -81,10 +81,10 @@ export default class CUD extends Component {
|
|||
let sendMethod, url;
|
||||
if (this.props.entity) {
|
||||
sendMethod = FormSendMethod.PUT;
|
||||
url = `/rest/lists/${this.props.entity.id}`
|
||||
url = `rest/lists/${this.props.entity.id}`
|
||||
} else {
|
||||
sendMethod = FormSendMethod.POST;
|
||||
url = '/rest/lists'
|
||||
url = 'rest/lists'
|
||||
}
|
||||
|
||||
this.disableForm();
|
||||
|
@ -164,7 +164,7 @@ export default class CUD extends Component {
|
|||
<DeleteModalDialog
|
||||
stateOwner={this}
|
||||
visible={this.props.action === 'delete'}
|
||||
deleteUrl={`/rest/lists/${this.props.entity.id}`}
|
||||
deleteUrl={`rest/lists/${this.props.entity.id}`}
|
||||
cudUrl={`/lists/${this.props.entity.id}/edit`}
|
||||
listUrl="/lists"
|
||||
deletingMsg={t('Deleting list ...')}
|
||||
|
@ -186,14 +186,14 @@ export default class CUD extends Component {
|
|||
|
||||
<InputField id="contact_email" label={t('Contact email')} help={t('Contact email used in subscription forms and emails that are sent out. If not filled in, the admin email from the global settings will be used.')}/>
|
||||
<InputField id="homepage" label={t('Homepage')} help={t('Homepage URL used in subscription forms and emails that are sent out. If not filled in, the default homepage from global settings will be used.')}/>
|
||||
<TableSelect id="send_configuration" label={t('Send configuration')} withHeader dropdown dataUrl='/rest/send-configurations-table' columns={sendConfigurationsColumns} selectionLabelIndex={1} help={t('Send configuration that will be used for sending out subscription-related emails.')}/>
|
||||
<TableSelect id="send_configuration" label={t('Send configuration')} withHeader dropdown dataUrl='rest/send-configurations-table' columns={sendConfigurationsColumns} selectionLabelIndex={1} help={t('Send configuration that will be used for sending out subscription-related emails.')}/>
|
||||
|
||||
<NamespaceSelect/>
|
||||
|
||||
<Dropdown id="form" label={t('Forms')} options={formsOptions} help={t('Web and email forms and templates used in subscription management process.')}/>
|
||||
|
||||
{this.getFormValue('form') === 'custom' &&
|
||||
<TableSelect id="default_form" label={t('Custom forms')} withHeader dropdown dataUrl='/rest/forms-table' columns={customFormsColumns} selectionLabelIndex={1} help={<Trans>The custom form used for this list. You can create a form <a href={`/lists/forms/create/${this.props.entity.id}`}>here</a>.</Trans>}/>
|
||||
<TableSelect id="default_form" label={t('Custom forms')} withHeader dropdown dataUrl='rest/forms-table' columns={customFormsColumns} selectionLabelIndex={1} help={<Trans>The custom form used for this list. You can create a form <a href={`/lists/forms/create/${this.props.entity.id}`}>here</a>.</Trans>}/>
|
||||
}
|
||||
|
||||
<CheckBox id="public_subscribe" label={t('Subscription')} text={t('Allow public users to subscribe themselves')}/>
|
||||
|
|
|
@ -8,6 +8,7 @@ import { Table } from '../lib/table';
|
|||
import axios from '../lib/axios';
|
||||
import {Link} from "react-router-dom";
|
||||
import {Icon} from "../lib/bootstrap-components";
|
||||
import {checkPermissions} from "../lib/permissions";
|
||||
|
||||
@translate()
|
||||
@withPageHelpers
|
||||
|
@ -22,14 +23,12 @@ export default class List extends Component {
|
|||
|
||||
@withAsyncErrorHandler
|
||||
async fetchPermissions() {
|
||||
const request = {
|
||||
const result = await checkPermissions({
|
||||
createList: {
|
||||
entityTypeId: 'namespace',
|
||||
requiredOperations: ['createList']
|
||||
}
|
||||
};
|
||||
|
||||
const result = await axios.post('/rest/permissions-check', request);
|
||||
});
|
||||
|
||||
this.setState({
|
||||
createPermitted: result.data.createList
|
||||
|
@ -109,7 +108,7 @@ export default class List extends Component {
|
|||
|
||||
<Title>{t('Lists')}</Title>
|
||||
|
||||
<Table withHeader dataUrl="/rest/lists-table" columns={columns} />
|
||||
<Table withHeader dataUrl="rest/lists-table" columns={columns} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -34,7 +34,7 @@ export default class CUD extends Component {
|
|||
|
||||
this.initForm({
|
||||
serverValidation: {
|
||||
url: `/rest/fields-validate/${this.props.list.id}`,
|
||||
url: `rest/fields-validate/${this.props.list.id}`,
|
||||
changed: ['key'],
|
||||
extra: ['id']
|
||||
},
|
||||
|
@ -221,10 +221,10 @@ export default class CUD extends Component {
|
|||
let sendMethod, url;
|
||||
if (this.props.entity) {
|
||||
sendMethod = FormSendMethod.PUT;
|
||||
url = `/rest/fields/${this.props.list.id}/${this.props.entity.id}`
|
||||
url = `rest/fields/${this.props.list.id}/${this.props.entity.id}`
|
||||
} else {
|
||||
sendMethod = FormSendMethod.POST;
|
||||
url = `/rest/fields/${this.props.list.id}`
|
||||
url = `rest/fields/${this.props.list.id}`
|
||||
}
|
||||
|
||||
try {
|
||||
|
@ -411,7 +411,7 @@ export default class CUD extends Component {
|
|||
|
||||
fieldSettings =
|
||||
<Fieldset label={t('Field settings')}>
|
||||
<TableSelect id="group" label={t('Group')} withHeader dropdown dataUrl={`/rest/fields-grouped-table/${this.props.list.id}`} columns={fieldsGroupedColumns} selectionLabelIndex={1} help={t('Select group to which the options should belong.')}/>
|
||||
<TableSelect id="group" label={t('Group')} withHeader dropdown dataUrl={`rest/fields-grouped-table/${this.props.list.id}`} columns={fieldsGroupedColumns} selectionLabelIndex={1} help={t('Select group to which the options should belong.')}/>
|
||||
<InputField id="default_value" label={t('Default value')} help={t('Default value used when the field is empty.')}/>
|
||||
</Fieldset>;
|
||||
break;
|
||||
|
@ -424,7 +424,7 @@ export default class CUD extends Component {
|
|||
<DeleteModalDialog
|
||||
stateOwner={this}
|
||||
visible={this.props.action === 'delete'}
|
||||
deleteUrl={`/rest/fields/${this.props.list.id}/${this.props.entity.id}`}
|
||||
deleteUrl={`rest/fields/${this.props.list.id}/${this.props.entity.id}`}
|
||||
cudUrl={`/lists/${this.props.list.id}/fields/${this.props.entity.id}/edit`}
|
||||
listUrl={`/lists/${this.props.list.id}/fields`}
|
||||
deletingMsg={t('Deleting field ...')}
|
||||
|
|
|
@ -55,7 +55,7 @@ export default class List extends Component {
|
|||
|
||||
<Title>{t('Fields')}</Title>
|
||||
|
||||
<Table withHeader dataUrl={`/rest/fields-table/${this.props.list.id}`} columns={columns} />
|
||||
<Table withHeader dataUrl={`rest/fields-table/${this.props.list.id}`} columns={columns} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -52,7 +52,7 @@ export default class CUD extends Component {
|
|||
|
||||
this.initForm({
|
||||
serverValidation: {
|
||||
url: '/rest/forms-validate',
|
||||
url: 'rest/forms-validate',
|
||||
changed: this.serverValidatedFields
|
||||
}
|
||||
});
|
||||
|
@ -316,10 +316,10 @@ export default class CUD extends Component {
|
|||
let sendMethod, url;
|
||||
if (this.props.entity) {
|
||||
sendMethod = FormSendMethod.PUT;
|
||||
url = `/rest/forms/${this.props.entity.id}`
|
||||
url = `rest/forms/${this.props.entity.id}`
|
||||
} else {
|
||||
sendMethod = FormSendMethod.POST;
|
||||
url = '/rest/forms'
|
||||
url = 'rest/forms'
|
||||
}
|
||||
|
||||
this.disableForm();
|
||||
|
@ -374,7 +374,7 @@ export default class CUD extends Component {
|
|||
<DeleteModalDialog
|
||||
stateOwner={this}
|
||||
visible={this.props.action === 'delete'}
|
||||
deleteUrl={`/rest/forms/${this.props.entity.id}`}
|
||||
deleteUrl={`rest/forms/${this.props.entity.id}`}
|
||||
cudUrl={`/lists/forms/${this.props.entity.id}/edit`}
|
||||
listUrl="/lists/forms"
|
||||
deletingMsg={t('Deleting form ...')}
|
||||
|
@ -391,7 +391,7 @@ export default class CUD extends Component {
|
|||
<NamespaceSelect/>
|
||||
|
||||
<Fieldset label={t('Forms Preview')}>
|
||||
<TableSelect id="previewList" label={t('List To Preview On')} withHeader dropdown dataUrl='/rest/lists-table' columns={listsColumns} selectionLabelIndex={1} help={t('Select list whose fields will be used to preview the forms.')}/>
|
||||
<TableSelect id="previewList" label={t('List To Preview On')} withHeader dropdown dataUrl='rest/lists-table' columns={listsColumns} selectionLabelIndex={1} help={t('Select list whose fields will be used to preview the forms.')}/>
|
||||
|
||||
{ previewListId &&
|
||||
<AlignedRow>
|
||||
|
|
|
@ -7,6 +7,7 @@ import { withErrorHandling, withAsyncErrorHandler } from '../../lib/error-handli
|
|||
import { Table } from '../../lib/table';
|
||||
import axios from '../../lib/axios';
|
||||
import {Icon} from "../../lib/bootstrap-components";
|
||||
import {checkPermissions} from "../../lib/permissions";
|
||||
|
||||
@translate()
|
||||
@withPageHelpers
|
||||
|
@ -21,14 +22,12 @@ export default class List extends Component {
|
|||
|
||||
@withAsyncErrorHandler
|
||||
async fetchPermissions() {
|
||||
const request = {
|
||||
const result = await checkPermissions({
|
||||
createCustomForm: {
|
||||
entityTypeId: 'namespace',
|
||||
requiredOperations: ['createCustomForm']
|
||||
}
|
||||
};
|
||||
|
||||
const result = await axios.post('/rest/permissions-check', request);
|
||||
});
|
||||
|
||||
this.setState({
|
||||
createPermitted: result.data.createCustomForm
|
||||
|
@ -79,7 +78,7 @@ export default class List extends Component {
|
|||
|
||||
<Title>{t('Forms')}</Title>
|
||||
|
||||
<Table withHeader dataUrl="/rest/forms-table" columns={columns} />
|
||||
<Table withHeader dataUrl="rest/forms-table" columns={columns} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -25,14 +25,14 @@ function getMenus(t) {
|
|||
':listId([0-9]+)': {
|
||||
title: resolved => t('List "{{name}}"', {name: resolved.list.name}),
|
||||
resolve: {
|
||||
list: params => `/rest/lists/${params.listId}`
|
||||
list: params => `rest/lists/${params.listId}`
|
||||
},
|
||||
link: params => `/lists/${params.listId}/subscriptions`,
|
||||
navs: {
|
||||
subscriptions: {
|
||||
title: t('Subscribers'),
|
||||
resolve: {
|
||||
segments: params => `/rest/segments/${params.listId}`,
|
||||
segments: params => `rest/segments/${params.listId}`,
|
||||
},
|
||||
link: params => `/lists/${params.listId}/subscriptions`,
|
||||
visible: resolved => resolved.list.permissions.includes('viewSubscriptions'),
|
||||
|
@ -41,8 +41,8 @@ function getMenus(t) {
|
|||
':subscriptionId([0-9]+)': {
|
||||
title: resolved => resolved.subscription.email,
|
||||
resolve: {
|
||||
subscription: params => `/rest/subscriptions/${params.listId}/${params.subscriptionId}`,
|
||||
fieldsGrouped: params => `/rest/fields-grouped/${params.listId}`
|
||||
subscription: params => `rest/subscriptions/${params.listId}/${params.subscriptionId}`,
|
||||
fieldsGrouped: params => `rest/fields-grouped/${params.listId}`
|
||||
},
|
||||
link: params => `/lists/${params.listId}/subscriptions/${params.subscriptionId}/edit`,
|
||||
navs: {
|
||||
|
@ -56,7 +56,7 @@ function getMenus(t) {
|
|||
create: {
|
||||
title: t('Create'),
|
||||
resolve: {
|
||||
fieldsGrouped: params => `/rest/fields-grouped/${params.listId}`
|
||||
fieldsGrouped: params => `rest/fields-grouped/${params.listId}`
|
||||
},
|
||||
panelRender: props => <SubscriptionsCUD action="create" list={props.resolved.list} fieldsGrouped={props.resolved.fieldsGrouped} />
|
||||
}
|
||||
|
@ -76,8 +76,8 @@ function getMenus(t) {
|
|||
':fieldId([0-9]+)': {
|
||||
title: resolved => t('Field "{{name}}"', {name: resolved.field.name}),
|
||||
resolve: {
|
||||
field: params => `/rest/fields/${params.listId}/${params.fieldId}`,
|
||||
fields: params => `/rest/fields/${params.listId}`
|
||||
field: params => `rest/fields/${params.listId}/${params.fieldId}`,
|
||||
fields: params => `rest/fields/${params.listId}`
|
||||
},
|
||||
link: params => `/lists/${params.listId}/fields/${params.fieldId}/edit`,
|
||||
navs: {
|
||||
|
@ -91,7 +91,7 @@ function getMenus(t) {
|
|||
create: {
|
||||
title: t('Create'),
|
||||
resolve: {
|
||||
fields: params => `/rest/fields/${params.listId}`
|
||||
fields: params => `rest/fields/${params.listId}`
|
||||
},
|
||||
panelRender: props => <FieldsCUD action="create" list={props.resolved.list} fields={props.resolved.fields} />
|
||||
}
|
||||
|
@ -106,8 +106,8 @@ function getMenus(t) {
|
|||
':segmentId([0-9]+)': {
|
||||
title: resolved => t('Segment "{{name}}"', {name: resolved.segment.name}),
|
||||
resolve: {
|
||||
segment: params => `/rest/segments/${params.listId}/${params.segmentId}`,
|
||||
fields: params => `/rest/fields/${params.listId}`
|
||||
segment: params => `rest/segments/${params.listId}/${params.segmentId}`,
|
||||
fields: params => `rest/fields/${params.listId}`
|
||||
},
|
||||
link: params => `/lists/${params.listId}/segments/${params.segmentId}/edit`,
|
||||
navs: {
|
||||
|
@ -121,7 +121,7 @@ function getMenus(t) {
|
|||
create: {
|
||||
title: t('Create'),
|
||||
resolve: {
|
||||
fields: params => `/rest/fields/${params.listId}`
|
||||
fields: params => `rest/fields/${params.listId}`
|
||||
},
|
||||
panelRender: props => <SegmentsCUD action="create" list={props.resolved.list} fields={props.resolved.fields} />
|
||||
}
|
||||
|
@ -147,7 +147,7 @@ function getMenus(t) {
|
|||
':formsId([0-9]+)': {
|
||||
title: resolved => t('Custom Forms "{{name}}"', {name: resolved.forms.name}),
|
||||
resolve: {
|
||||
forms: params => `/rest/forms/${params.formsId}`
|
||||
forms: params => `rest/forms/${params.formsId}`
|
||||
},
|
||||
link: params => `/lists/forms/${params.formsId}/edit`,
|
||||
navs: {
|
||||
|
|
|
@ -151,10 +151,10 @@ export default class CUD extends Component {
|
|||
let sendMethod, url;
|
||||
if (this.props.entity) {
|
||||
sendMethod = FormSendMethod.PUT;
|
||||
url = `/rest/segments/${this.props.list.id}/${this.props.entity.id}`
|
||||
url = `rest/segments/${this.props.list.id}/${this.props.entity.id}`
|
||||
} else {
|
||||
sendMethod = FormSendMethod.POST;
|
||||
url = `/rest/segments/${this.props.list.id}`
|
||||
url = `rest/segments/${this.props.list.id}`
|
||||
}
|
||||
|
||||
try {
|
||||
|
@ -172,7 +172,7 @@ export default class CUD extends Component {
|
|||
|
||||
if (submitSuccessful) {
|
||||
if (stay) {
|
||||
await this.getFormValuesFromURL(`/rest/segments/${this.props.list.id}/${this.props.entity.id}`, data => {
|
||||
await this.getFormValuesFromURL(`rest/segments/${this.props.list.id}/${this.props.entity.id}`, data => {
|
||||
data.rootRuleType = data.settings.rootRule.type;
|
||||
data.selectedRule = null; // Validation errors of the selected rule are attached to this which makes sure we don't submit the segment if the opened rule has errors
|
||||
|
||||
|
@ -332,7 +332,7 @@ export default class CUD extends Component {
|
|||
<DeleteModalDialog
|
||||
stateOwner={this}
|
||||
visible={this.props.action === 'delete'}
|
||||
deleteUrl={`/rest/segments/${this.props.list.id}/${this.props.entity.id}`}
|
||||
deleteUrl={`rest/segments/${this.props.list.id}/${this.props.entity.id}`}
|
||||
cudUrl={`/lists/${this.props.list.id}/segments/${this.props.entity.id}/edit`}
|
||||
listUrl={`/lists/${this.props.list.id}/segments`}
|
||||
deletingMsg={t('Deleting segment ...')}
|
||||
|
|
|
@ -47,7 +47,7 @@ export default class List extends Component {
|
|||
|
||||
<Title>{t('Segment')}</Title>
|
||||
|
||||
<Table withHeader dataUrl={`/rest/segments-table/${this.props.list.id}`} columns={columns} />
|
||||
<Table withHeader dataUrl={`rest/segments-table/${this.props.list.id}`} columns={columns} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -36,7 +36,7 @@ export default class CUD extends Component {
|
|||
|
||||
this.initForm({
|
||||
serverValidation: {
|
||||
url: `/rest/subscriptions-validate/${this.props.list.id}`,
|
||||
url: `rest/subscriptions-validate/${this.props.list.id}`,
|
||||
changed: ['email'],
|
||||
extra: ['id']
|
||||
},
|
||||
|
@ -102,10 +102,10 @@ export default class CUD extends Component {
|
|||
let sendMethod, url;
|
||||
if (this.props.entity) {
|
||||
sendMethod = FormSendMethod.PUT;
|
||||
url = `/rest/subscriptions/${this.props.list.id}/${this.props.entity.id}`
|
||||
url = `rest/subscriptions/${this.props.list.id}/${this.props.entity.id}`
|
||||
} else {
|
||||
sendMethod = FormSendMethod.POST;
|
||||
url = `/rest/subscriptions/${this.props.list.id}`
|
||||
url = `rest/subscriptions/${this.props.list.id}`
|
||||
}
|
||||
|
||||
try {
|
||||
|
@ -171,7 +171,7 @@ export default class CUD extends Component {
|
|||
stateOwner={this}
|
||||
visible={this.props.action === 'delete'}
|
||||
actionMethod={HTTPMethod.DELETE}
|
||||
actionUrl={`/rest/subscriptions/${this.props.list.id}/${this.props.entity.id}`}
|
||||
actionUrl={`rest/subscriptions/${this.props.list.id}/${this.props.entity.id}`}
|
||||
backUrl={`/lists/${this.props.list.id}/subscriptions/${this.props.entity.id}/edit`}
|
||||
successUrl={`/lists/${this.props.list.id}/subscriptions`}
|
||||
actionInProgressMsg={t('Deleting subscription ...')}
|
||||
|
|
|
@ -63,19 +63,19 @@ export default class List extends Component {
|
|||
|
||||
@withAsyncErrorHandler
|
||||
async deleteSubscription(id) {
|
||||
await axios.delete(`/rest/subscriptions/${this.props.list.id}/${id}`);
|
||||
await axios.delete(getUrl(`rest/subscriptions/${this.props.list.id}/${id}`));
|
||||
this.blacklistTable.refresh();
|
||||
}
|
||||
|
||||
@withAsyncErrorHandler
|
||||
async unsubscribeSubscription(id) {
|
||||
await axios.post(`/rest/subscriptions-unsubscribe/${this.props.list.id}/${id}`);
|
||||
await axios.post(getUrl(`rest/subscriptions-unsubscribe/${this.props.list.id}/${id}`));
|
||||
this.blacklistTable.refresh();
|
||||
}
|
||||
|
||||
@withAsyncErrorHandler
|
||||
async blacklistSubscription(email) {
|
||||
await axios.post("/rest/blacklist", { email });
|
||||
await axios.post(getUrl('rest/blacklist'), { email });
|
||||
this.blacklistTable.refresh();
|
||||
}
|
||||
|
||||
|
@ -146,7 +146,7 @@ export default class List extends Component {
|
|||
];
|
||||
|
||||
|
||||
let dataUrl = '/rest/subscriptions-table/' + list.id;
|
||||
let dataUrl = 'rest/subscriptions-table/' + list.id;
|
||||
if (this.props.segmentId) {
|
||||
dataUrl += '/' + this.props.segmentId;
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ import interoperableErrors from '../../../shared/interoperable-errors';
|
|||
import {DeleteModalDialog} from "../lib/modals";
|
||||
import mailtrainConfig from 'mailtrainConfig';
|
||||
import {getGlobalNamespaceId} from "../../../shared/namespaces";
|
||||
import {getUrl} from "../lib/urls";
|
||||
|
||||
@translate()
|
||||
@withForm
|
||||
|
@ -57,7 +58,7 @@ export default class CUD extends Component {
|
|||
|
||||
@withAsyncErrorHandler
|
||||
async loadTreeData() {
|
||||
const response = await axios.get('/rest/namespaces-tree');
|
||||
const response = await axios.get(getUrl('rest/namespaces-tree'));
|
||||
const data = response.data;
|
||||
for (const root of data) {
|
||||
root.expanded = true;
|
||||
|
@ -112,10 +113,10 @@ export default class CUD extends Component {
|
|||
let sendMethod, url;
|
||||
if (this.props.entity) {
|
||||
sendMethod = FormSendMethod.PUT;
|
||||
url = `/rest/namespaces/${this.props.entity.id}`
|
||||
url = `rest/namespaces/${this.props.entity.id}`
|
||||
} else {
|
||||
sendMethod = FormSendMethod.POST;
|
||||
url = '/rest/namespaces'
|
||||
url = 'rest/namespaces'
|
||||
}
|
||||
|
||||
try {
|
||||
|
@ -182,7 +183,7 @@ export default class CUD extends Component {
|
|||
<DeleteModalDialog
|
||||
stateOwner={this}
|
||||
visible={this.props.action === 'delete'}
|
||||
deleteUrl={`/rest/namespaces/${this.props.entity.id}`}
|
||||
deleteUrl={`rest/namespaces/${this.props.entity.id}`}
|
||||
cudUrl={`/namespaces/${this.props.entity.id}/edit`}
|
||||
listUrl="/namespaces"
|
||||
deletingMsg={t('Deleting namespace ...')}
|
||||
|
|
|
@ -7,6 +7,7 @@ import { TreeTable } from '../lib/tree';
|
|||
import { withErrorHandling, withAsyncErrorHandler } from '../lib/error-handling';
|
||||
import axios from '../lib/axios';
|
||||
import {Icon} from "../lib/bootstrap-components";
|
||||
import {checkPermissions} from "../lib/permissions";
|
||||
|
||||
@translate()
|
||||
@withErrorHandling
|
||||
|
@ -21,14 +22,12 @@ export default class List extends Component {
|
|||
|
||||
@withAsyncErrorHandler
|
||||
async fetchPermissions() {
|
||||
const request = {
|
||||
const result = await checkPermissions({
|
||||
createNamespace: {
|
||||
entityTypeId: 'namespace',
|
||||
requiredOperations: ['createNamespace']
|
||||
}
|
||||
};
|
||||
|
||||
const result = await axios.post('/rest/permissions-check', request);
|
||||
});
|
||||
|
||||
this.setState({
|
||||
createPermitted: result.data.createNamespace
|
||||
|
@ -72,7 +71,7 @@ export default class List extends Component {
|
|||
|
||||
<Title>{t('Namespaces')}</Title>
|
||||
|
||||
<TreeTable withHeader withDescription dataUrl="/rest/namespaces-tree" actions={actions} />
|
||||
<TreeTable withHeader withDescription dataUrl="rest/namespaces-tree" actions={actions} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@ function getMenus(t) {
|
|||
':namespaceId([0-9]+)': {
|
||||
title: resolved => t('Namespace "{{name}}"', {name: resolved.namespace.name}),
|
||||
resolve: {
|
||||
namespace: params => `/rest/namespaces/${params.namespaceId}`
|
||||
namespace: params => `rest/namespaces/${params.namespaceId}`
|
||||
},
|
||||
link: params => `/namespaces/${params.namespaceId}/edit`,
|
||||
navs: {
|
||||
|
|
|
@ -14,6 +14,7 @@ import moment from 'moment';
|
|||
import { validateNamespace, NamespaceSelect } from '../lib/namespace';
|
||||
import {DeleteModalDialog} from "../lib/modals";
|
||||
import mailtrainConfig from 'mailtrainConfig';
|
||||
import {getUrl} from "../lib/urls";
|
||||
|
||||
@translate()
|
||||
@withForm
|
||||
|
@ -40,7 +41,7 @@ export default class CUD extends Component {
|
|||
|
||||
@withAsyncErrorHandler
|
||||
async fetchUserFields(reportTemplateId) {
|
||||
const result = await axios.get(`/rest/report-template-user-fields/${reportTemplateId}`);
|
||||
const result = await axios.get(getUrl(`rest/report-template-user-fields/${reportTemplateId}`));
|
||||
this.updateFormValue('user_fields', result.data);
|
||||
}
|
||||
|
||||
|
@ -128,10 +129,10 @@ export default class CUD extends Component {
|
|||
let sendMethod, url;
|
||||
if (this.props.entity) {
|
||||
sendMethod = FormSendMethod.PUT;
|
||||
url = `/rest/reports/${this.props.entity.id}`
|
||||
url = `rest/reports/${this.props.entity.id}`
|
||||
} else {
|
||||
sendMethod = FormSendMethod.POST;
|
||||
url = '/rest/reports'
|
||||
url = 'rest/reports'
|
||||
}
|
||||
|
||||
this.disableForm();
|
||||
|
@ -191,7 +192,7 @@ export default class CUD extends Component {
|
|||
if (userFieldsSpec) {
|
||||
for (const spec of userFieldsSpec) {
|
||||
if (spec.type === 'campaign') {
|
||||
addUserFieldTableSelect(spec, '/rest/campaigns-table', 1,[
|
||||
addUserFieldTableSelect(spec, 'rest/campaigns-table', 1,[
|
||||
{data: 0, title: "#"},
|
||||
{data: 1, title: t('Name')},
|
||||
{data: 2, title: t('Description')},
|
||||
|
@ -199,7 +200,7 @@ export default class CUD extends Component {
|
|||
{data: 4, title: t('Created'), render: data => moment(data).fromNow()}
|
||||
]);
|
||||
} else if (spec.type === 'list') {
|
||||
addUserFieldTableSelect(spec, '/rest/lists-table', 1,[
|
||||
addUserFieldTableSelect(spec, 'rest/lists-table', 1,[
|
||||
{data: 0, title: "#"},
|
||||
{data: 1, title: t('Name')},
|
||||
{data: 2, title: t('ID')},
|
||||
|
@ -218,7 +219,7 @@ export default class CUD extends Component {
|
|||
<DeleteModalDialog
|
||||
stateOwner={this}
|
||||
visible={this.props.action === 'delete'}
|
||||
deleteUrl={`/rest/reports/${this.props.entity.id}`}
|
||||
deleteUrl={`rest/reports/${this.props.entity.id}`}
|
||||
cudUrl={`/reports/${this.props.entity.id}/edit`}
|
||||
listUrl="/reports"
|
||||
deletingMsg={t('Deleting report ...')}
|
||||
|
@ -231,7 +232,7 @@ export default class CUD extends Component {
|
|||
<InputField id="name" label={t('Name')}/>
|
||||
<TextArea id="description" label={t('Description')}/>
|
||||
|
||||
<TableSelect id="report_template" label={t('Report Template')} withHeader dropdown dataUrl="/rest/report-templates-table" columns={reportTemplateColumns} selectionLabelIndex={1}/>
|
||||
<TableSelect id="report_template" label={t('Report Template')} withHeader dropdown dataUrl="rest/report-templates-table" columns={reportTemplateColumns} selectionLabelIndex={1}/>
|
||||
|
||||
<NamespaceSelect/>
|
||||
|
||||
|
|
|
@ -9,6 +9,8 @@ import moment from 'moment';
|
|||
import axios from '../lib/axios';
|
||||
import { ReportState } from '../../../shared/reports';
|
||||
import {Icon} from "../lib/bootstrap-components";
|
||||
import {checkPermissions} from "../lib/permissions";
|
||||
import {getUrl} from "../lib/urls";
|
||||
|
||||
@translate()
|
||||
@withErrorHandling
|
||||
|
@ -23,7 +25,7 @@ export default class List extends Component {
|
|||
|
||||
@withAsyncErrorHandler
|
||||
async fetchPermissions() {
|
||||
const request = {
|
||||
const result = await checkPermissions({
|
||||
createReport: {
|
||||
entityTypeId: 'namespace',
|
||||
requiredOperations: ['createReport']
|
||||
|
@ -39,10 +41,8 @@ export default class List extends Component {
|
|||
viewReportTemplate: {
|
||||
entityTypeId: 'reportTemplate',
|
||||
requiredOperations: ['view']
|
||||
},
|
||||
};
|
||||
|
||||
const result = await axios.post('/rest/permissions-check', request);
|
||||
}
|
||||
});
|
||||
|
||||
this.setState({
|
||||
createPermitted: result.data.createReport && result.data.executeReportTemplate,
|
||||
|
@ -56,13 +56,13 @@ export default class List extends Component {
|
|||
|
||||
@withAsyncErrorHandler
|
||||
async stop(table, id) {
|
||||
await axios.post(`/rest/report-stop/${id}`);
|
||||
await axios.post(getUrl(`rest/report-stop/${id}`));
|
||||
table.refresh();
|
||||
}
|
||||
|
||||
@withAsyncErrorHandler
|
||||
async start(table, id) {
|
||||
await axios.post(`/rest/report-start/${id}`);
|
||||
await axios.post(getUrl(`rest/report-start/${id}`));
|
||||
table.refresh();
|
||||
}
|
||||
|
||||
|
@ -177,7 +177,7 @@ export default class List extends Component {
|
|||
|
||||
<Title>{t('Reports')}</Title>
|
||||
|
||||
<Table withHeader dataUrl="/rest/reports-table" columns={columns} />
|
||||
<Table withHeader dataUrl="rest/reports-table" columns={columns} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import { translate } from 'react-i18next';
|
|||
import { requiresAuthenticatedUser, withPageHelpers, Title } from '../lib/page'
|
||||
import { withErrorHandling, withAsyncErrorHandler } from '../lib/error-handling';
|
||||
import axios from '../lib/axios';
|
||||
import {getUrl} from "../lib/urls";
|
||||
|
||||
@translate()
|
||||
@withPageHelpers
|
||||
|
@ -22,8 +23,8 @@ export default class Output extends Component {
|
|||
@withAsyncErrorHandler
|
||||
async loadOutput() {
|
||||
const id = parseInt(this.props.match.params.id);
|
||||
const outputRespPromise = axios.get(`/rest/report-output/${id}`);
|
||||
const reportRespPromise = axios.get(`/rest/reports/${id}`);
|
||||
const outputRespPromise = axios.get(getUrl(`rest/report-output/${id}`));
|
||||
const reportRespPromise = axios.get(getUrl(`rest/reports/${id}`));
|
||||
const [outputResp, reportResp] = await Promise.all([outputRespPromise, reportRespPromise]);
|
||||
|
||||
this.setState({
|
||||
|
|
|
@ -6,6 +6,7 @@ import { requiresAuthenticatedUser, withPageHelpers, Title } from '../lib/page'
|
|||
import { withErrorHandling, withAsyncErrorHandler } from '../lib/error-handling';
|
||||
import axios from '../lib/axios';
|
||||
import { ReportState } from '../../../shared/reports';
|
||||
import {getUrl} from "../lib/urls";
|
||||
|
||||
@translate()
|
||||
@withPageHelpers
|
||||
|
@ -23,8 +24,8 @@ export default class View extends Component {
|
|||
@withAsyncErrorHandler
|
||||
async loadContent() {
|
||||
const id = parseInt(this.props.match.params.id);
|
||||
const contentRespPromise = axios.get(`/rest/report-content/${id}`);
|
||||
const reportRespPromise = axios.get(`/rest/reports/${id}`);
|
||||
const contentRespPromise = axios.get(getUrl(`rest/report-content/${id}`));
|
||||
const reportRespPromise = axios.get(getUrl(`rest/reports/${id}`));
|
||||
const [contentResp, reportResp] = await Promise.all([contentRespPromise, reportRespPromise]);
|
||||
|
||||
this.setState({
|
||||
|
|
|
@ -22,7 +22,7 @@ function getMenus(t) {
|
|||
':reportId([0-9]+)': {
|
||||
title: resolved => t('Report "{{name}}"', {name: resolved.report.name}),
|
||||
resolve: {
|
||||
report: params => `/rest/reports/${params.reportId}`
|
||||
report: params => `rest/reports/${params.reportId}`
|
||||
},
|
||||
link: params => `/reports/${params.reportId}/edit`,
|
||||
navs: {
|
||||
|
@ -69,7 +69,7 @@ function getMenus(t) {
|
|||
':templateId([0-9]+)': {
|
||||
title: resolved => t('Template "{{name}}"', {name: resolved.template.name}),
|
||||
resolve: {
|
||||
template: params => `/rest/report-templates/${params.templateId}`
|
||||
template: params => `rest/report-templates/${params.templateId}`
|
||||
},
|
||||
link: params => `/reports/templates/${params.templateId}/edit`,
|
||||
navs: {
|
||||
|
|
|
@ -262,10 +262,10 @@ export default class CUD extends Component {
|
|||
let sendMethod, url;
|
||||
if (this.props.entity) {
|
||||
sendMethod = FormSendMethod.PUT;
|
||||
url = `/rest/report-templates/${this.props.entity.id}`
|
||||
url = `rest/report-templates/${this.props.entity.id}`
|
||||
} else {
|
||||
sendMethod = FormSendMethod.POST;
|
||||
url = '/rest/report-templates'
|
||||
url = 'rest/report-templates'
|
||||
}
|
||||
|
||||
this.disableForm();
|
||||
|
@ -275,7 +275,7 @@ export default class CUD extends Component {
|
|||
|
||||
if (submitSuccessful) {
|
||||
if (stay) {
|
||||
await this.getFormValuesFromURL(`/rest/report-templates/${this.props.entity.id}`);
|
||||
await this.getFormValuesFromURL(`rest/report-templates/${this.props.entity.id}`);
|
||||
this.enableForm();
|
||||
this.setFormStatusMessage('success', t('Report template saved'));
|
||||
} else {
|
||||
|
@ -298,7 +298,7 @@ export default class CUD extends Component {
|
|||
<DeleteModalDialog
|
||||
stateOwner={this}
|
||||
visible={this.props.action === 'delete'}
|
||||
deleteUrl={`/rest/reports/templates/${this.props.entity.id}`}
|
||||
deleteUrl={`rest/reports/templates/${this.props.entity.id}`}
|
||||
cudUrl={`/reports/templates/${this.props.entity.id}/edit`}
|
||||
listUrl="/reports/templates"
|
||||
deletingMsg={t('Deleting report template ...')}
|
||||
|
|
|
@ -9,6 +9,7 @@ import { Table } from '../../lib/table';
|
|||
import axios from '../../lib/axios';
|
||||
import moment from 'moment';
|
||||
import mailtrainConfig from 'mailtrainConfig';
|
||||
import {checkPermissions} from "../../lib/permissions";
|
||||
|
||||
@translate()
|
||||
@withPageHelpers
|
||||
|
@ -23,14 +24,12 @@ export default class List extends Component {
|
|||
|
||||
@withAsyncErrorHandler
|
||||
async fetchPermissions() {
|
||||
const request = {
|
||||
const result = await checkPermissions({
|
||||
createReportTemplate: {
|
||||
entityTypeId: 'namespace',
|
||||
requiredOperations: ['createReportTemplate']
|
||||
}
|
||||
};
|
||||
|
||||
const result = await axios.post('/rest/permissions-check', request);
|
||||
});
|
||||
|
||||
this.setState({
|
||||
createPermitted: result.data.createReportTemplate && mailtrainConfig.globalPermissions.includes('createJavascriptWithROAccess')
|
||||
|
@ -88,7 +87,7 @@ export default class List extends Component {
|
|||
|
||||
<Title>{t('Report Templates')}</Title>
|
||||
|
||||
<Table withHeader dataUrl="/rest/report-templates-table" columns={columns} />
|
||||
<Table withHeader dataUrl="rest/report-templates-table" columns={columns} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
'use strict';
|
||||
|
||||
import './lib/public-path';
|
||||
|
||||
import React, {Component} from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import {
|
||||
|
@ -128,7 +130,7 @@ class Root extends Component {
|
|||
}
|
||||
|
||||
async logout() {
|
||||
await axios.post('/rest/logout');
|
||||
await axios.post(getUrl('rest/logout'));
|
||||
window.location = getUrl();
|
||||
}
|
||||
|
||||
|
|
|
@ -132,10 +132,10 @@ export default class CUD extends Component {
|
|||
let sendMethod, url;
|
||||
if (this.props.entity) {
|
||||
sendMethod = FormSendMethod.PUT;
|
||||
url = `/rest/send-configurations/${this.props.entity.id}`
|
||||
url = `rest/send-configurations/${this.props.entity.id}`
|
||||
} else {
|
||||
sendMethod = FormSendMethod.POST;
|
||||
url = '/rest/send-configurations'
|
||||
url = 'rest/send-configurations'
|
||||
}
|
||||
|
||||
this.disableForm();
|
||||
|
@ -175,7 +175,7 @@ export default class CUD extends Component {
|
|||
<DeleteModalDialog
|
||||
stateOwner={this}
|
||||
visible={this.props.action === 'delete'}
|
||||
deleteUrl={`/rest/send-configurations/${this.props.entity.id}`}
|
||||
deleteUrl={`rest/send-configurations/${this.props.entity.id}`}
|
||||
cudUrl={`/send-configurations/${this.props.entity.id}/edit`}
|
||||
listUrl="/send-configurations"
|
||||
deletingMsg={t('Deleting send configuration ...')}
|
||||
|
|
|
@ -18,6 +18,7 @@ import {Table} from '../lib/table';
|
|||
import axios from '../lib/axios';
|
||||
import moment from 'moment';
|
||||
import {getMailerTypes} from './helpers';
|
||||
import {checkPermissions} from "../lib/permissions";
|
||||
|
||||
|
||||
@translate()
|
||||
|
@ -35,14 +36,12 @@ export default class List extends Component {
|
|||
|
||||
@withAsyncErrorHandler
|
||||
async fetchPermissions() {
|
||||
const request = {
|
||||
const result = await checkPermissions({
|
||||
createSendConfiguration: {
|
||||
entityTypeId: 'namespace',
|
||||
requiredOperations: ['createSendConfiguration']
|
||||
}
|
||||
};
|
||||
|
||||
const result = await axios.post('/rest/permissions-check', request);
|
||||
});
|
||||
|
||||
this.setState({
|
||||
createPermitted: result.data.createSendConfiguration
|
||||
|
@ -96,7 +95,7 @@ export default class List extends Component {
|
|||
|
||||
<Title>{t('Send Configurations')}</Title>
|
||||
|
||||
<Table withHeader dataUrl="/rest/send-configurations-table" columns={columns} />
|
||||
<Table withHeader dataUrl="rest/send-configurations-table" columns={columns} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -49,7 +49,6 @@ export function getMailerTypes(t) {
|
|||
} else {
|
||||
state.setIn([field, 'error'], null);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function getInitCommon() {
|
||||
|
|
|
@ -17,7 +17,7 @@ function getMenus(t) {
|
|||
':sendConfigurationId([0-9]+)': {
|
||||
title: resolved => t('Template "{{name}}"', {name: resolved.sendConfiguration.name}),
|
||||
resolve: {
|
||||
sendConfiguration: params => `/rest/send-configurations/${params.sendConfigurationId}`
|
||||
sendConfiguration: params => `rest/send-configurations/${params.sendConfigurationId}`
|
||||
},
|
||||
link: params => `/send-configurations/${params.sendConfigurationId}/edit`,
|
||||
navs: {
|
||||
|
|
|
@ -55,10 +55,10 @@ export default class Update extends Component {
|
|||
this.disableForm();
|
||||
this.setFormStatusMessage('info', t('Saving ...'));
|
||||
|
||||
const submitSuccessful = await this.validateAndSendFormValuesToURL(FormSendMethod.PUT, '/rest/settings');
|
||||
const submitSuccessful = await this.validateAndSendFormValuesToURL(FormSendMethod.PUT, 'rest/settings');
|
||||
|
||||
if (submitSuccessful) {
|
||||
await this.getFormValuesFromURL('/rest/settings');
|
||||
await this.getFormValuesFromURL('rest/settings');
|
||||
this.enableForm();
|
||||
this.setFormStatusMessage('success', t('Global settings saved'));
|
||||
} else {
|
||||
|
|
|
@ -9,7 +9,7 @@ function getMenus(t) {
|
|||
title: t('Global Settings'),
|
||||
link: '/settings',
|
||||
resolve: {
|
||||
configItems: params => `/rest/settings`
|
||||
configItems: params => `rest/settings`
|
||||
},
|
||||
panelRender: props => <Update entity={props.resolved.configItems} />
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ import {
|
|||
import { Table } from '../lib/table';
|
||||
import axios from '../lib/axios';
|
||||
import mailtrainConfig from 'mailtrainConfig';
|
||||
import {getUrl} from "../lib/urls";
|
||||
|
||||
@translate()
|
||||
@withForm
|
||||
|
@ -38,7 +39,7 @@ export default class Share extends Component {
|
|||
userId
|
||||
};
|
||||
|
||||
await axios.put('/rest/shares', data);
|
||||
await axios.put(getUrl('rest/shares', data));
|
||||
this.sharesTable.refresh();
|
||||
this.usersTableSelect.refresh();
|
||||
}
|
||||
|
@ -78,7 +79,7 @@ export default class Share extends Component {
|
|||
this.disableForm();
|
||||
this.setFormStatusMessage('info', t('Saving ...'));
|
||||
|
||||
const submitSuccessful = await this.validateAndSendFormValuesToURL(FormSendMethod.PUT, '/rest/shares');
|
||||
const submitSuccessful = await this.validateAndSendFormValuesToURL(FormSendMethod.PUT, 'rest/shares');
|
||||
|
||||
if (submitSuccessful) {
|
||||
this.hideFormValidation();
|
||||
|
@ -145,8 +146,8 @@ export default class Share extends Component {
|
|||
|
||||
<h3 className="legend">{t('Add User')}</h3>
|
||||
<Form stateOwner={this} onSubmitAsync={::this.submitHandler}>
|
||||
<TableSelect ref={node => this.usersTableSelect = node} id="userId" label={t('User')} withHeader dropdown dataUrl={`/rest/shares-unassigned-users-table/${this.props.entityTypeId}/${this.props.entity.id}`} columns={usersColumns} selectionLabelIndex={usersLabelIndex}/>
|
||||
<TableSelect id="role" label={t('Role')} withHeader dropdown dataUrl={`/rest/shares-roles-table/${this.props.entityTypeId}`} columns={rolesColumns} selectionLabelIndex={1}/>
|
||||
<TableSelect ref={node => this.usersTableSelect = node} id="userId" label={t('User')} withHeader dropdown dataUrl={`rest/shares-unassigned-users-table/${this.props.entityTypeId}/${this.props.entity.id}`} columns={usersColumns} selectionLabelIndex={usersLabelIndex}/>
|
||||
<TableSelect id="role" label={t('Role')} withHeader dropdown dataUrl={`rest/shares-roles-table/${this.props.entityTypeId}`} columns={rolesColumns} selectionLabelIndex={1}/>
|
||||
|
||||
<ButtonRow>
|
||||
<Button type="submit" className="btn-primary" icon="ok" label={t('Share')}/>
|
||||
|
@ -156,7 +157,7 @@ export default class Share extends Component {
|
|||
<hr/>
|
||||
<h3 className="legend">{t('Existing Users')}</h3>
|
||||
|
||||
<Table ref={node => this.sharesTable = node} withHeader dataUrl={`/rest/shares-table-by-entity/${this.props.entityTypeId}/${this.props.entity.id}`} columns={sharesColumns} />
|
||||
<Table ref={node => this.sharesTable = node} withHeader dataUrl={`rest/shares-table-by-entity/${this.props.entityTypeId}/${this.props.entity.id}`} columns={sharesColumns} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import { Table } from '../lib/table';
|
|||
import axios from '../lib/axios';
|
||||
import mailtrainConfig from 'mailtrainConfig';
|
||||
import {Icon} from "../lib/bootstrap-components";
|
||||
import {getUrl} from "../lib/urls";
|
||||
|
||||
@translate()
|
||||
@withPageHelpers
|
||||
|
@ -33,7 +34,7 @@ export default class UserShares extends Component {
|
|||
userId: this.props.user.id
|
||||
};
|
||||
|
||||
await axios.put('/rest/shares', data);
|
||||
await axios.put(getUrl('rest/shares', data));
|
||||
for (const key in this.sharesTables) {
|
||||
this.sharesTables[key].refresh();
|
||||
}
|
||||
|
@ -70,7 +71,7 @@ export default class UserShares extends Component {
|
|||
return (
|
||||
<div>
|
||||
<h3>{title}</h3>
|
||||
<Table ref={node => this.sharesTables[entityTypeId] = node} withHeader dataUrl={`/rest/shares-table-by-user/${entityTypeId}/${this.props.user.id}`} columns={columns} />
|
||||
<Table ref={node => this.sharesTables[entityTypeId] = node} withHeader dataUrl={`rest/shares-table-by-user/${entityTypeId}/${this.props.user.id}`} columns={columns} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,30 +1,42 @@
|
|||
'use strict';
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import React, {Component} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { translate, Trans } from 'react-i18next';
|
||||
import {requiresAuthenticatedUser, withPageHelpers, Title, NavButton} from '../lib/page'
|
||||
import {
|
||||
withForm,
|
||||
Trans,
|
||||
translate
|
||||
} from 'react-i18next';
|
||||
import {
|
||||
NavButton,
|
||||
requiresAuthenticatedUser,
|
||||
Title,
|
||||
withPageHelpers
|
||||
} from '../lib/page'
|
||||
import {
|
||||
ACEEditor,
|
||||
AlignedRow,
|
||||
Button,
|
||||
ButtonRow,
|
||||
Dropdown,
|
||||
Form,
|
||||
FormSendMethod,
|
||||
InputField,
|
||||
StaticField,
|
||||
TextArea,
|
||||
Dropdown,
|
||||
ACEEditor,
|
||||
ButtonRow,
|
||||
Button,
|
||||
AlignedRow,
|
||||
StaticField
|
||||
withForm
|
||||
} from '../lib/form';
|
||||
import { withErrorHandling, withAsyncErrorHandler } from '../lib/error-handling';
|
||||
import { validateNamespace, NamespaceSelect } from '../lib/namespace';
|
||||
import {withErrorHandling} from '../lib/error-handling';
|
||||
import {
|
||||
NamespaceSelect,
|
||||
validateNamespace
|
||||
} from '../lib/namespace';
|
||||
import {DeleteModalDialog} from "../lib/modals";
|
||||
import mailtrainConfig from 'mailtrainConfig';
|
||||
import { getTemplateTypes } from './helpers';
|
||||
import {getTemplateTypes} from './helpers';
|
||||
import {ActionLink} from "../lib/bootstrap-components";
|
||||
import axios from '../lib/axios';
|
||||
import styles from "../lib/styles.scss";
|
||||
import {getUrl} from "../lib/urls";
|
||||
|
||||
|
||||
@translate()
|
||||
|
@ -43,7 +55,11 @@ export default class CUD extends Component {
|
|||
elementInFullscreen: false
|
||||
};
|
||||
|
||||
this.initForm();
|
||||
this.initForm({
|
||||
onChangeBeforeValidation: {
|
||||
type: ::this.onTypeChanged
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
|
@ -52,9 +68,17 @@ export default class CUD extends Component {
|
|||
entity: PropTypes.object
|
||||
}
|
||||
|
||||
onTypeChanged(mutState, key, oldType, type) {
|
||||
if (type) {
|
||||
this.templateTypes[type].afterTypeChange(mutState);
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (this.props.entity) {
|
||||
this.getFormValuesFromEntity(this.props.entity);
|
||||
this.getFormValuesFromEntity(this.props.entity, data => {
|
||||
this.templateTypes[data.type].afterLoad(data);
|
||||
});
|
||||
} else {
|
||||
this.populateFormValues({
|
||||
name: '',
|
||||
|
@ -63,7 +87,8 @@ export default class CUD extends Component {
|
|||
type: mailtrainConfig.editors[0],
|
||||
text: '',
|
||||
html: '',
|
||||
data: {}
|
||||
data: {},
|
||||
...this.templateTypes[mailtrainConfig.editors[0]].initData()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -77,13 +102,18 @@ export default class CUD extends Component {
|
|||
state.setIn(['name', 'error'], null);
|
||||
}
|
||||
|
||||
if (!state.getIn(['type', 'value'])) {
|
||||
const typeKey = state.getIn(['type', 'value']);
|
||||
if (!typeKey) {
|
||||
state.setIn(['type', 'error'], t('Type must be selected'));
|
||||
} else {
|
||||
state.setIn(['type', 'error'], null);
|
||||
}
|
||||
|
||||
validateNamespace(t, state);
|
||||
|
||||
if (typeKey) {
|
||||
this.templateTypes[typeKey].validate(state);
|
||||
}
|
||||
}
|
||||
|
||||
async submitHandler() {
|
||||
|
@ -91,22 +121,23 @@ export default class CUD extends Component {
|
|||
|
||||
if (this.props.entity) {
|
||||
const typeKey = this.getFormValue('type');
|
||||
await this.templateTypes[typeKey].htmlEditorBeforeSave(this);
|
||||
await this.templateTypes[typeKey].exportHTMLEditorData(this);
|
||||
}
|
||||
|
||||
let sendMethod, url;
|
||||
if (this.props.entity) {
|
||||
sendMethod = FormSendMethod.PUT;
|
||||
url = `/rest/templates/${this.props.entity.id}`
|
||||
url = `rest/templates/${this.props.entity.id}`
|
||||
} else {
|
||||
sendMethod = FormSendMethod.POST;
|
||||
url = '/rest/templates'
|
||||
url = 'rest/templates'
|
||||
}
|
||||
|
||||
this.disableForm();
|
||||
this.setFormStatusMessage('info', t('Saving ...'));
|
||||
|
||||
const submitResponse = await this.validateAndSendFormValuesToURL(sendMethod, url, data => {
|
||||
this.templateTypes[data.type].beforeSave(data);
|
||||
});
|
||||
|
||||
if (submitResponse) {
|
||||
|
@ -123,7 +154,7 @@ export default class CUD extends Component {
|
|||
|
||||
async extractPlainText() {
|
||||
const typeKey = this.getFormValue('type');
|
||||
await this.templateTypes[typeKey].htmlEditorBeforeSave(this);
|
||||
await this.templateTypes[typeKey].exportHTMLEditorData(this);
|
||||
|
||||
const html = this.getFormValue('html');
|
||||
if (!html) {
|
||||
|
@ -137,7 +168,7 @@ export default class CUD extends Component {
|
|||
|
||||
this.disableForm();
|
||||
|
||||
const response = await axios.post('/rest/html-to-text', { html });
|
||||
const response = await axios.post(getUrl('rest/html-to-text', { html }));
|
||||
|
||||
this.updateFormValue('text', response.data.text);
|
||||
|
||||
|
@ -258,6 +289,14 @@ export default class CUD extends Component {
|
|||
</div>
|
||||
}
|
||||
|
||||
let typeForm = null;
|
||||
if (typeKey) {
|
||||
typeForm = <div>
|
||||
{this.templateTypes[typeKey].getTypeForm(this, isEdit)}
|
||||
</div>;
|
||||
}
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<div className={this.state.elementInFullscreen ? styles.withElementInFullscreen : ''}>
|
||||
|
@ -265,7 +304,7 @@ export default class CUD extends Component {
|
|||
<DeleteModalDialog
|
||||
stateOwner={this}
|
||||
visible={this.props.action === 'delete'}
|
||||
deleteUrl={`/rest/templates/${this.props.entity.id}`}
|
||||
deleteUrl={`rest/templates/${this.props.entity.id}`}
|
||||
cudUrl={`/templates/${this.props.entity.id}/edit`}
|
||||
listUrl="/templates"
|
||||
deletingMsg={t('Deleting template ...')}
|
||||
|
@ -287,6 +326,7 @@ export default class CUD extends Component {
|
|||
<Dropdown id="type" label={t('Type')} options={typeOptions}/>
|
||||
}
|
||||
|
||||
{typeForm}
|
||||
|
||||
<NamespaceSelect/>
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ import { Table } from '../lib/table';
|
|||
import axios from '../lib/axios';
|
||||
import moment from 'moment';
|
||||
import { getTemplateTypes } from './helpers';
|
||||
import {checkPermissions} from "../lib/permissions";
|
||||
|
||||
@translate()
|
||||
@withPageHelpers
|
||||
|
@ -25,7 +26,7 @@ export default class List extends Component {
|
|||
|
||||
@withAsyncErrorHandler
|
||||
async fetchPermissions() {
|
||||
const request = {
|
||||
const result = await checkPermissions({
|
||||
createTemplate: {
|
||||
entityTypeId: 'namespace',
|
||||
requiredOperations: ['createTemplate']
|
||||
|
@ -38,9 +39,7 @@ export default class List extends Component {
|
|||
entityTypeId: 'mosaicoTemplate',
|
||||
requiredOperations: ['view']
|
||||
}
|
||||
};
|
||||
|
||||
const result = await axios.post('/rest/permissions-check', request);
|
||||
});
|
||||
|
||||
this.setState({
|
||||
createPermitted: result.data.createTemplate,
|
||||
|
@ -105,7 +104,7 @@ export default class List extends Component {
|
|||
|
||||
<Title>{t('Templates')}</Title>
|
||||
|
||||
<Table withHeader dataUrl="/rest/templates-table" columns={columns} />
|
||||
<Table withHeader dataUrl="rest/templates-table" columns={columns} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -4,46 +4,161 @@ import React from "react";
|
|||
import {
|
||||
ACEEditor,
|
||||
AlignedRow,
|
||||
CKEditor
|
||||
CKEditor,
|
||||
TableSelect
|
||||
} from "../lib/form";
|
||||
import 'brace/mode/text';
|
||||
import 'brace/mode/html'
|
||||
import 'brace/mode/html';
|
||||
|
||||
import {MosaicoEditor, ResourceType} from "../lib/mosaico";
|
||||
import {
|
||||
MosaicoEditor,
|
||||
ResourceType
|
||||
} from "../lib/mosaico";
|
||||
|
||||
import {getTemplateTypes as getMosaicoTemplateTypes} from './mosaico/helpers';
|
||||
|
||||
|
||||
export function getTemplateTypes(t) {
|
||||
|
||||
const templateTypes = {};
|
||||
|
||||
function initFieldsIfMissing(mutState, templateType) {
|
||||
const initVals = templateTypes[templateType].initData();
|
||||
|
||||
for (const key in initVals) {
|
||||
if (!mutState.hasIn([key])) {
|
||||
mutState.setIn([key, 'value'], initVals[key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function clearBeforeSave(data) {
|
||||
for (const templateKey in templateTypes) {
|
||||
const initVals = templateTypes[templateKey].initData();
|
||||
for (const fieldKey in initVals) {
|
||||
delete data[fieldKey];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const mosaicoTemplateTypes = getMosaicoTemplateTypes(t);
|
||||
const mosaicoTemplatesColumns = [
|
||||
{ data: 1, title: t('Name') },
|
||||
{ data: 2, title: t('Description') },
|
||||
{ data: 3, title: t('Type'), render: data => mosaicoTemplateTypes[data].typeName },
|
||||
{ data: 5, title: t('Namespace') },
|
||||
];
|
||||
|
||||
templateTypes.mosaico = {
|
||||
typeName: t('Mosaico'),
|
||||
getHTMLEditor: owner => <AlignedRow label={t('Template content (HTML)')}><MosaicoEditor ref={node => owner.editorNode = node} entity={owner.props.entity} entityTypeId={ResourceType.TEMPLATE} title={t('Mosaico Template Designer')} onFullscreenAsync={::owner.setElementInFullscreen}/></AlignedRow>,
|
||||
htmlEditorBeforeSave: async owner => {
|
||||
getTypeForm: (owner, isEdit) =>
|
||||
<TableSelect id="mosaicoTemplate" label={t('Mosaico template')} withHeader dropdown dataUrl='rest/mosaico-templates-table' columns={mosaicoTemplatesColumns} selectionLabelIndex={1} disabled={isEdit} />,
|
||||
getHTMLEditor: owner =>
|
||||
<AlignedRow label={t('Template content (HTML)')}>
|
||||
<MosaicoEditor
|
||||
ref={node => owner.editorNode = node}
|
||||
entity={owner.props.entity}
|
||||
initialModel={owner.getFormValue('mosaicoData').model}
|
||||
initialMetadata={owner.getFormValue('mosaicoData').metadata}
|
||||
templateId={owner.getFormValue('mosaicoTemplate')}
|
||||
entityTypeId={ResourceType.TEMPLATE}
|
||||
title={t('Mosaico Template Designer')}
|
||||
onFullscreenAsync={::owner.setElementInFullscreen}/>
|
||||
</AlignedRow>,
|
||||
exportHTMLEditorData: async owner => {
|
||||
const {html, metadata, model} = await owner.editorNode.exportState();
|
||||
owner.updateFormValue('html', html);
|
||||
owner.updateFormValue('data', {metadata, model});
|
||||
owner.updateFormValue('mosaicoData', {
|
||||
metadata,
|
||||
model
|
||||
});
|
||||
},
|
||||
initData: () => ({
|
||||
mosaicoTemplate: '',
|
||||
mosaicoData: {}
|
||||
}),
|
||||
afterLoad: data => {
|
||||
data.mosaicoTemplate = data.data.mosaicoTemplate;
|
||||
data.html = data.data.html;
|
||||
data.mosaicoData = {
|
||||
metadata: data.data.metadata,
|
||||
model: data.data.model
|
||||
};
|
||||
},
|
||||
beforeSave: data => {
|
||||
data.data = {
|
||||
mosaicoTemplate: data.mosaicoTemplate,
|
||||
metadata: data.mosaicoData.metadata,
|
||||
model: data.mosaicoData.model
|
||||
};
|
||||
clearBeforeSave(data);
|
||||
},
|
||||
afterTypeChange: mutState => {
|
||||
initFieldsIfMissing(mutState, 'mosaico');
|
||||
},
|
||||
validate: state => {
|
||||
const mosaicoTemplate = state.getIn(['mosaicoTemplate', 'value']);
|
||||
if (!mosaicoTemplate) {
|
||||
state.setIn(['mosaicoTemplate', 'error'], t('Mosaico template must be selected'));
|
||||
} else {
|
||||
state.setIn(['mosaicoTemplate', 'error'], null);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
templateTypes.grapejs = {
|
||||
typeName: t('GrapeJS')
|
||||
templateTypes.grapejs = { // TODO
|
||||
typeName: t('GrapeJS'),
|
||||
getTypeForm: (owner, isEdit) => null,
|
||||
getHTMLEditor: owner => null,
|
||||
exportHTMLEditorData: async owner => {},
|
||||
initData: () => ({}),
|
||||
afterLoad: data => {},
|
||||
beforeSave: data => {
|
||||
clearBeforeSave(data);
|
||||
},
|
||||
afterTypeChange: mutState => {},
|
||||
validate: state => {}
|
||||
};
|
||||
|
||||
templateTypes.ckeditor = {
|
||||
typeName: t('CKEditor'),
|
||||
getTypeForm: (owner, isEdit) => null,
|
||||
getHTMLEditor: owner => <CKEditor id="html" height="600px" label={t('Template content (HTML)')}/>,
|
||||
htmlEditorBeforeSave: async owner => {}
|
||||
exportHTMLEditorData: async owner => {},
|
||||
initData: () => ({}),
|
||||
afterLoad: data => {},
|
||||
beforeSave: data => {
|
||||
clearBeforeSave(data);
|
||||
},
|
||||
afterTypeChange: mutState => {},
|
||||
validate: state => {}
|
||||
};
|
||||
|
||||
templateTypes.codeeditor = {
|
||||
typeName: t('Code Editor'),
|
||||
getTypeForm: (owner, isEdit) => null,
|
||||
getHTMLEditor: owner => <ACEEditor id="html" height="600px" mode="html" label={t('Template content (HTML)')}/>,
|
||||
htmlEditorBeforeSave: async owner => {}
|
||||
exportHTMLEditorData: async owner => {},
|
||||
initData: () => ({}),
|
||||
afterLoad: data => {},
|
||||
beforeSave: data => {
|
||||
clearBeforeSave(data);
|
||||
},
|
||||
afterTypeChange: mutState => {},
|
||||
validate: state => {}
|
||||
};
|
||||
|
||||
templateTypes.mjml = {
|
||||
typeName: t('MJML')
|
||||
templateTypes.mjml = { // TODO
|
||||
getTypeForm: (owner, isEdit) => null,
|
||||
getHTMLEditor: owner => null,
|
||||
exportHTMLEditorData: async owner => {},
|
||||
initData: () => ({}),
|
||||
afterLoad: data => {},
|
||||
beforeSave: data => {
|
||||
clearBeforeSave(data);
|
||||
},
|
||||
afterTypeChange: mutState => {},
|
||||
validate: state => {}
|
||||
};
|
||||
|
||||
return templateTypes;
|
||||
|
|
|
@ -124,10 +124,10 @@ export default class CUD extends Component {
|
|||
let sendMethod, url;
|
||||
if (this.props.entity) {
|
||||
sendMethod = FormSendMethod.PUT;
|
||||
url = `/rest/mosaico-templates/${this.props.entity.id}`
|
||||
url = `rest/mosaico-templates/${this.props.entity.id}`
|
||||
} else {
|
||||
sendMethod = FormSendMethod.POST;
|
||||
url = '/rest/mosaico-templates'
|
||||
url = 'rest/mosaico-templates'
|
||||
}
|
||||
|
||||
this.disableForm();
|
||||
|
@ -139,7 +139,7 @@ export default class CUD extends Component {
|
|||
|
||||
if (submitSuccessful) {
|
||||
if (stay) {
|
||||
await this.getFormValuesFromURL(`/rest/mosaico-templates/${this.props.entity.id}`, data => {
|
||||
await this.getFormValuesFromURL(`rest/mosaico-templates/${this.props.entity.id}`, data => {
|
||||
this.templateTypes[data.type].afterLoad(data);
|
||||
});
|
||||
this.enableForm();
|
||||
|
@ -170,7 +170,7 @@ export default class CUD extends Component {
|
|||
<DeleteModalDialog
|
||||
stateOwner={this}
|
||||
visible={this.props.action === 'delete'}
|
||||
deleteUrl={`/rest/templates/mosaico/${this.props.entity.id}`}
|
||||
deleteUrl={`rest/mosaico-templates/${this.props.entity.id}`}
|
||||
cudUrl={`/templates/mosaico/${this.props.entity.id}/edit`}
|
||||
listUrl="/templates/mosaico"
|
||||
deletingMsg={t('Deleting Mosaico template ...')}
|
||||
|
|
|
@ -9,6 +9,7 @@ import { Table } from '../../lib/table';
|
|||
import axios from '../../lib/axios';
|
||||
import moment from 'moment';
|
||||
import { getTemplateTypes } from './helpers';
|
||||
import {checkPermissions} from "../../lib/permissions";
|
||||
|
||||
|
||||
@translate()
|
||||
|
@ -26,14 +27,12 @@ export default class List extends Component {
|
|||
|
||||
@withAsyncErrorHandler
|
||||
async fetchPermissions() {
|
||||
const request = {
|
||||
const result = await checkPermissions({
|
||||
createMosaicoTemplate: {
|
||||
entityTypeId: 'namespace',
|
||||
requiredOperations: ['createMosaicoTemplate']
|
||||
}
|
||||
};
|
||||
|
||||
const result = await axios.post('/rest/permissions-check', request);
|
||||
});
|
||||
|
||||
this.setState({
|
||||
createPermitted: result.data.createMosaicoTemplate
|
||||
|
@ -90,7 +89,7 @@ export default class List extends Component {
|
|||
|
||||
<Title>{t('Mosaico Templates')}</Title>
|
||||
|
||||
<Table withHeader dataUrl="/rest/mosaico-templates-table" columns={columns} />
|
||||
<Table withHeader dataUrl="rest/mosaico-templates-table" columns={columns} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -20,7 +20,7 @@ function getMenus(t) {
|
|||
':templateId([0-9]+)': {
|
||||
title: resolved => t('Template "{{name}}"', {name: resolved.template.name}),
|
||||
resolve: {
|
||||
template: params => `/rest/templates/${params.templateId}`
|
||||
template: params => `rest/templates/${params.templateId}`
|
||||
},
|
||||
link: params => `/templates/${params.templateId}/edit`,
|
||||
navs: {
|
||||
|
@ -56,7 +56,7 @@ function getMenus(t) {
|
|||
':mosaiceTemplateId([0-9]+)': {
|
||||
title: resolved => t('Mosaico Template "{{name}}"', {name: resolved.mosaicoTemplate.name}),
|
||||
resolve: {
|
||||
mosaicoTemplate: params => `/rest/mosaico-templates/${params.mosaiceTemplateId}`
|
||||
mosaicoTemplate: params => `rest/mosaico-templates/${params.mosaiceTemplateId}`
|
||||
},
|
||||
link: params => `/templates/mosaico/${params.mosaiceTemplateId}/edit`,
|
||||
navs: {
|
||||
|
|
|
@ -27,7 +27,7 @@ export default class CUD extends Component {
|
|||
|
||||
this.initForm({
|
||||
serverValidation: {
|
||||
url: '/rest/users-validate',
|
||||
url: 'rest/users-validate',
|
||||
changed: mailtrainConfig.isAuthMethodLocal ? ['username', 'email'] : ['username'],
|
||||
extra: ['id']
|
||||
}
|
||||
|
@ -133,10 +133,10 @@ export default class CUD extends Component {
|
|||
let sendMethod, url;
|
||||
if (this.props.entity) {
|
||||
sendMethod = FormSendMethod.PUT;
|
||||
url = `/rest/users/${this.props.entity.id}`
|
||||
url = `rest/users/${this.props.entity.id}`
|
||||
} else {
|
||||
sendMethod = FormSendMethod.POST;
|
||||
url = '/rest/users'
|
||||
url = 'rest/users'
|
||||
}
|
||||
|
||||
try {
|
||||
|
@ -196,7 +196,7 @@ export default class CUD extends Component {
|
|||
<DeleteModalDialog
|
||||
stateOwner={this}
|
||||
visible={this.props.action === 'delete'}
|
||||
deleteUrl={`/rest/users/${this.props.entity.id}`}
|
||||
deleteUrl={`rest/users/${this.props.entity.id}`}
|
||||
cudUrl={`/users/${this.props.entity.id}/edit`}
|
||||
listUrl="/users"
|
||||
deletingMsg={t('Deleting user ...')}
|
||||
|
@ -215,7 +215,7 @@ export default class CUD extends Component {
|
|||
<InputField id="password2" label={t('Repeat Password')} type="password"/>
|
||||
</div>
|
||||
}
|
||||
<TableSelect id="role" label={t('Role')} withHeader dropdown dataUrl={'/rest/shares-roles-table/global'} columns={rolesColumns} selectionLabelIndex={1}/>
|
||||
<TableSelect id="role" label={t('Role')} withHeader dropdown dataUrl={'rest/shares-roles-table/global'} columns={rolesColumns} selectionLabelIndex={1}/>
|
||||
<NamespaceSelect/>
|
||||
|
||||
<ButtonRow>
|
||||
|
|
|
@ -53,7 +53,7 @@ export default class List extends Component {
|
|||
|
||||
<Title>{t('Users')}</Title>
|
||||
|
||||
<Table withHeader dataUrl="/rest/users-table" columns={columns} />
|
||||
<Table withHeader dataUrl="rest/users-table" columns={columns} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -20,7 +20,7 @@ function getMenus(t) {
|
|||
':userId([0-9]+)': {
|
||||
title: resolved => t('User "{{name}}"', {name: resolved.user.name}),
|
||||
resolve: {
|
||||
user: params => `/rest/users/${params.userId}`
|
||||
user: params => `rest/users/${params.userId}`
|
||||
},
|
||||
link: params => `/users/${params.userId}/edit`,
|
||||
navs: {
|
||||
|
|
|
@ -14,9 +14,9 @@ async function getAnonymousConfig(context, trusted) {
|
|||
externalPasswordResetLink: config.ldap.passwordresetlink,
|
||||
language: config.language || 'en',
|
||||
isAuthenticated: !!context.user,
|
||||
trustedUrlBase: urls.getTrustedUrl(),
|
||||
trustedUrlBase: urls.getTrustedUrlBase(),
|
||||
trustedUrlBaseDir: urls.getTrustedUrlBaseDir(),
|
||||
sandboxUrlBase: urls.getSandboxUrl(),
|
||||
sandboxUrlBase: urls.getSandboxUrlBase(),
|
||||
sandboxUrlBaseDir: urls.getSandboxUrlBaseDir(),
|
||||
trusted
|
||||
}
|
||||
|
|
|
@ -10,9 +10,9 @@ const multer = require('multer')({
|
|||
dest: uploadedFilesDir
|
||||
});
|
||||
|
||||
function installUploadHandler(router, url, dontReplace = false) {
|
||||
function installUploadHandler(router, url, getUrl = null, dontReplace = false) {
|
||||
router.postAsync(url, passport.loggedIn, multer.array('files[]'), async (req, res) => {
|
||||
return res.json(await files.createFiles(req.context, req.params.type, req.params.entityId, req.files, dontReplace));
|
||||
return res.json(await files.createFiles(req.context, req.params.type, req.params.entityId, req.files, getUrl, dontReplace));
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -116,16 +116,22 @@ module.exports.authByAccessToken = (req, res, next) => {
|
|||
};
|
||||
|
||||
module.exports.tryAuthByRestrictedAccessToken = (req, res, next) => {
|
||||
if (req.cookies.restricted_access_token) {
|
||||
users.getByRestrictedAccessToken(req.cookies.restricted_access_token).then(user => {
|
||||
req.user = user;
|
||||
next();
|
||||
}).catch(err => {
|
||||
next();
|
||||
});
|
||||
} else {
|
||||
const pathComps = req.url.split('/');
|
||||
|
||||
pathComps.shift();
|
||||
const restrictedAccessToken = pathComps.shift();
|
||||
pathComps.unshift('');
|
||||
|
||||
const url = pathComps.join('/');
|
||||
|
||||
req.url = url;
|
||||
|
||||
users.getByRestrictedAccessToken(restrictedAccessToken).then(user => {
|
||||
req.user = user;
|
||||
next();
|
||||
}
|
||||
}).catch(err => {
|
||||
next();
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.setup = app => {
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
const _ = require('./translate')._;
|
||||
const util = require('util');
|
||||
const isemail = require('isemail');
|
||||
const path = require('path');
|
||||
|
||||
const bluebird = require('bluebird');
|
||||
|
||||
|
@ -46,7 +47,7 @@ async function getTemplate(template) {
|
|||
source = compiled.html;
|
||||
}
|
||||
|
||||
const renderer = hbs.handlebars.compile(compiled.html);
|
||||
const renderer = hbs.handlebars.compile(source);
|
||||
templates.set(key, renderer);
|
||||
|
||||
return renderer;
|
||||
|
|
26
lib/urls.js
26
lib/urls.js
|
@ -3,27 +3,41 @@
|
|||
const config = require('config');
|
||||
const urllib = require('url');
|
||||
|
||||
function getTrustedUrlBase() {
|
||||
return urllib.resolve(config.www.trustedUrlBase, '');
|
||||
}
|
||||
|
||||
function getSandboxUrlBase() {
|
||||
return urllib.resolve(config.www.sandboxUrlBase, '');
|
||||
}
|
||||
|
||||
function getTrustedUrl(path) {
|
||||
return urllib.resolve(config.www.trustedUrlBase, path || '');
|
||||
}
|
||||
|
||||
function getSandboxUrl(path) {
|
||||
return urllib.resolve(config.www.sandboxUrlBase, path || '');
|
||||
function getSandboxUrl(path, context) {
|
||||
if (context && context.user && context.user.restrictedAccessToken) {
|
||||
return urllib.resolve(config.www.sandboxUrlBase, context.user.restrictedAccessToken + '/' + (path || ''));
|
||||
} else {
|
||||
return urllib.resolve(config.www.sandboxUrlBase, 'ANONYMOUS/' + (path || ''));
|
||||
}
|
||||
}
|
||||
|
||||
function getTrustedUrlBaseDir() {
|
||||
const mailtrainUrl = urllib.parse(getTrustedUrl());
|
||||
return mailtrainUrl.pathname;
|
||||
const ivisUrl = urllib.parse(config.www.trustedUrlBase);
|
||||
return ivisUrl.pathname;
|
||||
}
|
||||
|
||||
function getSandboxUrlBaseDir() {
|
||||
const mailtrainUrl = urllib.parse(getSandboxUrl());
|
||||
return mailtrainUrl.pathname;
|
||||
const ivisUrl = urllib.parse(config.www.sandboxUrlBase);
|
||||
return ivisUrl.pathname;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getTrustedUrl,
|
||||
getSandboxUrl,
|
||||
getTrustedUrlBase,
|
||||
getSandboxUrlBase,
|
||||
getTrustedUrlBaseDir,
|
||||
getSandboxUrlBaseDir
|
||||
};
|
|
@ -21,6 +21,10 @@ function getFilePath(type, entityId, filename) {
|
|||
return path.join(path.join(filesDir, type, entityId.toString()), filename);
|
||||
}
|
||||
|
||||
function getFileUrl(context, type, entityId, filename, getUrl) {
|
||||
return getUrl(`files/${type}/${entityId}/${filename}`, context)
|
||||
}
|
||||
|
||||
function getFilesTable(type) {
|
||||
return entityTypes[type].filesTable;
|
||||
}
|
||||
|
@ -46,7 +50,7 @@ async function getFileById(context, type, id) {
|
|||
enforceTypePermitted(type);
|
||||
const file = await knex.transaction(async tx => {
|
||||
const file = await tx(getFilesTable(type)).where('id', id).first();
|
||||
await shares.enforceEntityPermissionTx(tx, context, type, file.entity, 'manageFiles');
|
||||
await shares.enforceEntityPermissionTx(tx, context, type, file.entity, 'view');
|
||||
return file;
|
||||
});
|
||||
|
||||
|
@ -64,7 +68,7 @@ async function getFileById(context, type, id) {
|
|||
async function getFileByFilename(context, type, entityId, name) {
|
||||
enforceTypePermitted(type);
|
||||
const file = await knex.transaction(async tx => {
|
||||
await shares.enforceEntityPermissionTx(tx, context, type, entityId, 'view');
|
||||
// XXX - Note that we don't check permissions here. This makes files generally public. However one has to know the generated name of the file.
|
||||
const file = await tx(getFilesTable(type)).where({entity: entityId, filename: name}).first();
|
||||
return file;
|
||||
});
|
||||
|
@ -80,7 +84,17 @@ async function getFileByFilename(context, type, entityId, name) {
|
|||
};
|
||||
}
|
||||
|
||||
async function createFiles(context, type, entityId, files, dontReplace = false) {
|
||||
async function getFileByUrl(context, type, entityId, url, getUrl) {
|
||||
const urlPrefix = getUrl(`files/${type}/${entityId}/`, context);
|
||||
if (url.startsWith(urlPrefix)) {
|
||||
const name = url.substring(urlPrefix.length);
|
||||
return await getFileByFilename(context, type, entityId, name);
|
||||
} else {
|
||||
throw new interoperableErrors.NotFoundError();
|
||||
}
|
||||
}
|
||||
|
||||
async function createFiles(context, type, entityId, files, getUrl = null, dontReplace = false) {
|
||||
enforceTypePermitted(type);
|
||||
if (files.length == 0) {
|
||||
// No files uploaded
|
||||
|
@ -133,14 +147,19 @@ async function createFiles(context, type, entityId, files, dontReplace = false)
|
|||
size: file.size
|
||||
});
|
||||
|
||||
filesRet.push({
|
||||
const filesRetEntry = {
|
||||
name: file.filename,
|
||||
originalName: originalName,
|
||||
size: file.size,
|
||||
type: file.mimetype,
|
||||
url: `/files/${type}/${entityId}/${file.filename}`,
|
||||
thumbnailUrl: `/files/${type}/${entityId}/${file.filename}` // TODO - use smaller thumbnails
|
||||
});
|
||||
};
|
||||
|
||||
if (getUrl) {
|
||||
filesRetEntry.url = getFileUrl(context, type, entityId, file.filename, getUrl);
|
||||
filesRetEntry.thumbnailUrl = getFileUrl(context, type, entityId, file.filename, getUrl); // TODO - use smaller thumbnails
|
||||
}
|
||||
|
||||
filesRet.push(filesRetEntry);
|
||||
|
||||
if (existingNameMap.has(originalName)) {
|
||||
removedFiles.push(existingNameMap.get(originalName));
|
||||
|
@ -164,16 +183,16 @@ async function createFiles(context, type, entityId, files, dontReplace = false)
|
|||
// The names should be unique, so overwrite is disabled
|
||||
// The directory is created if it does not exist
|
||||
// Empty options argument is passed, otherwise fails
|
||||
await fs.move(file.path, filePath, {});
|
||||
await fs.moveAsync(file.path, filePath, {});
|
||||
}
|
||||
// Remove replaced files from files directory
|
||||
for (const file of removedFiles) {
|
||||
const filePath = getFilePath(type, entityId, file.filename);
|
||||
await fs.remove(filePath);
|
||||
await fs.removeAsync(filePath);
|
||||
}
|
||||
// Remove ignored files from upload directory
|
||||
for (const file of ignoredFiles) {
|
||||
await fs.remove(file.path);
|
||||
await fs.removeAsync(file.path);
|
||||
}
|
||||
|
||||
return {
|
||||
|
@ -194,15 +213,18 @@ async function removeFile(context, type, id) {
|
|||
});
|
||||
|
||||
const filePath = getFilePath(type, file.entity, file.filename);
|
||||
await fs.remove(filePath);
|
||||
await fs.removeAsync(filePath);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
filesDir,
|
||||
listDTAjax,
|
||||
list,
|
||||
getFileById,
|
||||
getFileByFilename,
|
||||
getFileByUrl,
|
||||
createFiles,
|
||||
removeFile,
|
||||
filesDir
|
||||
getFileUrl,
|
||||
getFilePath
|
||||
};
|
|
@ -97,7 +97,6 @@ async function remove(context, id) {
|
|||
});
|
||||
}
|
||||
|
||||
|
||||
module.exports = {
|
||||
hash,
|
||||
getById,
|
||||
|
|
|
@ -15,13 +15,16 @@ function hash(entity) {
|
|||
return hasher.hash(filterObject(entity, allowedKeys));
|
||||
}
|
||||
|
||||
async function getById(context, id) {
|
||||
async function getById(context, id, withPermissions = true) {
|
||||
return await knex.transaction(async tx => {
|
||||
await shares.enforceEntityPermissionTx(tx, context, 'template', id, 'view');
|
||||
const entity = await tx('templates').where('id', id).first();
|
||||
entity.data = JSON.parse(entity.data);
|
||||
|
||||
entity.permissions = await shares.getPermissionsTx(tx, context, 'template', id);
|
||||
if (withPermissions) {
|
||||
entity.permissions = await shares.getPermissionsTx(tx, context, 'template', id);
|
||||
}
|
||||
|
||||
return entity;
|
||||
});
|
||||
}
|
||||
|
|
|
@ -385,6 +385,16 @@ async function getRestrictedAccessToken(context, method, params) {
|
|||
return token;
|
||||
}
|
||||
|
||||
async function refreshRestrictedAccessToken(context, token) {
|
||||
const tokenEntry = restrictedAccessTokens.get(token);
|
||||
|
||||
if (tokenEntry && tokenEntry.userId === context.user.id) {
|
||||
tokenEntry.expires = Date.now() + 120 * 1000
|
||||
} else {
|
||||
shares.throwPermissionDenied();
|
||||
}
|
||||
}
|
||||
|
||||
async function getByRestrictedAccessToken(token) {
|
||||
const now = Date.now();
|
||||
for (const entry of restrictedAccessTokens.values()) {
|
||||
|
@ -398,6 +408,7 @@ async function getByRestrictedAccessToken(token) {
|
|||
if (tokenEntry) {
|
||||
const user = await getById(contextHelpers.getAdminContext(), tokenEntry.userId);
|
||||
user.restrictedAccessHandler = tokenEntry.handler;
|
||||
user.restrictedAccessToken = tokenEntry.token;
|
||||
|
||||
return user;
|
||||
|
||||
|
@ -425,5 +436,6 @@ module.exports = {
|
|||
resetPassword,
|
||||
getByRestrictedAccessToken,
|
||||
getRestrictedAccessToken,
|
||||
refreshRestrictedAccessToken,
|
||||
registerRestrictedAccessTokenMethod
|
||||
};
|
|
@ -3,6 +3,7 @@
|
|||
const passport = require('../lib/passport');
|
||||
const _ = require('../lib/translate')._;
|
||||
const clientHelpers = require('../lib/client-helpers');
|
||||
const { getTrustedUrl } = require('../lib/urls');
|
||||
|
||||
const routerFactory = require('../lib/router-async');
|
||||
|
||||
|
@ -18,7 +19,11 @@ function getRouter(trusted) {
|
|||
|
||||
res.render('root', {
|
||||
reactCsrfToken: req.csrfToken(),
|
||||
mailtrainConfig: JSON.stringify(mailtrainConfig)
|
||||
mailtrainConfig: JSON.stringify(mailtrainConfig),
|
||||
scriptFiles: [
|
||||
getTrustedUrl('mailtrain/common.js'),
|
||||
getTrustedUrl('mailtrain/root.js')
|
||||
]
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
'use strict';
|
||||
|
||||
const config = require('config');
|
||||
const express = require('express');
|
||||
const path = require('path');
|
||||
const routerFactory = require('../lib/router-async');
|
||||
const passport = require('../lib/passport');
|
||||
const clientHelpers = require('../lib/client-helpers');
|
||||
|
@ -12,28 +14,38 @@ const users = require('../models/users');
|
|||
const bluebird = require('bluebird');
|
||||
const fsReadFile = bluebird.promisify(require('fs').readFile);
|
||||
|
||||
const path = require('path');
|
||||
|
||||
const files = require('../models/files');
|
||||
const fileHelpers = require('../lib/file-helpers');
|
||||
|
||||
const templates = require('../models/templates');
|
||||
const mosaicoTemplates = require('../models/mosaico-templates');
|
||||
|
||||
const contextHelpers = require('../lib/context-helpers');
|
||||
|
||||
const { getTrustedUrl, getSandboxUrl } = require('../lib/urls');
|
||||
const { base } = require('../shared/templates');
|
||||
|
||||
|
||||
users.registerRestrictedAccessTokenMethod('mosaico', async ({entityTypeId, entityId}) => {
|
||||
if (entityTypeId === 'template' || entityTypeId === 'campaign') {
|
||||
return {
|
||||
permissions: {
|
||||
[entityTypeId]: {
|
||||
[entityId]: new Set(['manageFiles', 'view'])
|
||||
if (entityTypeId === 'template') {
|
||||
const tmpl = await templates.getById(contextHelpers.getAdminContext(), entityId, false);
|
||||
|
||||
if (tmpl.type === 'mosaico') {
|
||||
return {
|
||||
permissions: {
|
||||
'template': {
|
||||
[entityId]: new Set(['manageFiles', 'view'])
|
||||
},
|
||||
'mosaicoTemplate': {
|
||||
[tmpl.data.mosaicoTemplate]: new Set(['view'])
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
// FIXME - add authentication by sandboxToken
|
||||
|
||||
async function placeholderImage(width, height) {
|
||||
const magick = gm(width, height, '#707070');
|
||||
const streamAsync = bluebird.promisify(magick.stream.bind(magick));
|
||||
|
@ -69,9 +81,7 @@ async function placeholderImage(width, height) {
|
|||
};
|
||||
}
|
||||
|
||||
async function resizedImage(src, method, width, height) {
|
||||
const filePath = path.join(__dirname, '..', src);
|
||||
|
||||
async function resizedImage(filePath, method, width, height) {
|
||||
const magick = gm(filePath);
|
||||
const streamAsync = bluebird.promisify(magick.stream.bind(magick));
|
||||
const formatAsync = bluebird.promisify(magick.format.bind(magick));
|
||||
|
@ -114,42 +124,55 @@ function sanitizeSize(val, min, max, defaultVal, allowNull) {
|
|||
|
||||
function getRouter(trusted) {
|
||||
const router = routerFactory.create();
|
||||
|
||||
const getUrl = trusted ? getTrustedUrl : getSandboxUrl;
|
||||
|
||||
router.getAsync('/img/:type/:entityId', passport.loggedIn, async (req, res) => {
|
||||
const method = req.query.method;
|
||||
const params = req.query.params;
|
||||
let [width, height] = params.split(',');
|
||||
let image;
|
||||
|
||||
if (method === 'placeholder') {
|
||||
width = sanitizeSize(width, 1, 2048, 600, false);
|
||||
height = sanitizeSize(height, 1, 2048, 300, false);
|
||||
image = await placeholderImage(width, height);
|
||||
|
||||
} else {
|
||||
width = sanitizeSize(width, 1, 2048, 600, false);
|
||||
height = sanitizeSize(height, 1, 2048, 300, true);
|
||||
|
||||
const file = await files.getFileByUrl(req.context, req.params.type, req.params.entityId, req.query.src, getUrl);
|
||||
image = await resizedImage(file.path, method, width, height);
|
||||
}
|
||||
|
||||
res.set('Content-Type', 'image/' + image.format);
|
||||
image.stream.pipe(res);
|
||||
});
|
||||
|
||||
router.getAsync('/templates/:mosaicoTemplateId/index.html', passport.loggedIn, async (req, res) => {
|
||||
const tmpl = await mosaicoTemplates.getById(req.context, req.params.mosaicoTemplateId);
|
||||
|
||||
res.set('Content-Type', 'text/html');
|
||||
res.send(base(tmpl.data.html, getUrl('', req.context)));
|
||||
});
|
||||
|
||||
router.use('/templates/:mosaicoTemplateId', express.static(path.join(__dirname, '..', 'client', 'public', 'mosaico', 'templates', 'versafix-1')));
|
||||
|
||||
|
||||
if (!trusted) {
|
||||
router.getAsync('/img/:type/:fileId', passport.loggedIn, async (req, res) => {
|
||||
const method = req.query.method;
|
||||
const params = req.query.params;
|
||||
let [width, height] = params.split(',');
|
||||
let image;
|
||||
fileHelpers.installUploadHandler(router, '/upload/:type/:entityId', getUrl, true);
|
||||
|
||||
if (method === 'placeholder') {
|
||||
width = sanitizeSize(width, 1, 2048, 600, false);
|
||||
height = sanitizeSize(height, 1, 2048, 300, false);
|
||||
image = await placeholderImage(width, height);
|
||||
} else {
|
||||
width = sanitizeSize(width, 1, 2048, 600, false);
|
||||
height = sanitizeSize(height, 1, 2048, 300, true);
|
||||
// TODO - validate that one has the rights to read this ???
|
||||
image = await resizedImage(req.query.src, method, width, height);
|
||||
}
|
||||
|
||||
res.set('Content-Type', 'image/' + image.format);
|
||||
image.stream.pipe(res);
|
||||
});
|
||||
|
||||
|
||||
fileHelpers.installUploadHandler(router, '/upload/:type/:entityId', true);
|
||||
|
||||
router.getAsync('/upload/:type/:fileId', passport.loggedIn, async (req, res) => {
|
||||
const entries = await files.list(req.context, req.params.type, req.params.fileId);
|
||||
router.getAsync('/upload/:type/:entityId', passport.loggedIn, async (req, res) => {
|
||||
const entries = await files.list(req.context, req.params.type, req.params.entityId);
|
||||
|
||||
const filesOut = [];
|
||||
for (const entry of entries) {
|
||||
filesOut.push({
|
||||
name: entry.originalname,
|
||||
url: `/files/${req.params.type}/${req.params.fileId}/${entry.filename}`,
|
||||
url: files.getFileUrl(req.context, req.params.type, req.params.entityId, entry.filename, getUrl),
|
||||
size: entry.size,
|
||||
thumbnailUrl: `/files/${req.params.type}/${req.params.fileId}/${entry.filename}` // TODO - use smaller thumbnails
|
||||
thumbnailUrl: files.getFileUrl(req.context, req.params.type, req.params.entityId, entry.filename, getUrl) // TODO - use smaller thumbnails
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -158,7 +181,6 @@ function getRouter(trusted) {
|
|||
});
|
||||
});
|
||||
|
||||
|
||||
router.getAsync('/editor', passport.csrfProtection, async (req, res) => {
|
||||
const resourceType = req.query.type;
|
||||
const resourceId = req.query.id;
|
||||
|
@ -180,7 +202,12 @@ function getRouter(trusted) {
|
|||
editorConfig: config.mosaico,
|
||||
languageStrings: languageStrings,
|
||||
reactCsrfToken: req.csrfToken(),
|
||||
mailtrainConfig: JSON.stringify(mailtrainConfig)
|
||||
mailtrainConfig: JSON.stringify(mailtrainConfig),
|
||||
scriptFiles: [
|
||||
getUrl('mailtrain/common.js'),
|
||||
getUrl('mailtrain/mosaico.js')
|
||||
],
|
||||
mosaicoPublicPath: getUrl('public/mosaico')
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -65,4 +65,9 @@ router.postAsync('/restricted-access-token', passport.loggedIn, async (req, res)
|
|||
|
||||
});
|
||||
|
||||
router.putAsync('/restricted-access-token', passport.loggedIn, async (req, res) => {
|
||||
await users.refreshRestrictedAccessToken(req.context, req.body.token);
|
||||
return res.json();
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
|
|
@ -13,6 +13,7 @@ const _ = require('../lib/translate')._;
|
|||
const contextHelpers = require('../lib/context-helpers');
|
||||
const forms = require('../models/forms');
|
||||
const {getTrustedUrl} = require('../lib/urls');
|
||||
const bluebird = require('bluebird');
|
||||
|
||||
const { SubscriptionStatus } = require('../shared/lists');
|
||||
|
||||
|
|
|
@ -1397,78 +1397,78 @@ const versafix = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
|
|||
' <td align="right" valign="middle" class="links-color socialLinks mobile-textcenter" data-ko-display="socialIconType eq \'colors\'">\n' +
|
||||
' <span data-ko-display="fbVisible" data-ko-wrap="false"> </span>\n' +
|
||||
' <a data-ko-display="fbVisible" href="" style="-ko-attr-href: @fbUrl">\n' +
|
||||
' <img src="[URL_BASE]/public/mosaico/img/social_def/facebook_ok.png" alt="Facebook" border="0" class="socialIcon" />\n' +
|
||||
' <img src="img/social_def/facebook_ok.png" alt="Facebook" border="0" class="socialIcon" />\n' +
|
||||
' </a>\n' +
|
||||
' <span data-ko-display="twVisible" data-ko-wrap="false"> </span>\n' +
|
||||
' <a data-ko-display="twVisible" href="" style="-ko-attr-href: @twUrl">\n' +
|
||||
' <img src="[URL_BASE]/public/mosaico/img/social_def/twitter_ok.png" alt="Twitter" border="0" class="socialIcon" />\n' +
|
||||
' <img src="img/social_def/twitter_ok.png" alt="Twitter" border="0" class="socialIcon" />\n' +
|
||||
' </a>\n' +
|
||||
' <span data-ko-display="ggVisible" data-ko-wrap="false"> </span>\n' +
|
||||
' <a data-ko-display="ggVisible" href="" style="-ko-attr-href: @ggUrl">\n' +
|
||||
' <img src="[URL_BASE]/public/mosaico/img/social_def/google+_ok.png" alt="Google+" border="0" class="socialIcon" />\n' +
|
||||
' <img src="img/social_def/google+_ok.png" alt="Google+" border="0" class="socialIcon" />\n' +
|
||||
' </a>\n' +
|
||||
' <span data-ko-display="webVisible" data-ko-wrap="false" style="display: none"> </span>\n' +
|
||||
' <a data-ko-display="webVisible" href="" style="-ko-attr-href: @webUrl; display: none">\n' +
|
||||
' <img src="[URL_BASE]/public/mosaico/img/social_def/web_ok.png" alt="Web" border="0" class="socialIcon" />\n' +
|
||||
' <img src="img/social_def/web_ok.png" alt="Web" border="0" class="socialIcon" />\n' +
|
||||
' </a>\n' +
|
||||
' <span data-ko-display="inVisible" data-ko-wrap="false" style="display: none"> </span>\n' +
|
||||
' <a data-ko-display="inVisible" href="" style="-ko-attr-href: @inUrl; display: none">\n' +
|
||||
' <img src="[URL_BASE]/public/mosaico/img/social_def/linkedin_ok.png" alt="Linkedin" border="0" class="socialIcon" />\n' +
|
||||
' <img src="img/social_def/linkedin_ok.png" alt="Linkedin" border="0" class="socialIcon" />\n' +
|
||||
' </a>\n' +
|
||||
' <span data-ko-display="flVisible" data-ko-wrap="false" style="display: none"> </span>\n' +
|
||||
' <a data-ko-display="flVisible" href="" style="-ko-attr-href: @flUrl; display: none">\n' +
|
||||
' <img src="[URL_BASE]/public/mosaico/img/social_def/flickr_ok.png" alt="Flickr" border="0" class="socialIcon" />\n' +
|
||||
' <img src="img/social_def/flickr_ok.png" alt="Flickr" border="0" class="socialIcon" />\n' +
|
||||
' </a>\n' +
|
||||
' <span data-ko-display="viVisible" data-ko-wrap="false" style="display: none"> </span>\n' +
|
||||
' <a data-ko-display="viVisible" href="" style="-ko-attr-href: @viUrl; display: none">\n' +
|
||||
' <img src="[URL_BASE]/public/mosaico/img/social_def/vimeo_ok.png" alt="Vimeo" border="0" class="socialIcon" />\n' +
|
||||
' <img src="img/social_def/vimeo_ok.png" alt="Vimeo" border="0" class="socialIcon" />\n' +
|
||||
' </a>\n' +
|
||||
' <span data-ko-display="instVisible" data-ko-wrap="false" style="display: none"> </span>\n' +
|
||||
' <a data-ko-display="instVisible" href="" style="-ko-attr-href: @instUrl; display: none">\n' +
|
||||
' <img src="[URL_BASE]/public/mosaico/img/social_def/instagram_ok.png" alt="Instagram" border="0" class="socialIcon" />\n' +
|
||||
' <img src="img/social_def/instagram_ok.png" alt="Instagram" border="0" class="socialIcon" />\n' +
|
||||
' </a>\n' +
|
||||
' <span data-ko-display="youVisible" data-ko-wrap="false" style="display: none"> </span>\n' +
|
||||
' <a data-ko-display="youVisible" href="" style="-ko-attr-href: @youUrl; display: none">\n' +
|
||||
' <img src="[URL_BASE]/public/mosaico/img/social_def/youtube_ok.png" alt="Youtube" border="0" class="socialIcon" />\n' +
|
||||
' <img src="img/social_def/youtube_ok.png" alt="Youtube" border="0" class="socialIcon" />\n' +
|
||||
' </a>\n' +
|
||||
' </td>\n' +
|
||||
' <td align="right" valign="middle" class="links-color socialLinks mobile-textcenter" data-ko-display="socialIconType eq \'bw\'"\n' +
|
||||
' style="display: none">\n' +
|
||||
' <span data-ko-display="fbVisible" data-ko-wrap="false"> </span>\n' +
|
||||
' <a data-ko-display="fbVisible" href="" style="-ko-attr-href: @fbUrl">\n' +
|
||||
' <img src="[URL_BASE]/public/mosaico/img/social_def/facebook_bw_ok.png" alt="Facebook" border="0" class="socialIcon" />\n' +
|
||||
' <img src="img/social_def/facebook_bw_ok.png" alt="Facebook" border="0" class="socialIcon" />\n' +
|
||||
' </a>\n' +
|
||||
' <span data-ko-display="twVisible" data-ko-wrap="false"> </span>\n' +
|
||||
' <a data-ko-display="twVisible" href="" style="-ko-attr-href: @twUrl">\n' +
|
||||
' <img src="[URL_BASE]/public/mosaico/img/social_def/twitter_bw_ok.png" alt="Twitter" border="0" class="socialIcon" />\n' +
|
||||
' <img src="img/social_def/twitter_bw_ok.png" alt="Twitter" border="0" class="socialIcon" />\n' +
|
||||
' </a>\n' +
|
||||
' <span data-ko-display="ggVisible" data-ko-wrap="false"> </span>\n' +
|
||||
' <a data-ko-display="ggVisible" href="" style="-ko-attr-href: @ggUrl">\n' +
|
||||
' <img src="[URL_BASE]/public/mosaico/img/social_def/google+_bw_ok.png" alt="Google+" border="0" class="socialIcon" />\n' +
|
||||
' <img src="img/social_def/google+_bw_ok.png" alt="Google+" border="0" class="socialIcon" />\n' +
|
||||
' </a>\n' +
|
||||
' <span data-ko-display="webVisible" data-ko-wrap="false" style="display: none"> </span>\n' +
|
||||
' <a data-ko-display="webVisible" href="" style="-ko-attr-href: @webUrl; display: none">\n' +
|
||||
' <img src="[URL_BASE]/public/mosaico/img/social_def/web_bw_ok.png" alt="Web" border="0" class="socialIcon" />\n' +
|
||||
' <img src="img/social_def/web_bw_ok.png" alt="Web" border="0" class="socialIcon" />\n' +
|
||||
' </a>\n' +
|
||||
' <span data-ko-display="inVisible" data-ko-wrap="false" style="display: none"> </span>\n' +
|
||||
' <a data-ko-display="inVisible" href="" style="-ko-attr-href: @inUrl; display: none">\n' +
|
||||
' <img src="[URL_BASE]/public/mosaico/img/social_def/linkedin_bw_ok.png" alt="Linkedin" border="0" class="socialIcon" />\n' +
|
||||
' <img src="img/social_def/linkedin_bw_ok.png" alt="Linkedin" border="0" class="socialIcon" />\n' +
|
||||
' </a>\n' +
|
||||
' <span data-ko-display="flVisible" data-ko-wrap="false" style="display: none"> </span>\n' +
|
||||
' <a data-ko-display="flVisible" href="" style="-ko-attr-href: @flUrl; display: none">\n' +
|
||||
' <img src="[URL_BASE]/public/mosaico/img/social_def/flickr_bw_ok.png" alt="Flickr" border="0" class="socialIcon" />\n' +
|
||||
' <img src="img/social_def/flickr_bw_ok.png" alt="Flickr" border="0" class="socialIcon" />\n' +
|
||||
' </a>\n' +
|
||||
' <span data-ko-display="viVisible" data-ko-wrap="false" style="display: none"> </span>\n' +
|
||||
' <a data-ko-display="viVisible" href="" style="-ko-attr-href: @viUrl; display: none">\n' +
|
||||
' <img src="[URL_BASE]/public/mosaico/img/social_def/vimeo_bw_ok.png" alt="Vimeo" border="0" class="socialIcon" />\n' +
|
||||
' <img src="img/social_def/vimeo_bw_ok.png" alt="Vimeo" border="0" class="socialIcon" />\n' +
|
||||
' </a>\n' +
|
||||
' <span data-ko-display="instVisible" data-ko-wrap="false" style="display: none"> </span>\n' +
|
||||
' <a data-ko-display="instVisible" href="" style="-ko-attr-href: @instUrl; display: none">\n' +
|
||||
' <img src="[URL_BASE]/public/mosaico/img/social_def/instagram_bw_ok.png" alt="Instagram" border="0" class="socialIcon" />\n' +
|
||||
' <img src="img/social_def/instagram_bw_ok.png" alt="Instagram" border="0" class="socialIcon" />\n' +
|
||||
' </a>\n' +
|
||||
' <span data-ko-display="youVisible" data-ko-wrap="false" style="display: none"> </span>\n' +
|
||||
' <a data-ko-display="youVisible" href="" style="-ko-attr-href: @youUrl; display: none">\n' +
|
||||
' <img src="[URL_BASE]/public/mosaico/img/social_def/youtube_bw_ok.png" alt="Youtube" border="0" class="socialIcon" />\n' +
|
||||
' <img src="img/social_def/youtube_bw_ok.png" alt="Youtube" border="0" class="socialIcon" />\n' +
|
||||
' </a>\n' +
|
||||
' </td>\n' +
|
||||
' </tr>\n' +
|
||||
|
@ -1516,7 +1516,7 @@ const versafix = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
|
|||
'\n' +
|
||||
' <tr data-ko-display="_root_.sponsor.visible" style="display: none;text-align:center">\n' +
|
||||
' <td align="center">\n' +
|
||||
' <a href="http://www.void.it" target="_blank" rel="noreferrer"><img border="0" hspace="0" vspace="0" src="[URL_BASE]/public/mosaico/img/sponsor.gif" alt="sponsor"\n' +
|
||||
' <a href="http://www.void.it" target="_blank" rel="noreferrer"><img border="0" hspace="0" vspace="0" src="img/sponsor.gif" alt="sponsor"\n' +
|
||||
' style="Margin:auto;display:inline !important;" /></a>\n' +
|
||||
' </td>\n' +
|
||||
' </tr>\n' +
|
||||
|
|
22
shared/templates.js
Normal file
22
shared/templates.js
Normal file
|
@ -0,0 +1,22 @@
|
|||
'use strict';
|
||||
|
||||
function base(text, baseUrl) {
|
||||
if (baseUrl.endsWith('/')) {
|
||||
baseUrl = baseUrl.substring(0, baseUrl.length - 1);
|
||||
}
|
||||
|
||||
return text.split('[URL_BASE]').join(baseUrl);
|
||||
}
|
||||
|
||||
function unbase(text, baseUrl) {
|
||||
if (baseUrl.endsWith('/')) {
|
||||
baseUrl = baseUrl.substring(0, baseUrl.length - 1);
|
||||
}
|
||||
|
||||
return text.split(baseUrl).join('[URL_BASE]');
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
base,
|
||||
unbase
|
||||
};
|
|
@ -25,8 +25,9 @@
|
|||
window.mailtrainConfig = {{{mailtrainConfig}}};
|
||||
</script>
|
||||
|
||||
<script src="/mailtrain/common.js"></script>
|
||||
<script src="/mailtrain/root.js"></script>
|
||||
{{#each scriptFiles}}
|
||||
<script src="{{this}}"></script>
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
</head>
|
||||
|
||||
|
|
|
@ -9,22 +9,22 @@
|
|||
|
||||
<title>Mailtrain</title>
|
||||
|
||||
<script src="/public/mosaico/vendor/jquery.min.js"></script>
|
||||
<script src="/public/mosaico/vendor/jquery-migrate.min.js"></script>
|
||||
<script src="/public/mosaico/vendor/knockout.js"></script>
|
||||
<script src="/public/mosaico/vendor/jquery-ui.min.js"></script>
|
||||
<script src="/public/mosaico/vendor/jquery.ui.touch-punch.min.js"></script>
|
||||
<script src="/public/mosaico/vendor/load-image.all.min.js"></script>
|
||||
<script src="/public/mosaico/vendor/canvas-to-blob.min.js"></script>
|
||||
<script src="/public/mosaico/vendor/jquery.iframe-transport.js"></script>
|
||||
<script src="/public/mosaico/vendor/jquery.fileupload.js"></script>
|
||||
<script src="/public/mosaico/vendor/jquery.fileupload-process.js"></script>
|
||||
<script src="/public/mosaico/vendor/jquery.fileupload-image.js"></script>
|
||||
<script src="/public/mosaico/vendor/jquery.fileupload-validate.js"></script>
|
||||
<script src="/public/mosaico/vendor/knockout-jqueryui.min.js"></script>
|
||||
<script src="/public/mosaico/vendor/tinymce.min.js"></script>
|
||||
<script src="{{mosaicoPublicPath}}/vendor/jquery.min.js"></script>
|
||||
<script src="{{mosaicoPublicPath}}/vendor/jquery-migrate.min.js"></script>
|
||||
<script src="{{mosaicoPublicPath}}/vendor/knockout.js"></script>
|
||||
<script src="{{mosaicoPublicPath}}/vendor/jquery-ui.min.js"></script>
|
||||
<script src="{{mosaicoPublicPath}}/vendor/jquery.ui.touch-punch.min.js"></script>
|
||||
<script src="{{mosaicoPublicPath}}/vendor/load-image.all.min.js"></script>
|
||||
<script src="{{mosaicoPublicPath}}/vendor/canvas-to-blob.min.js"></script>
|
||||
<script src="{{mosaicoPublicPath}}/vendor/jquery.iframe-transport.js"></script>
|
||||
<script src="{{mosaicoPublicPath}}/vendor/jquery.fileupload.js"></script>
|
||||
<script src="{{mosaicoPublicPath}}/vendor/jquery.fileupload-process.js"></script>
|
||||
<script src="{{mosaicoPublicPath}}/vendor/jquery.fileupload-image.js"></script>
|
||||
<script src="{{mosaicoPublicPath}}/vendor/jquery.fileupload-validate.js"></script>
|
||||
<script src="{{mosaicoPublicPath}}/vendor/knockout-jqueryui.min.js"></script>
|
||||
<script src="{{mosaicoPublicPath}}/vendor/tinymce.min.js"></script>
|
||||
|
||||
<script src="/public/mosaico/mosaico.min.js?v=0.16"></script>
|
||||
<script src="{{mosaicoPublicPath}}/mosaico.min.js?v=0.16"></script>
|
||||
|
||||
{{#if languageStrings}}<script> window.mosaicoLanguageStrings = {{{languageStrings}}}; </script>{{/if}}
|
||||
<script> window.mosaicoPlugins = []; </script>
|
||||
|
@ -41,11 +41,12 @@
|
|||
window.mailtrainConfig = {{{mailtrainConfig}}};
|
||||
</script>
|
||||
|
||||
<script src="/mailtrain/common.js"></script>
|
||||
<script src="/mailtrain/mosaico.js"></script>
|
||||
{{#each scriptFiles}}
|
||||
<script src="{{this}}"></script>
|
||||
{{/each}}
|
||||
|
||||
<link rel="stylesheet" href="/public/mosaico/mosaico-material.min.css?v=0.10" />
|
||||
<link rel="stylesheet" href="/public/mosaico/vendor/notoregular/stylesheet.css" />
|
||||
<link rel="stylesheet" href="{{mosaicoPublicPath}}/mosaico-material.min.css?v=0.10" />
|
||||
<link rel="stylesheet" href="{{mosaicoPublicPath}}/vendor/notoregular/stylesheet.css" />
|
||||
</head>
|
||||
<body class="mo-standalone">
|
||||
{{{body}}}
|
||||
|
|
Loading…
Reference in a new issue