- }
-
- if (format === 'inline') {
- return (
-
- {labelBlock}{input}
- {helpBlock}
- {validationBlock}
-
- );
- } else {
- return (
-
- {labelBlock}
-
- {input}
- {helpBlock}
- {validationBlock}
-
-
- );
- }
-}
-
-@withComponentMixins([
- withFormStateOwner
-])
-class StaticField extends Component {
- static propTypes = {
- id: PropTypes.string.isRequired,
- label: PropTypes.string,
- help: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
- className: PropTypes.string,
- format: PropTypes.string,
- withValidation: PropTypes.bool
- }
-
- render() {
- const props = this.props;
- const owner = this.getFormStateOwner();
- const id = this.props.id;
- const htmlId = 'form_' + id;
-
- let className = 'form-control';
-
- if (props.withValidation) {
- className = owner.addFormValidationClass(className, id);
- }
-
- if (props.className) {
- className += ' ' + props.className;
- }
-
- return wrapInput(props.withValidation ? id : null, htmlId, owner, props.format, '', props.label, props.help,
-
{props.children}
- );
- }
-}
-
-@withComponentMixins([
- withFormStateOwner
-])
-class InputField extends Component {
- static propTypes = {
- id: PropTypes.string.isRequired,
- label: PropTypes.string,
- placeholder: PropTypes.string,
- type: PropTypes.string,
- help: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
- format: PropTypes.string
- }
-
- static defaultProps = {
- type: 'text'
- }
-
- render() {
- const props = this.props;
- const owner = this.getFormStateOwner();
- const id = this.props.id;
- const htmlId = 'form_' + id;
-
- let type = 'text';
- if (props.type === 'password') {
- type = 'password';
- } else if (props.type === 'hidden') {
- type = 'hidden';
- }
-
- const className = owner.addFormValidationClass('form-control', id);
-
- /* This is for debugging purposes when React reports that InputField is uncontrolled
- const value = owner.getFormValue(id);
- if (value === null || value === undefined) console.log(`Warning: InputField ${id} is ${value}`);
- */
- const value = owner.getFormValue(id);
- if (value === null || value === undefined) console.log(`Warning: InputField ${id} is ${value}`);
-
- return wrapInput(id, htmlId, owner, props.format, '', props.label, props.help,
-
owner.updateFormValue(id, evt.target.value)}/>
- );
- }
-}
-
-class CheckBox extends Component {
- static propTypes = {
- id: PropTypes.string.isRequired,
- text: PropTypes.string,
- label: PropTypes.string,
- help: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
- format: PropTypes.string,
- className: PropTypes.string
- }
-
- render() {
- return (
-
- {
- owner => {
- const props = this.props;
- const id = this.props.id;
- const htmlId = 'form_' + id;
-
- const inputClassName = owner.addFormValidationClass('form-check-input', id);
-
- return wrapInput(id, htmlId, owner, props.format, '', props.label, props.help,
-
- owner.updateFormValue(id, !owner.getFormValue(id))}/>
- {props.text}
-
- );
- }
-
- }
-
- );
-
- }
-}
-
-@withComponentMixins([
- withFormStateOwner
-])
-class CheckBoxGroup extends Component {
- static propTypes = {
- id: PropTypes.string.isRequired,
- label: PropTypes.string.isRequired,
- help: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
- options: PropTypes.array,
- className: PropTypes.string,
- format: PropTypes.string
- }
-
- onChange(key) {
- const id = this.props.id;
- const owner = this.getFormStateOwner();
- const existingSelection = owner.getFormValue(id);
-
- let newSelection;
- if (existingSelection.includes(key)) {
- newSelection = existingSelection.filter(x => x !== key);
- } else {
- newSelection = [key, ...existingSelection];
- }
- owner.updateFormValue(id, newSelection.sort());
- }
-
- render() {
- const props = this.props;
-
- const owner = this.getFormStateOwner();
- const id = this.props.id;
- const htmlId = 'form_' + id;
-
- const selection = owner.getFormValue(id);
-
- const options = [];
- for (const option of props.options) {
- const optClassName = owner.addFormValidationClass('form-check-input', id);
- const optId = htmlId + '_' + option.key;
-
- let number = options.push(
-
- this.onChange(option.key)}/>
- {option.label}
-
- );
- }
-
- let className = 'form-control';
- if (props.className) {
- className += ' ' + props.className;
- }
-
- return wrapInput(id, htmlId, owner, props.format, '', props.label, props.help,
-
- {options}
-
- );
- }
-}
-
-@withComponentMixins([
- withFormStateOwner
-])
-class RadioGroup extends Component {
- static propTypes = {
- id: PropTypes.string.isRequired,
- label: PropTypes.string.isRequired,
- help: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
- options: PropTypes.array,
- className: PropTypes.string,
- format: PropTypes.string
- }
-
- render() {
- const props = this.props;
-
- const owner = this.getFormStateOwner();
- const id = this.props.id;
- const htmlId = 'form_' + id;
-
- const value = owner.getFormValue(id);
-
- const options = [];
- for (const option of props.options) {
- const optClassName = owner.addFormValidationClass('form-check-input', id);
- const optId = htmlId + '_' + option.key;
-
- let number = options.push(
-
- owner.updateFormValue(id, option.key)}/>
- {option.label}
-
- );
- }
-
- let className = 'form-control';
- if (props.className) {
- className += ' ' + props.className;
- }
-
- return wrapInput(id, htmlId, owner, props.format, '', props.label, props.help,
-
- {options}
-
- );
- }
-}
-
-@withComponentMixins([
- withFormStateOwner
-])
-class TextArea extends Component {
- constructor() {
- super();
- this.onChange = evt => {
- const id = this.props.id;
- const owner = this.getFormStateOwner();
- owner.updateFormValue(id, evt.target.value);
- }
- }
-
- static propTypes = {
- id: PropTypes.string.isRequired,
- label: PropTypes.string.isRequired,
- placeholder: PropTypes.string,
- help: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
- format: PropTypes.string,
- className: PropTypes.string
- }
-
- render() {
- const props = this.props;
- const owner = this.getFormStateOwner();
- const id = props.id;
- const htmlId = 'form_' + id;
- const className = owner.addFormValidationClass('form-control ' + (props.className || '') , id);
-
- return wrapInput(id, htmlId, owner, props.format, '', props.label, props.help,
-
- );
- }
-}
-
-
-@withComponentMixins([
- withFormStateOwner
-])
-class ColorPicker extends Component {
- constructor(props) {
- super(props);
-
- this.state = {
- opened: false
- };
- }
-
- static propTypes = {
- id: PropTypes.string.isRequired,
- label: PropTypes.string.isRequired,
- help: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
- }
-
- toggle() {
- this.setState({
- opened: !this.state.opened
- });
- }
-
- selected(value) {
- const owner = this.getFormStateOwner();
- const id = this.props.id;
-
- this.setState({
- opened: false
- });
-
- owner.updateFormValue(id, value.rgb);
- }
-
- render() {
- const props = this.props;
- const owner = this.getFormStateOwner();
- const id = this.props.id;
- const htmlId = 'form_' + id;
- const t = props.t;
- const color = owner.getFormValue(id);
-
- return wrapInput(id, htmlId, owner, props.format, '', props.label, props.help,
-
-
- {this.state.opened &&
-
-
-
- }
-
- );
- }
-}
-
-@withComponentMixins([
- withTranslation,
- withFormStateOwner
-])
-class DatePicker extends Component {
- constructor(props) {
- super(props);
-
- this.state = {
- opened: false
- };
- }
-
- static propTypes = {
- id: PropTypes.string.isRequired,
- label: PropTypes.string.isRequired,
- help: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
- format: PropTypes.string,
- birthday: PropTypes.bool,
- dateFormat: PropTypes.string,
- formatDate: PropTypes.func,
- parseDate: PropTypes.func
- }
-
- static defaultProps = {
- dateFormat: DateFormat.INTL
- }
-
- async toggleDayPicker() {
- this.setState({
- opened: !this.state.opened
- });
- }
-
- daySelected(date) {
- const owner = this.getFormStateOwner();
- const id = this.props.id;
- const props = this.props;
-
- if (props.formatDate) {
- owner.updateFormValue(id, props.formatDate(date));
- } else {
- owner.updateFormValue(id, props.birthday ? formatBirthday(props.dateFormat, date) : formatDate(props.dateFormat, date));
- }
-
- this.setState({
- opened: false
- });
- }
-
- render() {
- const props = this.props;
- const owner = this.getFormStateOwner();
- const id = this.props.id;
- const htmlId = 'form_' + id;
- const t = props.t;
-
- function BirthdayPickerCaption({ date, localeUtils, onChange }) {
- const months = localeUtils.getMonths();
- return (
-
- {months[date.getMonth()]}
-
- );
- }
-
- let selectedDate, captionElement, fromMonth, toMonth, placeholder;
- const selectedDateStr = owner.getFormValue(id) || '';
- if (props.birthday) {
- if (props.parseDate) {
- selectedDate = props.parseDate(selectedDateStr);
- if (selectedDate) {
- selectedDate = moment(selectedDate).set('year', birthdayYear).toDate();
- }
- } else {
- selectedDate = parseBirthday(props.dateFormat, selectedDateStr);
- }
-
- if (!selectedDate) {
- selectedDate = moment().set('year', birthdayYear).toDate();
- }
-
- captionElement =
;
- fromMonth = new Date(birthdayYear, 0, 1);
- toMonth = new Date(birthdayYear, 11, 31);
- placeholder = getBirthdayFormatString(props.dateFormat);
-
- } else {
- if (props.parseDate) {
- selectedDate = props.parseDate(selectedDateStr);
- } else {
- selectedDate = parseDate(props.dateFormat, selectedDateStr);
- }
-
- if (!selectedDate) {
- selectedDate = moment().toDate();
- }
-
- placeholder = getDateFormatString(props.dateFormat);
- }
-
- const className = owner.addFormValidationClass('form-control', id);
-
- return wrapInput(id, htmlId, owner, props.format, '', props.label, props.help,
- <>
-
-
owner.updateFormValue(id, evt.target.value)}/>
-
-
-
-
- {this.state.opened &&
-
- this.daySelected(date)}
- selectedDays={selectedDate}
- initialMonth={selectedDate}
- fromMonth={fromMonth}
- toMonth={toMonth}
- captionElement={captionElement}
- />
-
- }
- >
- );
- }
-}
-
-
-@withComponentMixins([
- withFormStateOwner
-])
-class Dropdown extends Component {
- static propTypes = {
- id: PropTypes.string.isRequired,
- label: PropTypes.string.isRequired,
- help: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
- options: PropTypes.array,
- className: PropTypes.string,
- format: PropTypes.string,
- disabled: PropTypes.bool
- }
-
- render() {
- const props = this.props;
-
- const owner = this.getFormStateOwner();
- const id = this.props.id;
- const htmlId = 'form_' + id;
- const options = [];
-
- if (this.props.options) {
- for (const optOrGrp of props.options) {
- if (optOrGrp.options) {
- options.push(
-
- {optOrGrp.options.map(opt => {opt.label} )}
-
- )
- } else {
- options.push(
{optOrGrp.label} )
- }
- }
- }
-
- const className = owner.addFormValidationClass('form-control ' + (props.className || '') , id);
-
- return wrapInput(id, htmlId, owner, props.format, '', props.label, props.help,
-
owner.updateFormValue(id, evt.target.value)} disabled={props.disabled}>
- {options}
-
- );
- }
-}
-
-@withComponentMixins([
- withFormStateOwner
-])
-class AlignedRow extends Component {
- static propTypes = {
- className: PropTypes.string,
- label: PropTypes.string,
- htmlId: PropTypes.string,
- format: PropTypes.string
- }
-
- static defaultProps = {
- className: ''
- }
-
- render() {
- const props = this.props;
- const owner = this.getFormStateOwner();
-
- return wrapInput(null, props.htmlId, owner, props.format, props.className, props.label, null, this.props.children);
- }
-}
-
-
-class ButtonRow extends Component {
- static propTypes = {
- className: PropTypes.string,
- format: PropTypes.string
- }
-
- render() {
- let className = styles.buttonRow;
- if (this.props.className) {
- className += ' ' + this.props.className;
- }
-
- return (
-
{this.props.children}
- );
- }
-}
-
-
-@withComponentMixins([
- withFormStateOwner
-])
-class TreeTableSelect extends Component {
- static propTypes = {
- id: PropTypes.string.isRequired,
- label: PropTypes.string,
- dataUrl: PropTypes.string,
- data: PropTypes.array,
- help: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
- format: PropTypes.string,
- }
-
- async onSelectionChangedAsync(sel) {
- const owner = this.getFormStateOwner();
- owner.updateFormValue(this.props.id, sel);
- }
-
- render() {
- const props = this.props;
- const owner = this.getFormStateOwner();
- const id = this.props.id;
- const htmlId = 'form_' + id;
-
- const className = owner.addFormValidationClass('' , id);
-
- return wrapInput(id, htmlId, owner, props.format, '', props.label, props.help,
-
- );
- }
-}
-
-@withComponentMixins([
- withTranslation,
- withFormStateOwner
-], ['refresh'])
-class TableSelect extends Component {
- constructor(props) {
- super(props);
-
- this.state = {
- selectedLabel: '',
- open: false
- };
- }
-
- static propTypes = {
- dataUrl: PropTypes.string,
- data: PropTypes.array,
- columns: PropTypes.array,
- selectionKeyIndex: PropTypes.number,
- selectionLabelIndex: PropTypes.number,
- selectionAsArray: PropTypes.bool,
- selectMode: PropTypes.number,
- withHeader: PropTypes.bool,
- dropdown: PropTypes.bool,
-
- id: PropTypes.string.isRequired,
- label: PropTypes.string,
- help: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
- format: PropTypes.string,
- disabled: PropTypes.bool,
-
- pageLength: PropTypes.number
- }
-
- static defaultProps = {
- selectMode: TableSelectMode.SINGLE,
- selectionLabelIndex: 0,
- pageLength: 10
- }
-
- async onSelectionChangedAsync(sel, data) {
- if (this.props.selectMode === TableSelectMode.SINGLE && this.props.dropdown) {
- this.setState({
- open: false
- });
- }
-
- const owner = this.getFormStateOwner();
- owner.updateFormValue(this.props.id, sel);
- }
-
- async onSelectionDataAsync(sel, data) {
- if (this.props.dropdown) {
- let label;
-
- if (!data) {
- label = '';
- } else if (this.props.selectMode === TableSelectMode.SINGLE && !this.props.selectionAsArray) {
- label = data[this.props.selectionLabelIndex];
- } else {
- label = data.map(entry => entry[this.props.selectionLabelIndex]).join('; ');
- }
-
- this.setState({
- selectedLabel: label
- });
- }
- }
-
- async toggleOpen() {
- this.setState({
- open: !this.state.open
- });
- }
-
- refresh() {
- this.table.refresh();
- }
-
- render() {
- const props = this.props;
- const owner = this.getFormStateOwner();
- const id = this.props.id;
- const htmlId = 'form_' + id;
- const t = props.t;
-
- if (props.dropdown) {
- const className = owner.addFormValidationClass('form-control' , id);
-
- return wrapInput(id, htmlId, owner, props.format, '', props.label, props.help,
-
-
-
- {!props.disabled &&
-
-
-
- }
-
-
-
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}/>
-
-
- );
- } else {
- return wrapInput(id, htmlId, owner, props.format, '', props.label, props.help,
-
-
-
this.table = node} data={props.data} dataUrl={props.dataUrl} columns={props.columns} pageLength={props.pageLength} selectMode={props.selectMode} selectionAsArray={this.props.selectionAsArray} withHeader={props.withHeader} selectionKeyIndex={props.selectionKeyIndex} selection={owner.getFormValue(id)} onSelectionChangedAsync={::this.onSelectionChangedAsync}/>
-
-
- );
- }
- }
-}
-
-
-@withComponentMixins([
- withFormStateOwner
-])
-class ACEEditor extends Component {
- static propTypes = {
- id: PropTypes.string.isRequired,
- label: PropTypes.string,
- help: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
- height: PropTypes.string,
- mode: PropTypes.string,
- format: PropTypes.string
- }
-
- render() {
- const props = this.props;
- const owner = this.getFormStateOwner();
- const id = this.props.id;
- const htmlId = 'form_' + id;
-
- return wrapInput(id, htmlId, owner, props.format, '', props.label, props.help,
- owner.updateFormValue(id, data)}
- fontSize={12}
- width="100%"
- height={props.height}
- showPrintMargin={false}
- value={owner.getFormValue(id)}
- tabSize={2}
- setOptions={{useWorker: false}} // This disables syntax check because it does not always work well (e.g. in case of JS code in report templates)
- />
- );
- }
-}
-
-
-const withForm = createComponentMixin({
- decoratorFn: (TargetClass, InnerClass) => {
- const proto = InnerClass.prototype;
-
- const cleanFormState = Immutable.Map({
- state: FormState.Loading,
- isValidationShown: false,
- isDisabled: false,
- statusMessageText: '',
- data: Immutable.Map(),
- savedData: Immutable.Map(),
- isServerValidationRunning: false
- });
-
- const getSaveData = (self, formStateData) => {
- let data = formStateData.map(attr => attr.get('value')).toJS();
-
- if (self.submitFormValuesMutator) {
- const newData = self.submitFormValuesMutator(data, false);
- if (newData !== undefined) {
- data = newData;
- }
- }
-
- return data;
- };
-
- // formValidateResolve is called by "validateForm" once client receives validation response from server that does not
- // trigger another server validation
- let formValidateResolve = null;
-
- function scheduleValidateForm(self) {
- setTimeout(() => {
- self.setState(previousState => ({
- formState: previousState.formState.withMutations(mutState => {
- validateFormState(self, mutState);
- })
- }));
- }, 0);
- }
-
- function validateFormState(self, mutState) {
- const settings = self.state.formSettings;
-
- if (!mutState.get('isServerValidationRunning') && settings.serverValidation) {
- const payload = {};
- let payloadNotEmpty = false;
-
- for (const attr of settings.serverValidation.extra || []) {
- if (typeof attr === 'string') {
- payload[attr] = mutState.getIn(['data', attr, 'value']);
- } else {
- const data = mutState.get('data').map(attr => attr.get('value')).toJS();
- payload[attr.key] = attr.data(data);
- }
- }
-
- for (const attr of settings.serverValidation.changed) {
- const currValue = mutState.getIn(['data', attr, 'value']);
- const serverValue = mutState.getIn(['data', attr, 'serverValue']);
-
- // This really assumes that all form values are preinitialized (i.e. not undef)
- if (currValue !== serverValue) {
- mutState.setIn(['data', attr, 'serverValidated'], false);
- payload[attr] = currValue;
- payloadNotEmpty = true;
- }
- }
-
- if (payloadNotEmpty) {
- mutState.set('isServerValidationRunning', true);
-
- axios.post(getUrl(settings.serverValidation.url), payload)
- .then(response => {
-
- if (self.isComponentMounted()) {
- self.setState(previousState => ({
- formState: previousState.formState.withMutations(mutState => {
- mutState.set('isServerValidationRunning', false);
-
- mutState.update('data', stateData => stateData.withMutations(mutStateData => {
- for (const attr in payload) {
- mutStateData.setIn([attr, 'serverValue'], payload[attr]);
-
- if (payload[attr] === mutState.getIn(['data', attr, 'value'])) {
- mutStateData.setIn([attr, 'serverValidated'], true);
- mutStateData.setIn([attr, 'serverValidation'], response.data[attr] || true);
- }
- }
- }));
- })
- }));
-
- scheduleValidateForm(self);
- }
- })
- .catch(error => {
- if (self.isComponentMounted()) {
- console.log('Error in "validateFormState": ' + error);
-
- self.setState(previousState => ({
- formState: previousState.formState.set('isServerValidationRunning', false)
- }));
-
- // TODO: It might be good not to give up immediatelly, but retry a couple of times
- // scheduleValidateForm(self);
- }
- });
- } else {
- if (formValidateResolve) {
- const resolve = formValidateResolve;
- formValidateResolve = null;
- resolve();
- }
- }
- }
-
- if (self.localValidateFormValues) {
- mutState.update('data', stateData => stateData.withMutations(mutStateData => {
- self.localValidateFormValues(mutStateData);
- }));
- }
- }
-
- const previousComponentDidMount = proto.componentDidMount;
- proto.componentDidMount = function () {
- this._isComponentMounted = true;
- if (previousComponentDidMount) {
- previousComponentDidMount.apply(this);
- }
- };
-
- const previousComponentWillUnmount = proto.componentWillUnmount;
- proto.componentWillUnmount = function () {
- this._isComponentMounted = false;
- if (previousComponentWillUnmount) {
- previousComponentDidMount.apply(this);
- }
- };
-
- proto.isComponentMounted = function () {
- return !!this._isComponentMounted;
- }
-
- proto.initForm = function (settings) {
- const state = this.state || {};
- state.formState = cleanFormState;
- state.formSettings = {
- leaveConfirmation: true,
- ...(settings || {})
- };
- this.state = state;
- };
-
- proto.resetFormState = function () {
- this.setState({
- formState: cleanFormState
- });
- };
-
- proto.getFormValuesFromEntity = function (entity) {
- const settings = this.state.formSettings;
- const data = Object.assign({}, entity);
-
- data.originalHash = data.hash;
- delete data.hash;
-
- if (this.getFormValuesMutator) {
- this.getFormValuesMutator(data, this.getFormValues());
- }
-
- this.populateFormValues(data);
- };
-
- proto.getFormValuesFromURL = async function (url) {
- const settings = this.state.formSettings;
- setTimeout(() => {
- this.setState(previousState => {
- if (previousState.formState.get('state') === FormState.Loading) {
- return {
- formState: previousState.formState.set('state', FormState.LoadingWithNotice)
- };
- }
- });
- }, 500);
-
- const response = await axios.get(getUrl(url));
-
- let data = response.data;
-
- data.originalHash = data.hash;
- delete data.hash;
-
- if (this.getFormValuesMutator) {
- const newData = this.getFormValuesMutator(data, this.getFormValues());
-
- if (newData !== undefined) {
- data = newData;
- }
- }
-
- this.populateFormValues(data);
- };
-
- proto.validateAndSendFormValuesToURL = async function (method, url) {
- const settings = this.state.formSettings;
- await this.waitForFormServerValidated();
-
- if (this.isFormWithoutErrors()) {
- if (settings.getPreSubmitUpdater) {
- const preSubmitUpdater = await settings.getPreSubmitUpdater();
-
- await new Promise((resolve, reject) => {
- this.setState(previousState => ({
- formState: previousState.formState.withMutations(mutState => {
- mutState.update('data', stateData => stateData.withMutations(preSubmitUpdater));
- })
- }), resolve);
- });
- }
-
- let data = this.getFormValues();
-
- if (this.submitFormValuesMutator) {
- const newData = this.submitFormValuesMutator(data, true);
- if (newData !== undefined) {
- data = newData;
- }
- }
-
- const response = await axios.method(method, getUrl(url), data);
-
- if (settings.leaveConfirmation) {
- await new Promise((resolve, reject) => {
- this.setState(previousState => ({
- formState: previousState.formState.set('savedData', getSaveData(this, previousState.formState.get('data')))
- }), resolve);
- });
- }
-
- return response.data || true;
-
- } else {
- this.showFormValidation();
- return false;
- }
- };
-
-
- proto.populateFormValues = function (data) {
- const settings = this.state.formSettings;
-
- this.setState(previousState => ({
- formState: previousState.formState.withMutations(mutState => {
- mutState.set('state', FormState.Ready);
-
- mutState.update('data', stateData => stateData.withMutations(mutStateData => {
- for (const key in data) {
- mutStateData.set(key, Immutable.Map({
- value: data[key]
- }));
- }
- }));
-
- if (settings.leaveConfirmation) {
- mutState.set('savedData', getSaveData(this, mutState.get('data')));
- }
-
- validateFormState(this, mutState);
- })
- }));
- };
-
- proto.waitForFormServerValidated = async function () {
- if (!this.isFormServerValidated()) {
- await new Promise(resolve => {
- formValidateResolve = resolve;
- });
- }
- };
-
- proto.scheduleFormRevalidate = function () {
- scheduleValidateForm(this);
- };
-
- proto.updateForm = function (mutator) {
- this.setState(previousState => {
- const onChangeBeforeValidationCallback = this.state.formSettings.onChangeBeforeValidation || {};
-
- const formState = previousState.formState.withMutations(mutState => {
- mutState.update('data', stateData => stateData.withMutations(mutStateData => {
- mutator(mutStateData);
-
- if (typeof onChangeBeforeValidationCallback === 'object') {
- for (const key in onChangeBeforeValidationCallback) {
- const oldValue = previousState.formState.getIn(['data', key, 'value']);
- const newValue = mutStateData.getIn([key, 'value']);
- onChangeBeforeValidationCallback[key](mutStateData, key, oldValue, newValue);
- }
- } else {
- onChangeBeforeValidationCallback(mutStateData);
- }
- }));
-
- validateFormState(this, mutState);
- });
-
- let newState = {
- formState
- };
-
-
- const onChangeCallback = this.state.formSettings.onChange || {};
-
- if (typeof onChangeCallback === 'object') {
- for (const key in onChangeCallback) {
- const oldValue = previousState.formState.getIn(['data', key, 'value']);
- const newValue = formState.getIn(['data', key, 'value']);
- onChangeCallback[key](newState, key, oldValue, newValue);
- }
- } else {
- onChangeCallback(newState);
- }
-
- return newState;
- });
- };
-
- proto.updateFormValue = function (key, value) {
- this.setState(previousState => {
- const oldValue = previousState.formState.getIn(['data', key, 'value']);
-
- const onChangeBeforeValidationCallback = this.state.formSettings.onChangeBeforeValidation || {};
-
- const formState = previousState.formState.withMutations(mutState => {
- mutState.update('data', stateData => stateData.withMutations(mutStateData => {
- mutStateData.setIn([key, 'value'], value);
-
- if (typeof onChangeBeforeValidationCallback === 'object') {
- if (onChangeBeforeValidationCallback[key]) {
- onChangeBeforeValidationCallback[key](mutStateData, key, oldValue, value);
- }
- } else {
- onChangeBeforeValidationCallback(mutStateData, key, oldValue, value);
- }
- }));
-
- validateFormState(this, mutState);
- });
-
- let newState = {
- formState
- };
-
-
- const onChangeCallback = this.state.formSettings.onChange || {};
-
- if (typeof onChangeCallback === 'object') {
- if (onChangeCallback[key]) {
- onChangeCallback[key](newState, key, oldValue, value);
- }
- } else {
- onChangeCallback(newState, key, oldValue, value);
- }
-
- return newState;
- });
- };
-
- proto.getFormValue = function (name) {
- return this.state.formState.getIn(['data', name, 'value']);
- };
-
- proto.getFormValues = function (name) {
- if (!this.state || !this.state.formState) return undefined;
- return this.state.formState.get('data').map(attr => attr.get('value')).toJS();
- };
-
- proto.getFormError = function (name) {
- return this.state.formState.getIn(['data', name, 'error']);
- };
-
- proto.isFormWithLoadingNotice = function () {
- return this.state.formState.get('state') === FormState.LoadingWithNotice;
- };
-
- proto.isFormLoading = function () {
- return this.state.formState.get('state') === FormState.Loading || this.state.formState.get('state') === FormState.LoadingWithNotice;
- };
-
- proto.isFormReady = function () {
- return this.state.formState.get('state') === FormState.Ready;
- };
-
- const _isFormChanged = self => {
- const currentData = getSaveData(self, self.state.formState.get('data'));
- const savedData = self.state.formState.get('savedData');
-
- function isDifferent(data1, data2, prefix) {
- if (typeof data1 === 'object' && typeof data2 === 'object' && data1 && data2) {
- const keys = new Set([...Object.keys(data1), ...Object.keys(data2)]);
- for (const key of keys) {
- if (isDifferent(data1[key], data2[key], `${prefix}/${key}`)) {
- return true;
- }
- }
- } else if (data1 !== data2) {
- // console.log(prefix);
- return true;
- }
- return false;
- }
-
- const result = isDifferent(currentData, savedData, '');
-
- return result;
- };
-
- proto.isFormChanged = function () {
- const settings = this.state.formSettings;
-
- if (!settings.leaveConfirmation) return false;
-
- if (settings.getPreSubmitUpdater) {
- // getPreSubmitUpdater is an async function. We cannot do anything async here. So to be on the safe side,
- // we simply assume that the form has been changed.
- return true;
- }
-
- return _isFormChanged(this);
- };
-
- proto.isFormChangedAsync = async function () {
- const settings = this.state.formSettings;
-
- if (!settings.leaveConfirmation) return false;
-
- if (settings.getPreSubmitUpdater) {
- const preSubmitUpdater = await settings.getPreSubmitUpdater();
-
- await new Promise((resolve, reject) => {
- this.setState(previousState => ({
- formState: previousState.formState.withMutations(mutState => {
- mutState.update('data', stateData => stateData.withMutations(preSubmitUpdater));
- })
- }), resolve);
- });
- }
-
- return _isFormChanged(this);
-
- };
-
- proto.isFormValidationShown = function () {
- return this.state.formState.get('isValidationShown');
- };
-
- proto.addFormValidationClass = function (className, name) {
- if (this.isFormValidationShown()) {
- const error = this.getFormError(name);
- if (error) {
- return className + ' is-invalid';
- } else {
- return className + ' is-valid';
- }
- } else {
- return className;
- }
- };
-
- proto.getFormValidationMessage = function (name) {
- if (this.isFormValidationShown()) {
- return this.getFormError(name);
- } else {
- return '';
- }
- };
-
- proto.showFormValidation = function () {
- this.setState(previousState => ({formState: previousState.formState.set('isValidationShown', true)}));
- };
-
- proto.hideFormValidation = function () {
- this.setState(previousState => ({formState: previousState.formState.set('isValidationShown', false)}));
- };
-
- proto.isFormWithoutErrors = function () {
- return !this.state.formState.get('data').find(attr => attr.get('error'));
- };
-
- proto.isFormServerValidated = function () {
- return !this.state.formSettings.serverValidation || this.state.formSettings.serverValidation.changed.every(attr => this.state.formState.getIn(['data', attr, 'serverValidated']));
- };
-
- proto.getFormStatusMessageText = function () {
- return this.state.formState.get('statusMessageText');
- };
-
- proto.getFormStatusMessageSeverity = function () {
- return this.state.formState.get('statusMessageSeverity');
- };
-
- proto.setFormStatusMessage = function (severity, text) {
- this.setState(previousState => ({
- formState: previousState.formState.withMutations(map => {
- map.set('statusMessageText', text);
- map.set('statusMessageSeverity', severity);
- })
- }));
- };
-
- proto.clearFormStatusMessage = function () {
- this.setState(previousState => ({
- formState: previousState.formState.withMutations(map => {
- map.set('statusMessageText', '');
- })
- }));
- };
-
- proto.enableForm = function () {
- this.setState(previousState => ({formState: previousState.formState.set('isDisabled', false)}));
- };
-
- proto.disableForm = function () {
- this.setState(previousState => ({formState: previousState.formState.set('isDisabled', true)}));
- };
-
- proto.isFormDisabled = function () {
- return this.state.formState.get('isDisabled');
- };
-
- proto.formHandleErrors = async function (fn) {
- const t = this.props.t;
- try {
- await fn();
- } catch (error) {
- if (error instanceof interoperableErrors.ChangedError) {
- this.disableForm();
- this.setFormStatusMessage('danger',
-
- {t('yourUpdatesCannotBeSaved')} {' '}
- {t('someoneElseHasIntroducedModificationIn')}
-
- );
- return;
- }
-
- if (error instanceof interoperableErrors.NamespaceNotFoundError) {
- this.disableForm();
- this.setFormStatusMessage('danger',
-
- {t('yourUpdatesCannotBeSaved')} {' '}
- {t('itSeemsThatSomeoneElseHasDeletedThe')}
-
- );
- return;
- }
-
- if (error instanceof interoperableErrors.NotFoundError) {
- this.disableForm();
- this.setFormStatusMessage('danger',
-
- {t('yourUpdatesCannotBeSaved')} {' '}
- {t('itSeemsThatSomeoneElseHasDeletedThe-1')}
-
- );
- return;
- }
-
- throw error;
- }
- };
-
- return {};
- }
-});
-
-function filterData(obj, allowedKeys) {
- const result = {};
- for (const key in obj) {
- if (key === 'originalHash') {
- result[key] = obj[key];
- } else {
- for (const allowedKey of allowedKeys) {
- if ((typeof allowedKey === 'function' && allowedKey(key)) || allowedKey === key) {
- result[key] = obj[key];
- break;
- }
- }
- }
- }
-
- return result;
-}
-
-export {
- withForm,
- Form,
- Fieldset,
- StaticField,
- InputField,
- CheckBox,
- CheckBoxGroup,
- RadioGroup,
- TextArea,
- ColorPicker,
- DatePicker,
- Dropdown,
- AlignedRow,
- ButtonRow,
- Button,
- TreeTableSelect,
- TableSelect,
- TableSelectMode,
- ACEEditor,
- FormSendMethod,
- filterData
-}
diff --git a/client/src/lib/helpers.js b/client/src/lib/helpers.js
deleted file mode 100644
index 8c2f8e27..00000000
--- a/client/src/lib/helpers.js
+++ /dev/null
@@ -1,8 +0,0 @@
-'use strict';
-
-import ellipsize from "ellipsize";
-
-
-export function ellipsizeBreadcrumbLabel(label) {
- return ellipsize(label, 40)
-}
\ No newline at end of file
diff --git a/client/src/lib/i18n.js b/client/src/lib/i18n.js
deleted file mode 100644
index ba39afe7..00000000
--- a/client/src/lib/i18n.js
+++ /dev/null
@@ -1,92 +0,0 @@
-'use strict';
-
-import React from 'react';
-import {I18nextProvider, withNamespaces} from 'react-i18next';
-import i18n from 'i18next';
-import LanguageDetector from 'i18next-browser-languagedetector';
-import mailtrainConfig from 'mailtrainConfig';
-
-import {convertToFake, getLang} from '../../../shared/langs';
-import {createComponentMixin} from "./decorator-helpers";
-
-import lang_en_US_common from "../../../locales/en-US/common";
-import lang_es_ES_common from "../../../locales/es-ES/common";
-import lang_pt_BR_common from "../../../locales/pt-BR/common";
-
-
-const resourcesCommon = {
- 'en-US': lang_en_US_common,
- 'es-ES': lang_es_ES_common,
- 'pt-BR': lang_pt_BR_common,
- 'fk-FK': convertToFake(lang_en_US_common)
-};
-
-const resources = {};
-for (const lng of mailtrainConfig.enabledLanguages) {
- const langDesc = getLang(lng);
- resources[langDesc.longCode] = {
- common: resourcesCommon[langDesc.longCode]
- };
-}
-
-i18n
- .use(LanguageDetector)
- .init({
- resources,
-
- fallbackLng: mailtrainConfig.defaultLanguage,
- defaultNS: 'common',
-
- interpolation: {
- escapeValue: false // not needed for react
- },
-
- react: {
- wait: true
- },
-
- detection: {
- order: ['querystring', 'cookie', 'localStorage', 'navigator'],
- lookupQuerystring: 'locale',
- lookupCookie: 'i18nextLng',
- lookupLocalStorage: 'i18nextLng',
- caches: ['localStorage', 'cookie']
- },
-
- whitelist: mailtrainConfig.enabledLanguages,
- load: 'currentOnly',
-
- debug: false
- });
-
-
-export default i18n;
-
-
-export const TranslationContext = React.createContext(null);
-
-export const withTranslation = createComponentMixin({
- contexts: [{context: TranslationContext, propName: 't'}]
-});
-
-const TranslationContextProvider = withNamespaces()(props => {
- return (
-
- {props.children}
-
- );
-});
-
-export function TranslationRoot(props) {
- return (
-
-
- {props.children}
-
-
- );
-}
-
-export function tMark(key) {
- return key;
-}
diff --git a/client/src/lib/mjml.js b/client/src/lib/mjml.js
deleted file mode 100644
index a721cab8..00000000
--- a/client/src/lib/mjml.js
+++ /dev/null
@@ -1,77 +0,0 @@
-'use strict';
-
-import {isArray, mergeWith} from 'lodash';
-import kebabCase from 'lodash/kebabCase';
-import mjml2html, {BodyComponent, components, defaultSkeleton, dependencies, HeadComponent} from "mjml4-in-browser";
-
-export { BodyComponent, HeadComponent };
-
-const initComponents = {...components};
-const initDependencies = {...dependencies};
-
-
-// MJML uses global state. This class wraps MJML state and provides a custom mjml2html function which sets the right state before calling the original mjml2html
-export class MJML {
- constructor() {
- this.components = initComponents;
- this.dependencies = initDependencies;
- this.headRaw = [];
- }
-
- registerDependencies(dep) {
- function mergeArrays(objValue, srcValue) {
- if (isArray(objValue) && isArray(srcValue)) {
- return objValue.concat(srcValue)
- }
- }
-
- mergeWith(this.dependencies, dep, mergeArrays);
- }
-
- registerComponent(Component) {
- this.components[kebabCase(Component.name)] = Component;
- }
-
- addToHeader(src) {
- this.headRaw.push(src);
- }
-
- mjml2html(mjml) {
- function setObj(obj, src) {
- for (const prop of Object.keys(obj)) {
- delete obj[prop];
- }
-
- Object.assign(obj, src);
- }
-
- const origComponents = {...components};
- const origDependencies = {...dependencies};
-
- setObj(components, this.components);
- setObj(dependencies, this.dependencies);
-
- const res = mjml2html(mjml, {
- skeleton: options => {
- const headRaw = options.headRaw || [];
- options.headRaw = headRaw.concat(this.headRaw);
- return defaultSkeleton(options);
- }
- });
-
- setObj(components, origComponents);
- setObj(dependencies, origDependencies);
-
- return res;
- }
-}
-
-const mjmlInstance = new MJML();
-
-export default function defaultMjml2html(src) {
- return mjmlInstance.mjml2html(src);
-}
-
-
-
-
diff --git a/client/src/lib/modals.js b/client/src/lib/modals.js
deleted file mode 100644
index 76b99a65..00000000
--- a/client/src/lib/modals.js
+++ /dev/null
@@ -1,399 +0,0 @@
-'use strict';
-
-import React, {Component} from 'react';
-import axios, {HTTPMethod} from './axios';
-import {withTranslation} from './i18n';
-import PropTypes from 'prop-types';
-import {Icon, ModalDialog} from "./bootstrap-components";
-import {getUrl} from "./urls";
-import {withPageHelpers} from "./page";
-import styles from './styles.scss';
-import interoperableErrors from '../../../shared/interoperable-errors';
-import {Link} from "react-router-dom";
-import {withComponentMixins} from "./decorator-helpers";
-import {withAsyncErrorHandler} from "./error-handling";
-import ACEEditorRaw from 'react-ace';
-
-@withComponentMixins([
- withTranslation,
- withPageHelpers
-])
-export class RestActionModalDialog extends Component {
- static propTypes = {
- title: PropTypes.string.isRequired,
- message: PropTypes.string.isRequired,
- stateOwner: PropTypes.object,
- visible: PropTypes.bool.isRequired,
- actionMethod: PropTypes.func.isRequired,
- actionUrl: PropTypes.string.isRequired,
- actionData: PropTypes.object,
-
- backUrl: PropTypes.string,
- successUrl: PropTypes.string,
-
- onBack: PropTypes.func,
- onPerformingAction: PropTypes.func,
- onSuccess: PropTypes.func,
-
- actionInProgressMsg: PropTypes.string.isRequired,
- actionDoneMsg: PropTypes.string.isRequired,
-
- onErrorAsync: PropTypes.func
- }
-
- async hideModal(isBack) {
- if (this.props.backUrl) {
- this.navigateTo(this.props.backUrl);
- } else {
- if (isBack) {
- this.props.onBack();
- } else {
- this.props.onPerformingAction();
- }
- }
- }
-
- async performAction() {
- const props = this.props;
- const t = props.t;
- const owner = props.stateOwner;
-
- await this.hideModal(false);
-
- try {
- if (!owner) {
- this.setFlashMessage('info', props.actionInProgressMsg);
- } else {
- owner.disableForm();
- owner.setFormStatusMessage('info', props.actionInProgressMsg);
- }
-
- await axios.method(props.actionMethod, getUrl(props.actionUrl), props.actionData);
-
- if (props.successUrl) {
- this.navigateToWithFlashMessage(props.successUrl, 'success', props.actionDoneMsg);
- } else {
- props.onSuccess();
- this.setFlashMessage('success', props.actionDoneMsg);
- }
- } catch (err) {
- if (props.onErrorAsync) {
- await props.onErrorAsync(err);
- } else {
- throw err;
- }
- }
- }
-
- render() {
- const t = this.props.t;
-
- return (
- await this.hideModal(true)} buttons={[
- { label: t('no'), className: 'btn-primary', onClickAsync: async () => await this.hideModal(true) },
- { label: t('yes'), className: 'btn-danger', onClickAsync: ::this.performAction }
- ]}>
- {this.props.message}
-
- );
- }
-}
-
-const entityTypeLabels = {
- 'namespace': t => t('namespace'),
- 'list': t => t('list'),
- 'customForm': t => t('customForms'),
- 'campaign': t => t('campaign'),
- 'template': t => t('template'),
- 'sendConfiguration': t => t('sendConfiguration'),
- 'report': t => t('report'),
- 'reportTemplate': t => t('reportTemplate'),
- 'mosaicoTemplate': t => t('mosaicoTemplate'),
- 'user': t => t('User')
-};
-
-function _getDependencyErrorMessage(err, t, name) {
- return (
-
- {err.data.dependencies.length > 0 ?
- <>
-
{t('cannoteDeleteNameDueToTheFollowing', {name})}
-
- {err.data.dependencies.map(dep =>
- dep.link ?
- {entityTypeLabels[dep.entityTypeId](t)}: {dep.name}
- : // if no dep.link is present, it means the user has no permission to view the entity, thus only id without the link is shown
- {entityTypeLabels[dep.entityTypeId](t)}: [{dep.id}]
- )}
- {err.data.andMore && {t('andMore')} }
-
- >
- :
-
{t('Cannot delete {{name}} due to hidden dependencies', {name})}
- }
-
- );
-}
-
-
-@withComponentMixins([
- withTranslation,
- withPageHelpers
-])
-export class DeleteModalDialog extends Component {
- constructor(props) {
- super(props);
- const t = props.t;
- }
-
- static propTypes = {
- visible: PropTypes.bool.isRequired,
- stateOwner: PropTypes.object.isRequired,
- deleteUrl: PropTypes.string.isRequired,
- backUrl: PropTypes.string.isRequired,
- successUrl: PropTypes.string.isRequired,
- deletingMsg: PropTypes.string.isRequired,
- deletedMsg: PropTypes.string.isRequired,
- name: PropTypes.string
- }
-
- async onErrorAsync(err) {
- const t = this.props.t;
-
- if (err instanceof interoperableErrors.DependencyPresentError) {
- const owner = this.props.stateOwner;
-
- const name = owner.getFormValue('name');
- this.setFlashMessage('danger', _getDependencyErrorMessage(err, t, name));
-
- window.scrollTo(0, 0); // This is to scroll up because the flash message appears on top and it's quite misleading if the delete fails and the message is not in the viewport
-
- owner.enableForm();
- owner.clearFormStatusMessage();
-
- } else {
- throw err;
- }
- }
-
- render() {
- const t = this.props.t;
- const owner = this.props.stateOwner;
- const name = this.props.name || owner.getFormValue('name') || '';
-
- return
- }
-}
-
-export function tableRestActionDialogInit(owner) {
- owner.tableRestActionDialogData = {};
- owner.state.tableRestActionDialogShown = false;
-}
-
-
-
-function _hide(owner, dontRefresh = false) {
- const refreshTables = owner.tableRestActionDialogData.refreshTables;
-
- owner.setState({ tableRestActionDialogShown: false });
-
- if (!dontRefresh) {
- owner.tableRestActionDialogData = {};
-
- if (refreshTables) {
- refreshTables();
- } else {
- owner.table.refresh();
- }
- } else {
- // _hide is called twice: (1) at performing action, and at (2) success. Here we keep the refreshTables
- // reference till it is really needed in step #2.
- owner.tableRestActionDialogData = { refreshTables };
- }
-}
-
-export function tableAddDeleteButton(actions, owner, perms, deleteUrl, name, deletingMsg, deletedMsg) {
- const t = owner.props.t;
-
- async function onErrorAsync(err) {
- if (err instanceof interoperableErrors.DependencyPresentError) {
- owner.setFlashMessage('danger', _getDependencyErrorMessage(err, t, name));
- window.scrollTo(0, 0); // This is to scroll up because the flash message appears on top and it's quite misleading if the delete fails and the message is not in the viewport
- _hide(owner);
- } else {
- throw err;
- }
- }
-
- if (!perms || perms.includes('delete')) {
- if (owner.tableRestActionDialogData.shown) {
- actions.push({
- label:
- });
- } else {
- actions.push({
- label: ,
- action: () => {
- owner.tableRestActionDialogData = {
- shown: true,
- title: t('confirmDeletion'),
- message:t('areYouSureYouWantToDeleteName?', {name}),
- httpMethod: HTTPMethod.DELETE,
- actionUrl: deleteUrl,
- actionInProgressMsg: deletingMsg,
- actionDoneMsg: deletedMsg,
- onErrorAsync: onErrorAsync
- };
-
- owner.setState({
- tableRestActionDialogShown: true
- });
-
- owner.table.refresh();
- }
- });
- }
- }
-}
-
-export function tableAddRestActionButton(actions, owner, action, button, title, message, actionInProgressMsg, actionDoneMsg, onErrorAsync) {
- const t = owner.props.t;
-
- if (owner.tableRestActionDialogData.shown) {
- actions.push({
- label:
- });
- } else {
- actions.push({
- label: ,
- action: () => {
- owner.tableRestActionDialogData = {
- shown: true,
- title: title,
- message: message,
- httpMethod: action.method,
- actionUrl: action.url,
- actionData: action.data,
- actionInProgressMsg: actionInProgressMsg,
- actionDoneMsg: actionDoneMsg,
- onErrorAsync: onErrorAsync,
- refreshTables: action.refreshTables
- };
-
- owner.setState({
- tableRestActionDialogShown: true
- });
-
- if (action.refreshTables) {
- action.refreshTables();
- } else {
- owner.table.refresh();
- }
- }
- });
- }
-}
-
-export function tableRestActionDialogRender(owner) {
- const data = owner.tableRestActionDialogData;
-
- return _hide(owner)}
- onPerformingAction={() => _hide(owner, true)}
- onSuccess={() => _hide(owner)}
- actionInProgressMsg={data.actionInProgressMsg || ''}
- actionDoneMsg={data.actionDoneMsg || ''}
- onErrorAsync={data.onErrorAsync}
- />
-
-}
-
-
-@withComponentMixins([
- withTranslation
-])
-export class ContentModalDialog extends Component {
- constructor(props) {
- super(props);
- const t = props.t;
-
- this.state = {
- content: null
- };
- }
-
- static propTypes = {
- visible: PropTypes.bool.isRequired,
- title: PropTypes.string.isRequired,
- getContentAsync: PropTypes.func.isRequired,
- onHide: PropTypes.func.isRequired
- }
-
- @withAsyncErrorHandler
- async fetchContent() {
- const content = await this.props.getContentAsync();
- this.setState({
- content
- });
- }
-
- componentDidMount() {
- if (this.props.visible) {
- // noinspection JSIgnoredPromiseFromCall
- this.fetchContent();
- }
- }
-
- componentDidUpdate(prevProps) {
- if (this.props.visible && !prevProps.visible) {
- // noinspection JSIgnoredPromiseFromCall
- this.fetchContent();
- } else if (!this.props.visible && this.state.content !== null) {
- this.setState({
- content: null
- });
- }
- }
-
- render() {
- const t = this.props.t;
-
- return (
- this.props.onHide()}>
- {this.props.visible && this.state.content &&
-
- }
-
- );
- }
-}
diff --git a/client/src/lib/namespace.js b/client/src/lib/namespace.js
deleted file mode 100644
index 2c8aeef7..00000000
--- a/client/src/lib/namespace.js
+++ /dev/null
@@ -1,47 +0,0 @@
-'use strict';
-
-import React, {Component} from 'react';
-import {withTranslation} from './i18n';
-import {TreeTableSelect} from './form';
-import {withComponentMixins} from "./decorator-helpers";
-
-
-@withComponentMixins([
- withTranslation
-])
-export class NamespaceSelect extends Component {
- render() {
- const t = this.props.t;
-
- return (
-
- );
- }
-}
-
-export function validateNamespace(t, state) {
- if (!state.getIn(['namespace', 'value'])) {
- state.setIn(['namespace', 'error'], t('namespaceMustBeSelected'));
- } else {
- state.setIn(['namespace', 'error'], null);
- }
-}
-
-export function getDefaultNamespace(permissions) {
- return permissions.viewUsersNamespace && permissions.createEntityInUsersNamespace ? mailtrainConfig.user.namespace : null;
-}
-
-export function namespaceCheckPermissions(createOperation) {
- return {
- createEntityInUsersNamespace: {
- entityTypeId: 'namespace',
- entityId: mailtrainConfig.user.namespace,
- requiredOperations: [createOperation]
- },
- viewUsersNamespace: {
- entityTypeId: 'namespace',
- entityId: mailtrainConfig.user.namespace,
- requiredOperations: ['view']
- }
- };
-}
diff --git a/client/src/lib/page-common.js b/client/src/lib/page-common.js
deleted file mode 100644
index d0f1493e..00000000
--- a/client/src/lib/page-common.js
+++ /dev/null
@@ -1,448 +0,0 @@
-'use strict';
-
-import React, {Component} from "react";
-import PropTypes from "prop-types";
-import {Redirect, Route, Switch} from "react-router-dom";
-import {withAsyncErrorHandler, withErrorHandling} from "./error-handling";
-import axios from "../lib/axios";
-import {getUrl} from "./urls";
-import {createComponentMixin, withComponentMixins} from "./decorator-helpers";
-import {withTranslation} from "./i18n";
-import shallowEqual from "shallowequal";
-import {checkPermissions} from "./permissions";
-
-async function resolve(route, match, prevResolverState) {
- const resolved = {};
- const permissions = {};
- const resolverState = {
- resolvedByUrl: {},
- permissionsBySig: {}
- };
-
- prevResolverState = prevResolverState || {
- resolvedByUrl: {},
- permissionsBySig: {}
- };
-
- async function processResolve() {
- const keysToGo = new Set(Object.keys(route.resolve));
-
- while (keysToGo.size > 0) {
- const urlsToResolve = [];
- const keysToResolve = [];
-
- for (const key of keysToGo) {
- const resolveEntry = route.resolve[key];
-
- let allDepsSatisfied = true;
- let urlFn = null;
-
- if (typeof resolveEntry === 'function') {
- urlFn = resolveEntry;
-
- } else {
- if (resolveEntry.dependencies) {
- for (const dep of resolveEntry.dependencies) {
- if (!(dep in resolved)) {
- allDepsSatisfied = false;
- break;
- }
- }
- }
-
- urlFn = resolveEntry.url;
- }
-
- if (allDepsSatisfied) {
- urlsToResolve.push(urlFn(match.params, resolved));
- keysToResolve.push(key);
- }
- }
-
- if (keysToResolve.length === 0) {
- throw new Error('Cyclic dependency in "resolved" entries of ' + route.path);
- }
-
- const urlsToResolveByRest = [];
- const keysToResolveByRest = [];
-
- for (let idx = 0; idx < keysToResolve.length; idx++) {
- const key = keysToResolve[idx];
- const url = urlsToResolve[idx];
-
- if (url in prevResolverState.resolvedByUrl) {
- const entity = prevResolverState.resolvedByUrl[url];
- resolved[key] = entity;
- resolverState.resolvedByUrl[url] = entity;
-
- } else {
- urlsToResolveByRest.push(url);
- keysToResolveByRest.push(key);
- }
- }
-
- if (keysToResolveByRest.length > 0) {
- const promises = urlsToResolveByRest.map(url => {
- if (url) {
- return axios.get(getUrl(url));
- } else {
- return Promise.resolve({data: null});
- }
- });
- const resolvedArr = await Promise.all(promises);
-
- for (let idx = 0; idx < keysToResolveByRest.length; idx++) {
- resolved[keysToResolveByRest[idx]] = resolvedArr[idx].data;
- resolverState.resolvedByUrl[urlsToResolveByRest[idx]] = resolvedArr[idx].data;
- }
- }
-
- for (const key of keysToResolve) {
- keysToGo.delete(key);
- }
- }
- }
-
- async function processCheckPermissions() {
- const checkPermsRequest = {};
-
- function getSig(checkPermissionsEntry) {
- return `${checkPermissionsEntry.entityTypeId}-${checkPermissionsEntry.entityId || ''}-${checkPermissionsEntry.requiredOperations.join(',')}`;
- }
-
- for (const key in route.checkPermissions) {
- const checkPermissionsEntry = route.checkPermissions[key];
- const sig = getSig(checkPermissionsEntry);
-
- if (sig in prevResolverState.permissionsBySig) {
- const perm = prevResolverState.permissionsBySig[sig];
- permissions[key] = perm;
- resolverState.permissionsBySig[sig] = perm;
-
- } else {
- checkPermsRequest[key] = checkPermissionsEntry;
- }
- }
-
- if (Object.keys(checkPermsRequest).length > 0) {
- const result = await checkPermissions(checkPermsRequest);
-
- for (const key in checkPermsRequest) {
- const checkPermissionsEntry = checkPermsRequest[key];
- const perm = result.data[key];
-
- permissions[key] = perm;
- resolverState.permissionsBySig[getSig(checkPermissionsEntry)] = perm;
- }
- }
-
- }
-
- await Promise.all([processResolve(), processCheckPermissions()]);
-
- return { resolved, permissions, resolverState };
-}
-
-export function getRoutes(structure, parentRoute) {
- function _getRoutes(urlPrefix, resolve, checkPermissions, parents, structure, navs, primaryMenuComponent, secondaryMenuComponent) {
- let routes = [];
- for (let routeKey in structure) {
- const entry = structure[routeKey];
-
- let path = urlPrefix + routeKey;
- let pathWithParams = path;
-
- if (entry.extraParams) {
- pathWithParams = pathWithParams + '/' + entry.extraParams.join('/');
- }
-
- let entryResolve;
- if (entry.resolve) {
- entryResolve = Object.assign({}, resolve, entry.resolve);
- } else {
- entryResolve = resolve;
- }
-
- let entryCheckPermissions;
- if (entry.checkPermissions) {
- entryCheckPermissions = Object.assign({}, checkPermissions, entry.checkPermissions);
- } else {
- entryCheckPermissions = checkPermissions;
- }
-
- let navKeys;
- const entryNavs = [];
- if (entry.navs) {
- navKeys = Object.keys(entry.navs);
-
- for (const navKey of navKeys) {
- const nav = entry.navs[navKey];
-
- entryNavs.push({
- title: nav.title,
- visible: nav.visible,
- link: nav.link,
- externalLink: nav.externalLink
- });
- }
- }
-
- const route = {
- path: (pathWithParams === '' ? '/' : pathWithParams),
- exact: !entry.structure && entry.exact !== false,
- structure: entry.structure,
- panelComponent: entry.panelComponent,
- panelRender: entry.panelRender,
- primaryMenuComponent: entry.primaryMenuComponent || primaryMenuComponent,
- secondaryMenuComponent: entry.secondaryMenuComponent || secondaryMenuComponent,
- title: entry.title,
- link: entry.link,
- panelInFullScreen: entry.panelInFullScreen,
- insideIframe: entry.insideIframe,
- resolve: entryResolve,
- checkPermissions: entryCheckPermissions,
- parents,
- navs: [...navs, ...entryNavs],
-
- // This is primarily for route embedding via "structure"
- routeSpec: entry,
- urlPrefix,
- siblingNavs: navs,
- routeKey
- };
-
- routes.push(route);
-
- const childrenParents = [...parents, route];
-
- if (entry.navs) {
- for (let navKeyIdx = 0; navKeyIdx < navKeys.length; navKeyIdx++) {
- const navKey = navKeys[navKeyIdx];
- const nav = entry.navs[navKey];
-
- const childNavs = [...entryNavs];
- childNavs[navKeyIdx] = Object.assign({}, childNavs[navKeyIdx], { active: true });
-
- routes = routes.concat(_getRoutes(path + '/', entryResolve, entryCheckPermissions, childrenParents, { [navKey]: nav }, childNavs, route.primaryMenuComponent, route.secondaryMenuComponent));
- }
- }
-
- if (entry.children) {
- routes = routes.concat(_getRoutes(path + '/', entryResolve, entryCheckPermissions, childrenParents, entry.children, entryNavs, route.primaryMenuComponent, route.secondaryMenuComponent));
- }
- }
-
- return routes;
- }
-
- if (parentRoute) {
- // This embeds the structure in the parent route.
-
- const routeSpec = parentRoute.routeSpec;
-
- const extStructure = {
- ...routeSpec,
- structure: undefined,
- ...structure,
- navs: { ...(routeSpec.navs || {}), ...(structure.navs || {}) },
- children: { ...(routeSpec.children || {}), ...(structure.children || {}) }
- };
-
- return _getRoutes(parentRoute.urlPrefix, parentRoute.resolve, parentRoute.checkPermissions, parentRoute.parents, { [parentRoute.routeKey]: extStructure }, parentRoute.siblingNavs, parentRoute.primaryMenuComponent, parentRoute.secondaryMenuComponent);
-
- } else {
- return _getRoutes('', {}, {}, [], { "": structure }, [], null, null);
- }
-}
-
-
-@withComponentMixins([
- withErrorHandling
-])
-export class Resolver extends Component {
- constructor(props) {
- super(props);
-
- this.state = {
- resolved: null,
- permissions: null,
- resolverState: null
- };
-
- if (Object.keys(props.route.resolve).length === 0 && Object.keys(props.route.checkPermissions).length === 0) {
- this.state.resolved = {};
- this.state.permissions = {};
- }
- }
-
- static propTypes = {
- route: PropTypes.object.isRequired,
- render: PropTypes.func.isRequired,
- location: PropTypes.object,
- match: PropTypes.object
- }
-
- @withAsyncErrorHandler
- async resolve(prevMatch) {
- const props = this.props;
-
- if (Object.keys(props.route.resolve).length === 0 && Object.keys(props.route.checkPermissions).length === 0) {
- this.setState({
- resolved: {},
- permissions: {},
- resolverState: null
- });
-
- } else {
- const prevResolverState = this.state.resolverState;
-
- if (this.state.resolverState) {
- this.setState({
- resolved: null,
- permissions: null,
- resolverState: null
- });
- }
-
- const {resolved, permissions, resolverState} = await resolve(props.route, props.match, prevResolverState);
-
- if (!this.disregardResolve) { // This is to prevent the warning about setState on discarded component when we immediatelly redirect.
- this.setState({
- resolved,
- permissions,
- resolverState
- });
- }
- }
- }
-
- componentDidMount() {
- // noinspection JSIgnoredPromiseFromCall
- this.resolve();
- }
-
- componentDidUpdate(prevProps) {
- if (this.props.location.state !== prevProps.location.state || !shallowEqual(this.props.match.params, prevProps.match.params)) {
- // noinspection JSIgnoredPromiseFromCall
- this.resolve(prevProps.route, prevProps.match);
- }
- }
-
- componentWillUnmount() {
- this.disregardResolve = true; // This is to prevent the warning about setState on discarded component when we immediatelly redirect.
- }
-
- render() {
- return this.props.render(this.state.resolved, this.state.permissions, this.props);
- }
-}
-
-
-class RedirectRoute extends Component {
- static propTypes = {
- route: PropTypes.object.isRequired
- }
-
- render() {
- const route = this.props.route;
- const params = this.props.match.params;
-
- let link;
- if (typeof route.link === 'function') {
- link = route.link(params);
- } else {
- link = route.link;
- }
-
- return ;
- }
-}
-
-
-@withComponentMixins([
- withTranslation
-])
-class SubRoute extends Component {
- static propTypes = {
- route: PropTypes.object.isRequired,
- location: PropTypes.object.isRequired,
- match: PropTypes.object.isRequired,
- flashMessage: PropTypes.object,
- panelRouteCtor: PropTypes.func.isRequired,
- loadingMessageFn: PropTypes.func.isRequired
- }
-
- render() {
- const t = this.props.t;
- const route = this.props.route;
- const params = this.props.match.params;
-
- const render = (resolved, permissions) => {
- if (resolved && permissions) {
- const subStructure = route.structure(resolved, permissions, params);
- const routes = getRoutes(subStructure, route);
-
- const _renderRoute = route => {
- const render = props => renderRoute(route, this.props.panelRouteCtor, this.props.loadingMessageFn, this.props.flashMessage, props);
- return
- };
-
- return (
- {routes.map(x => _renderRoute(x))}
- );
-
- } else {
- return this.props.loadingMessageFn();
- }
- };
-
- return ;
- }
-}
-
-export function renderRoute(route, panelRouteCtor, loadingMessageFn, flashMessage, props) {
- if (route.structure) {
- return ;
-
- } else if (!route.panelRender && !route.panelComponent && route.link) {
- return ;
-
- } else {
- const PanelRoute = panelRouteCtor;
- return ;
- }
-
-}
-
-export const SectionContentContext = React.createContext(null);
-export const withPageHelpers = createComponentMixin({
- contexts: [{context: SectionContentContext, propName: 'sectionContent'}],
- deps: [withErrorHandling],
- decoratorFn: (TargetClass, InnerClass) => {
- InnerClass.prototype.setFlashMessage = function (severity, text) {
- return this.props.sectionContent.setFlashMessage(severity, text);
- };
-
- InnerClass.prototype.navigateTo = function (path) {
- return this.props.sectionContent.navigateTo(path);
- };
-
- InnerClass.prototype.navigateBack = function () {
- return this.props.sectionContent.navigateBack();
- };
-
- InnerClass.prototype.navigateToWithFlashMessage = function (path, severity, text) {
- return this.props.sectionContent.navigateToWithFlashMessage(path, severity, text);
- };
-
- InnerClass.prototype.registerBeforeUnloadHandlers = function (handlers) {
- return this.props.sectionContent.registerBeforeUnloadHandlers(handlers);
- };
-
- InnerClass.prototype.deregisterBeforeUnloadHandlers = function (handlers) {
- return this.props.sectionContent.deregisterBeforeUnloadHandlers(handlers);
- };
-
- return {};
- }
-});
diff --git a/client/src/lib/page.js b/client/src/lib/page.js
deleted file mode 100644
index be9562f6..00000000
--- a/client/src/lib/page.js
+++ /dev/null
@@ -1,727 +0,0 @@
-'use strict';
-
-import React, {Component} from "react";
-import i18n, {withTranslation} from './i18n';
-import PropTypes from "prop-types";
-import {withRouter} from "react-router";
-import {BrowserRouter as Router, Link, Route, Switch} from "react-router-dom";
-import {withErrorHandling} from "./error-handling";
-import interoperableErrors from "../../../shared/interoperable-errors";
-import {ActionLink, Button, DismissibleAlert, DropdownActionLink, Icon} from "./bootstrap-components";
-import mailtrainConfig from "mailtrainConfig";
-import styles from "./styles.scss";
-import {getRoutes, renderRoute, Resolver, SectionContentContext, withPageHelpers} from "./page-common";
-import {getBaseDir} from "./urls";
-import {createComponentMixin, withComponentMixins} from "./decorator-helpers";
-import {getLang} from "../../../shared/langs";
-
-export { withPageHelpers }
-
-class Breadcrumb extends Component {
- constructor(props) {
- super(props);
- }
-
- static propTypes = {
- route: PropTypes.object.isRequired,
- params: PropTypes.object.isRequired,
- resolved: PropTypes.object.isRequired
- }
-
- renderElement(entry, isActive) {
- const params = this.props.params;
- let title;
- if (typeof entry.title === 'function') {
- title = entry.title(this.props.resolved, params);
- } else {
- title = entry.title;
- }
-
- if (isActive) {
- return {title} ;
-
- } else if (entry.externalLink) {
- let externalLink;
- if (typeof entry.externalLink === 'function') {
- externalLink = entry.externalLink(params);
- } else {
- externalLink = entry.externalLink;
- }
-
- return {title} ;
-
- } else if (entry.link) {
- let link;
- if (typeof entry.link === 'function') {
- link = entry.link(params);
- } else {
- link = entry.link;
- }
- return {title} ;
-
- } else {
- return {title} ;
- }
- }
-
- render() {
- const route = this.props.route;
-
- const renderedElems = [...route.parents.map(x => this.renderElement(x)), this.renderElement(route, true)];
-
- return {renderedElems} ;
- }
-}
-
-class TertiaryNavBar extends Component {
- static propTypes = {
- route: PropTypes.object.isRequired,
- params: PropTypes.object.isRequired,
- resolved: PropTypes.object.isRequired,
- className: PropTypes.string
- }
-
- renderElement(key, entry) {
- const params = this.props.params;
- let title;
- if (typeof entry.title === 'function') {
- title = entry.title(this.props.resolved);
- } else {
- title = entry.title;
- }
-
- let liClassName = 'nav-item';
- let linkClassName = 'nav-link';
- if (entry.active) {
- linkClassName += ' active';
- }
-
- if (entry.link) {
- let link;
-
- if (typeof entry.link === 'function') {
- link = entry.link(params);
- } else {
- link = entry.link;
- }
-
- return {title} ;
-
- } else if (entry.externalLink) {
- let externalLink;
- if (typeof entry.externalLink === 'function') {
- externalLink = entry.externalLink(params);
- } else {
- externalLink = entry.externalLink;
- }
-
- return {title} ;
-
- } else {
- return {title} ;
- }
- }
-
- render() {
- const route = this.props.route;
-
- const keys = Object.keys(route.navs);
- const renderedElems = [];
-
- for (const key of keys) {
- const entry = route.navs[key];
-
- let visible = true;
- if (typeof entry.visible === 'function') {
- visible = entry.visible(this.props.resolved);
- }
-
- if (visible) {
- renderedElems.push(this.renderElement(key, entry));
- }
- }
-
- if (renderedElems.length > 1) {
- let className = styles.tertiaryNav + ' nav nav-pills';
- if (this.props.className) {
- className += ' ' + this.props.className;
- }
-
- return ;
- } else {
- return null;
- }
- }
-}
-
-
-
-function getLoadingMessage(t) {
- return (
-
- {t('loading')}
-
- );
-}
-
-function renderFrameWithContent(t, panelInFullScreen, showSidebar, primaryMenu, secondaryMenu, content) {
- if (panelInFullScreen) {
- return (
-
- );
-
- } else {
- return (
-
-
-
- {showSidebar &&
-
-
-
- }
-
- Mailtrain
-
-
-
-
-
-
- {primaryMenu}
-
-
-
-
-
- {showSidebar &&
-
- {secondaryMenu}
-
- }
-
- {content}
-
-
-
-
-
- );
- }
-}
-
-
-@withComponentMixins([
- withTranslation
-])
-class PanelRoute extends Component {
- constructor(props) {
- super(props);
- this.state = {
- panelInFullScreen: props.route.panelInFullScreen
- };
-
- this.sidebarAnimationNodeListener = evt => {
- if (evt.propertyName === 'left') {
- this.forceUpdate();
- }
- };
-
- this.setPanelInFullScreen = panelInFullScreen => this.setState({ panelInFullScreen });
- }
-
- static propTypes = {
- route: PropTypes.object.isRequired,
- location: PropTypes.object.isRequired,
- match: PropTypes.object.isRequired,
- flashMessage: PropTypes.object
- }
-
- registerSidebarAnimationListener() {
- if (this.sidebarAnimationNode) {
- this.sidebarAnimationNode.addEventListener("transitionend", this.sidebarAnimationNodeListener);
- }
- }
-
- componentDidMount() {
- this.registerSidebarAnimationListener();
- }
-
- componentDidUpdate(prevProps) {
- this.registerSidebarAnimationListener();
- }
-
- render() {
- const t = this.props.t;
- const route = this.props.route;
- const params = this.props.match.params;
-
- const showSidebar = !!route.secondaryMenuComponent;
-
- const panelInFullScreen = this.state.panelInFullScreen;
-
- const render = (resolved, permissions) => {
- let primaryMenu = null;
- let secondaryMenu = null;
- let content = null;
-
- if (resolved && permissions) {
- const compProps = {
- match: this.props.match,
- location: this.props.location,
- resolved,
- permissions,
- setPanelInFullScreen: this.setPanelInFullScreen,
- panelInFullScreen: this.state.panelInFullScreen
- };
-
- let panel;
- if (route.panelComponent) {
- panel = React.createElement(route.panelComponent, compProps);
- } else if (route.panelRender) {
- panel = route.panelRender(compProps);
- }
-
- if (route.primaryMenuComponent) {
- primaryMenu = React.createElement(route.primaryMenuComponent, compProps);
- }
-
- if (route.secondaryMenuComponent) {
- secondaryMenu = React.createElement(route.secondaryMenuComponent, compProps);
- }
-
- const panelContent = (
-
- {this.props.flashMessage}
- {panel}
-
- );
-
- if (panelInFullScreen) {
- content = panelContent;
- } else {
- content = (
- <>
-
-
-
-
- {panelContent}
- >
- );
- }
-
- } else {
- content = getLoadingMessage(t);
- }
-
- return renderFrameWithContent(t, panelInFullScreen, showSidebar, primaryMenu, secondaryMenu, content);
- };
-
-
- return ;
- }
-}
-
-
-export class BeforeUnloadListeners {
- constructor() {
- this.listeners = new Set();
- }
-
- register(listener) {
- this.listeners.add(listener);
- }
-
- deregister(listener) {
- this.listeners.delete(listener);
- }
-
- shouldUnloadBeCancelled() {
- for (const lst of this.listeners) {
- if (lst.handler()) return true;
- }
-
- return false;
- }
-
- async shouldUnloadBeCancelledAsync() {
- for (const lst of this.listeners) {
- if (await lst.handlerAsync()) return true;
- }
-
- return false;
- }
-}
-
-@withRouter
-@withComponentMixins([
- withTranslation,
- withErrorHandling
-], ['onNavigationConfirmationDialog'])
-export class SectionContent extends Component {
- constructor(props) {
- super(props);
-
- this.state = {
- flashMessageText: ''
- };
-
- this.historyUnlisten = props.history.listen((location, action) => {
- // noinspection JSIgnoredPromiseFromCall
- this.closeFlashMessage();
- });
-
- this.beforeUnloadListeners = new BeforeUnloadListeners();
- this.beforeUnloadHandler = ::this.onBeforeUnload;
- this.historyUnblock = null;
- }
-
- static propTypes = {
- structure: PropTypes.object.isRequired,
- root: PropTypes.string.isRequired
- }
-
- onBeforeUnload(event) {
- if (this.beforeUnloadListeners.shouldUnloadBeCancelled()) {
- event.preventDefault();
- event.returnValue = '';
- }
- }
-
- onNavigationConfirmationDialog(message, callback) {
- this.beforeUnloadListeners.shouldUnloadBeCancelledAsync().then(res => {
- if (res) {
- const allowTransition = window.confirm(message);
- callback(allowTransition);
- } else {
- callback(true);
- }
- });
- }
-
- componentDidMount() {
- window.addEventListener('beforeunload', this.beforeUnloadHandler);
- this.historyUnblock = this.props.history.block('Changes you made may not be saved. Are you sure you want to leave this page?');
- }
-
- componentWillUnmount() {
- window.removeEventListener('beforeunload', this.beforeUnloadHandler);
- this.historyUnblock();
- }
-
- setFlashMessage(severity, text) {
- this.setState({
- flashMessageText: text,
- flashMessageSeverity: severity
- });
- }
-
- navigateTo(path) {
- this.props.history.push(path);
- }
-
- navigateBack() {
- this.props.history.goBack();
- }
-
- navigateToWithFlashMessage(path, severity, text) {
- this.props.history.push(path);
- this.setFlashMessage(severity, text);
- }
-
- ensureAuthenticated() {
- if (!mailtrainConfig.isAuthenticated) {
- this.navigateTo('/login?next=' + encodeURIComponent(window.location.pathname));
- }
- }
-
- registerBeforeUnloadHandlers(handlers) {
- this.beforeUnloadListeners.register(handlers);
- }
-
- deregisterBeforeUnloadHandlers(handlers) {
- this.beforeUnloadListeners.deregister(handlers);
- }
-
- errorHandler(error) {
- if (error instanceof interoperableErrors.NotLoggedInError) {
- if (window.location.pathname !== '/login') { // There may be multiple async requests failing at the same time. So we take the pathname only from the first one.
- this.navigateTo('/login?next=' + encodeURIComponent(window.location.pathname));
- }
- } else if (error.response && error.response.data && error.response.data.message) {
- console.error(error);
- this.navigateToWithFlashMessage(this.props.root, 'danger', error.response.data.message);
- } else {
- console.error(error);
- this.navigateToWithFlashMessage(this.props.root, 'danger', error.message);
- }
- return true;
- }
-
- async closeFlashMessage() {
- this.setState({
- flashMessageText: ''
- });
- }
-
- renderRoute(route) {
- const t = this.props.t;
-
- const render = props => {
- let flashMessage;
- if (this.state.flashMessageText) {
- flashMessage = {this.state.flashMessageText} ;
- }
-
- return renderRoute(
- route,
- PanelRoute,
- () => renderFrameWithContent(t,false, false, null, null, getLoadingMessage(this.props.t)),
- flashMessage,
- props
- );
- };
-
- return
- }
-
- render() {
- const routes = getRoutes(this.props.structure);
-
- return (
-
- {routes.map(x => this.renderRoute(x))}
-
- );
- }
-}
-
-@withComponentMixins([
- withTranslation
-])
-export class Section extends Component {
- constructor(props) {
- super(props);
- this.getUserConfirmationHandler = ::this.onGetUserConfirmation;
- this.sectionContent = null;
- }
-
- static propTypes = {
- structure: PropTypes.oneOfType([PropTypes.object, PropTypes.func]).isRequired,
- root: PropTypes.string.isRequired
- }
-
- onGetUserConfirmation(message, callback) {
- this.sectionContent.onNavigationConfirmationDialog(message, callback);
- }
-
- render() {
- let structure = this.props.structure;
- if (typeof structure === 'function') {
- structure = structure(this.props.t);
- }
-
- return (
-
- this.sectionContent = node} root={this.props.root} structure={structure} />
-
- );
- }
-}
-
-
-export class Title extends Component {
- render() {
- return (
-
-
{this.props.children}
-
-
- );
- }
-}
-
-export class Toolbar extends Component {
- static propTypes = {
- className: PropTypes.string,
- };
-
- render() {
- let className = styles.toolbar + ' ' + styles.buttonRow;
- if (this.props.className) {
- className += ' ' + this.props.className;
- }
-
- return (
-
- {this.props.children}
-
- );
- }
-}
-
-export class LinkButton extends Component {
- static propTypes = {
- label: PropTypes.string,
- icon: PropTypes.string,
- className: PropTypes.string,
- to: PropTypes.string
- };
-
- render() {
- const props = this.props;
-
- return (
-
- );
- }
-}
-
-export class DropdownLink extends Component {
- static propTypes = {
- to: PropTypes.string,
- className: PropTypes.string
- }
-
- render() {
- const props = this.props;
-
- const clsName = "dropdown-item" + (props.className ? " " + props.className : "")
- return (
- {props.children}
- );
- }
-}
-
-export class NavLink extends Component {
- static propTypes = {
- to: PropTypes.string,
- icon: PropTypes.string,
- iconFamily: PropTypes.string,
- className: PropTypes.string
- }
-
- render() {
- const props = this.props;
-
- const clsName = "nav-item" + (props.className ? " " + props.className : "")
-
- let icon;
- if (props.icon) {
- icon = <>{' '}>;
- }
-
- return (
- {icon}{props.children}
- );
- }
-}
-
-export class NavActionLink extends Component {
- static propTypes = {
- onClickAsync: PropTypes.func,
- icon: PropTypes.string,
- iconFamily: PropTypes.string,
- className: PropTypes.string
- }
-
- render() {
- const props = this.props;
-
- const clsName = "nav-item" + (props.className ? " " + props.className : "")
-
- let icon;
- if (props.icon) {
- icon = <>{' '}>;
- }
-
- return (
- {icon}{props.children}
- );
- }
-}
-
-export class NavDropdown extends Component {
- static propTypes = {
- label: PropTypes.string,
- icon: PropTypes.string,
- className: PropTypes.string,
- menuClassName: PropTypes.string
- }
-
- render() {
- const props = this.props;
-
- const className = 'nav-item dropdown' + (props.className ? ' ' + props.className : '');
- const menuClassName = 'dropdown-menu' + (props.menuClassName ? ' ' + props.menuClassName : '');
-
- return (
-
- {props.icon ?
-
- {' '}{props.label}
-
- :
-
- {props.label}
-
- }
-
-
- );
- }
-}
-
-
-export const requiresAuthenticatedUser = createComponentMixin({
- deps: [withPageHelpers],
- decoratorFn: (TargetClass, InnerClass) => {
- class RequiresAuthenticatedUser extends React.Component {
- constructor(props) {
- super(props);
- props.sectionContent.ensureAuthenticated();
- }
-
- render() {
- return
- }
- }
-
- return {
- cls: RequiresAuthenticatedUser
- };
- }
-});
-
-export function getLanguageChooser(t) {
- const languageOptions = [];
- for (const lng of mailtrainConfig.enabledLanguages) {
- const langDesc = getLang(lng);
- const label = langDesc.getLabel(t);
-
- languageOptions.push(
- i18n.changeLanguage(langDesc.longCode)}>{label}
- )
- }
-
- const currentLngCode = getLang(i18n.language).getShortLabel(t);
-
- const languageChooser = (
-
- {languageOptions}
-
- );
-
- return languageChooser;
-}
\ No newline at end of file
diff --git a/client/src/lib/permissions.js b/client/src/lib/permissions.js
deleted file mode 100644
index ceb7d035..00000000
--- a/client/src/lib/permissions.js
+++ /dev/null
@@ -1,8 +0,0 @@
-'use strict';
-
-import {getUrl} from "./urls";
-import axios from "./axios";
-
-export async function checkPermissions(request) {
- return await axios.post(getUrl('rest/permissions-check'), request);
-}
diff --git a/client/src/lib/public-path.js b/client/src/lib/public-path.js
deleted file mode 100644
index 40acd017..00000000
--- a/client/src/lib/public-path.js
+++ /dev/null
@@ -1,5 +0,0 @@
-'use strict';
-
-import {getUrl} from "./urls";
-
-__webpack_public_path__ = getUrl('client/');
diff --git a/client/src/lib/sandbox-common.scss b/client/src/lib/sandbox-common.scss
deleted file mode 100644
index 818b31a9..00000000
--- a/client/src/lib/sandbox-common.scss
+++ /dev/null
@@ -1,90 +0,0 @@
-$navbarHeight: 34px;
-$editorNormalHeight: 800px !default;
-
-.editor {
- .host {
- @if $editorNormalHeight {
- height: $editorNormalHeight;
- }
- }
-}
-
-.editorFullscreen {
- position: fixed;
- top: 0px;
- bottom: 0px;
- left: 0px;
- right: 0px;
- z-index: 1000;
- background: white;
- margin-top: $navbarHeight;
-
- .navbar {
- margin-top: -$navbarHeight;
- }
-
- .host {
- height: 100%;
- }
-}
-
-.navbar {
- background: #f86c6b;
- width: 100%;
- height: $navbarHeight;
- display: flex;
- justify-content: space-between;
-}
-
-.navbarLeft {
- .logo {
- display: inline-block;
- height: $navbarHeight;
- padding: 5px 0 5px 10px;
- filter: brightness(0) invert(1);
- }
-
- .title {
- display: inline-block;
- padding: 5px 0 5px 10px;
- font-size: 18px;
- font-weight: bold;
- float: left;
- color: white;
- height: $navbarHeight;
- }
-}
-
-.navbarRight {
- .btn, .btnDisabled {
- display: inline-block;
- padding: 0px 15px;
- line-height: $navbarHeight;
- text-align: center;
- font-size: 14px;
- font-weight: bold;
- font-family: sans-serif;
- cursor: pointer;
-
- &, &:not([href]):not([tabindex]) { // This is to override reboot.scss in bootstrap
- color: white;
- }
- }
-
- .btn:hover {
- background-color: #c05454;
- text-decoration: none;
-
- &, &:not([href]):not([tabindex]) { // This is to override reboot.scss in bootstrap
- color: white;
- }
- }
-
- .btnDisabled {
- cursor: default;
-
- &, &:not([href]):not([tabindex]) { // This is to override reboot.scss in bootstrap
- color: #621d1d;
- }
- }
-}
diff --git a/client/src/lib/sandboxed-ckeditor-root.js b/client/src/lib/sandboxed-ckeditor-root.js
deleted file mode 100644
index 6ab2f49f..00000000
--- a/client/src/lib/sandboxed-ckeditor-root.js
+++ /dev/null
@@ -1,135 +0,0 @@
-'use strict';
-
-import './public-path';
-
-import React, {Component} from 'react';
-import ReactDOM from 'react-dom';
-import {TranslationRoot, withTranslation} from './i18n';
-import {parentRPC, UntrustedContentRoot} from './untrusted';
-import PropTypes from "prop-types";
-import styles from "./sandboxed-ckeditor.scss";
-import {getPublicUrl, getSandboxUrl, getTrustedUrl} from "./urls";
-import {base, unbase} from "../../../shared/templates";
-
-import CKEditor from "react-ckeditor-component";
-
-import {initialHeight} from "./sandboxed-ckeditor-shared";
-import {withComponentMixins} from "./decorator-helpers";
-
-
-@withComponentMixins([
- withTranslation
-])
-class CKEditorSandbox extends Component {
- constructor(props) {
- super(props);
-
- const trustedUrlBase = getTrustedUrl();
- const sandboxUrlBase = getSandboxUrl();
- const publicUrlBase = getPublicUrl();
- const source = this.props.initialSource && base(this.props.initialSource, trustedUrlBase, sandboxUrlBase, publicUrlBase);
-
- this.state = {
- source
- };
- }
-
- static propTypes = {
- entityTypeId: PropTypes.string,
- entityId: PropTypes.number,
- initialSource: PropTypes.string
- }
-
- async exportState(method, params) {
- const trustedUrlBase = getTrustedUrl();
- const sandboxUrlBase = getSandboxUrl();
- const publicUrlBase = getPublicUrl();
-
- const preHtml = ' ';
- const postHtml = '';
-
- const unbasedSource = unbase(this.state.source, trustedUrlBase, sandboxUrlBase, publicUrlBase, true);
-
- return {
- source: unbasedSource,
- html: preHtml + unbasedSource + postHtml
- };
- }
-
- async setHeight(methods, params) {
- this.node.editorInstance.resize('100%', params);
- }
-
- componentDidMount() {
- parentRPC.setMethodHandler('exportState', ::this.exportState);
- parentRPC.setMethodHandler('setHeight', ::this.setHeight);
- }
-
- render() {
- const config = {
- toolbarGroups: [
- {
- name: "document",
- groups: ["document", "doctools"]
- },
- {
- name: "clipboard",
- groups: ["clipboard", "undo"]
- },
- {name: "styles"},
- {
- name: "basicstyles",
- groups: ["basicstyles", "cleanup"]
- },
- {
- name: "editing",
- groups: ["find", "selection", "spellchecker"]
- },
- {name: "forms"},
- {
- name: "paragraph",
- groups: ["list",
- "indent", "blocks", "align", "bidi"]
- },
- {name: "links"},
- {name: "insert"},
- {name: "colors"},
- {name: "tools"},
- {name: "others"},
- {
- name: "document-mode",
- groups: ["mode"]
- }
- ],
-
- removeButtons: 'Underline,Subscript,Superscript,Maximize',
- resize_enabled: false,
- height: initialHeight
- };
-
- return (
-
- this.node = node}
- content={this.state.source}
- events={{
- change: evt => this.setState({source: evt.editor.getData()}),
- }}
- config={config}
- />
-
- );
- }
-}
-
-export default function() {
- parentRPC.init();
-
- ReactDOM.render(
-
- } />
- ,
- document.getElementById('root')
- );
-};
-
-
diff --git a/client/src/lib/sandboxed-ckeditor-shared.js b/client/src/lib/sandboxed-ckeditor-shared.js
deleted file mode 100644
index 8754615f..00000000
--- a/client/src/lib/sandboxed-ckeditor-shared.js
+++ /dev/null
@@ -1,3 +0,0 @@
-'use strict';
-
-export const initialHeight = 600;
diff --git a/client/src/lib/sandboxed-ckeditor.js b/client/src/lib/sandboxed-ckeditor.js
deleted file mode 100644
index 7e0c96d1..00000000
--- a/client/src/lib/sandboxed-ckeditor.js
+++ /dev/null
@@ -1,112 +0,0 @@
-'use strict';
-
-import React, {Component} from 'react';
-import {withTranslation} from './i18n';
-import PropTypes from "prop-types";
-import styles from "./sandboxed-ckeditor.scss";
-
-import {UntrustedContentHost} from './untrusted';
-import {Icon} from "./bootstrap-components";
-import {getTrustedUrl} from "./urls";
-
-import {initialHeight} from "./sandboxed-ckeditor-shared";
-import {withComponentMixins} from "./decorator-helpers";
-
-const navbarHeight = 34; // Sync this with navbarheight in sandboxed-ckeditor.scss
-
-@withComponentMixins([
- withTranslation
-], ['exportState'])
-export class CKEditorHost extends Component {
- constructor(props) {
- super(props);
-
- this.state = {
- fullscreen: false
- };
-
- this.onWindowResizeHandler = ::this.onWindowResize;
- this.contentNodeRefHandler = node => this.contentNode = node;
- }
-
- static propTypes = {
- entityTypeId: PropTypes.string,
- entity: PropTypes.object,
- initialSource: PropTypes.string,
- title: PropTypes.string,
- onSave: PropTypes.func,
- canSave: PropTypes.bool,
- onTestSend: PropTypes.func,
- onShowExport: PropTypes.func,
- onFullscreenAsync: PropTypes.func
- }
-
- async toggleFullscreenAsync() {
- const fullscreen = !this.state.fullscreen;
- this.setState({
- fullscreen
- });
- await this.props.onFullscreenAsync(fullscreen);
-
- let newHeight;
- if (fullscreen) {
- newHeight = window.innerHeight - navbarHeight;
- } else {
- newHeight = initialHeight;
- }
- await this.contentNode.ask('setHeight', newHeight);
- }
-
- async exportState() {
- return await this.contentNode.ask('exportState');
- }
-
- onWindowResize() {
- if (this.state.fullscreen) {
- const newHeight = window.innerHeight - navbarHeight;
- // noinspection JSIgnoredPromiseFromCall
- this.contentNode.ask('setHeight', newHeight);
- }
- }
-
- componentDidMount() {
- window.addEventListener('resize', this.onWindowResizeHandler, false);
- }
-
- componentWillUnmount() {
- window.removeEventListener('resize', this.onWindowResizeHandler, false);
- }
-
- render() {
- const t = this.props.t;
-
- const editorData = {
- entityTypeId: this.props.entityTypeId,
- entityId: this.props.entity.id,
- initialSource: this.props.initialSource
- };
-
- const tokenData = {
- entityTypeId: this.props.entityTypeId,
- entityId: this.props.entity.id
- };
-
- return (
-
-
-
- {this.state.fullscreen &&
}
-
{this.props.title}
-
-
-
-
-
- );
- }
-}
diff --git a/client/src/lib/sandboxed-ckeditor.scss b/client/src/lib/sandboxed-ckeditor.scss
deleted file mode 100644
index 36737f23..00000000
--- a/client/src/lib/sandboxed-ckeditor.scss
+++ /dev/null
@@ -1,7 +0,0 @@
-$editorNormalHeight: false;
-@import "sandbox-common";
-
-.sandbox {
- height: 100%;
- overflow: hidden;
-}
\ No newline at end of file
diff --git a/client/src/lib/sandboxed-codeeditor-root.js b/client/src/lib/sandboxed-codeeditor-root.js
deleted file mode 100644
index 97326e06..00000000
--- a/client/src/lib/sandboxed-codeeditor-root.js
+++ /dev/null
@@ -1,220 +0,0 @@
-'use strict';
-
-import './public-path';
-
-import React, {Component} from 'react';
-import ReactDOM from 'react-dom';
-import {TranslationRoot, withTranslation} from './i18n';
-import {parentRPC, UntrustedContentRoot} from './untrusted';
-import PropTypes from "prop-types";
-import styles from "./sandboxed-codeeditor.scss";
-import {getPublicUrl, getSandboxUrl, getTrustedUrl} from "./urls";
-import {base, unbase} from "../../../shared/templates";
-import ACEEditorRaw from 'react-ace';
-import 'brace/theme/github';
-import 'brace/ext/searchbox';
-import 'brace/mode/html';
-import {CodeEditorSourceType} from "./sandboxed-codeeditor-shared";
-
-import mjml2html from "./mjml";
-
-import juice from "juice";
-
-import {withComponentMixins} from "./decorator-helpers";
-
-const refreshTimeout = 1000;
-
-@withComponentMixins([
- withTranslation
-])
-class CodeEditorSandbox extends Component {
- constructor(props) {
- super(props);
-
- let defaultSource;
-
- if (props.sourceType === CodeEditorSourceType.MJML) {
- defaultSource =
- '\n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' ';
-
- } else if (props.sourceType === CodeEditorSourceType.HTML) {
- defaultSource =
- '\n' +
- '\n' +
- '\n' +
- ' \n' +
- ' Title of the document \n' +
- '\n' +
- '\n' +
- ' Content of the document......\n' +
- '\n' +
- '';
- }
-
-
- const trustedUrlBase = getTrustedUrl();
- const sandboxUrlBase = getSandboxUrl();
- const publicUrlBase = getPublicUrl();
- const source = this.props.initialSource ? base(this.props.initialSource, trustedUrlBase, sandboxUrlBase, publicUrlBase) : defaultSource;
-
- this.state = {
- source,
- preview: props.initialPreview,
- wrapEnabled: props.initialWrap
- };
- this.state.previewContents = this.getHtml();
-
- this.onCodeChangedHandler = ::this.onCodeChanged;
-
- this.refreshHandler = ::this.refresh;
- this.refreshTimeoutId = null;
-
- this.onMessageFromPreviewHandler = ::this.onMessageFromPreview;
- this.previewScroll = {x: 0, y: 0};
- }
-
- static propTypes = {
- entityTypeId: PropTypes.string,
- entityId: PropTypes.number,
- initialSource: PropTypes.string,
- sourceType: PropTypes.string,
- initialPreview: PropTypes.bool,
- initialWrap: PropTypes.bool
- }
-
- async exportState(method, params) {
- const trustedUrlBase = getTrustedUrl();
- const sandboxUrlBase = getSandboxUrl();
- const publicUrlBase = getPublicUrl();
- return {
- html: unbase(this.getHtml(), trustedUrlBase, sandboxUrlBase, publicUrlBase, true),
- source: unbase(this.state.source, trustedUrlBase, sandboxUrlBase, publicUrlBase, true)
- };
- }
-
- async setPreview(method, preview) {
- this.setState({
- preview
- });
- }
-
- async setWrap(method, wrap) {
- this.setState({
- wrapEnabled: wrap
- });
- }
-
- componentDidMount() {
- parentRPC.setMethodHandler('exportState', ::this.exportState);
- parentRPC.setMethodHandler('setPreview', ::this.setPreview);
- parentRPC.setMethodHandler('setWrap', ::this.setWrap);
-
- window.addEventListener('message', this.onMessageFromPreviewHandler, false);
- }
-
- componentWillUnmount() {
- clearTimeout(this.refreshTimeoutId);
- }
-
- getHtml() {
- let contents;
- if (this.props.sourceType === CodeEditorSourceType.MJML) {
- try {
- const res = mjml2html(this.state.source);
- contents = res.html;
- } catch (err) {
- contents = '';
- }
- } else if (this.props.sourceType === CodeEditorSourceType.HTML) {
- contents = juice(this.state.source);
- }
-
- return contents;
- }
-
- onCodeChanged(data) {
- this.setState({
- source: data
- });
-
- if (!this.refreshTimeoutId) {
- this.refreshTimeoutId = setTimeout(() => this.refresh(), refreshTimeout);
- }
- }
-
- onMessageFromPreview(evt) {
- if (evt.data.type === 'scroll') {
- this.previewScroll = evt.data.data;
- }
- }
-
- refresh() {
- this.refreshTimeoutId = null;
-
- this.setState({
- previewContents: this.getHtml()
- });
- }
-
- render() {
- const previewScript =
- '(function() {\n' +
- ' function reportScroll() { window.parent.postMessage({type: \'scroll\', data: {x: window.scrollX, y: window.scrollY}}, \'*\'); }\n' +
- ' reportScroll();\n' +
- ' window.addEventListener(\'scroll\', reportScroll);\n' +
- ' window.addEventListener(\'load\', function(evt) { window.scrollTo(' + this.previewScroll.x + ',' + this.previewScroll.y +'); });\n' +
- '})();\n';
-
- const previewContents = this.state.previewContents.replace(/<\s*head\s*>/i, ``);
-
- return (
-
-
- {
- this.state.preview &&
-
-
-
- }
-
- );
- }
-}
-
-export default function() {
- parentRPC.init();
-
- ReactDOM.render(
-
- } />
- ,
- document.getElementById('root')
- );
-};
-
-
diff --git a/client/src/lib/sandboxed-codeeditor-shared.js b/client/src/lib/sandboxed-codeeditor-shared.js
deleted file mode 100644
index bd886fa4..00000000
--- a/client/src/lib/sandboxed-codeeditor-shared.js
+++ /dev/null
@@ -1,11 +0,0 @@
-'use strict';
-
-export const CodeEditorSourceType = {
- MJML: 'mjml',
- HTML: 'html'
-};
-
-export const getCodeEditorSourceTypeOptions = t => [
- {key: CodeEditorSourceType.MJML, label: t('mjml')},
- {key: CodeEditorSourceType.HTML, label: t('html')}
-];
diff --git a/client/src/lib/sandboxed-codeeditor.js b/client/src/lib/sandboxed-codeeditor.js
deleted file mode 100644
index 21d59a8a..00000000
--- a/client/src/lib/sandboxed-codeeditor.js
+++ /dev/null
@@ -1,109 +0,0 @@
-'use strict';
-
-import React, {Component} from 'react';
-import {withTranslation} from './i18n';
-import PropTypes from "prop-types";
-import styles from "./sandboxed-codeeditor.scss";
-
-import {UntrustedContentHost} from './untrusted';
-import {Icon} from "./bootstrap-components";
-import {getTrustedUrl} from "./urls";
-import {withComponentMixins} from "./decorator-helpers";
-
-@withComponentMixins([
- withTranslation
-], ['exportState'])
-export class CodeEditorHost extends Component {
- constructor(props) {
- super(props);
-
- this.state = {
- fullscreen: false,
- preview: true,
- wrap: true
- };
-
- this.contentNodeRefHandler = node => this.contentNode = node;
- }
-
- static propTypes = {
- entityTypeId: PropTypes.string,
- entity: PropTypes.object,
- initialSource: PropTypes.string,
- sourceType: PropTypes.string,
- title: PropTypes.string,
- onSave: PropTypes.func,
- canSave: PropTypes.bool,
- onTestSend: PropTypes.func,
- onShowExport: PropTypes.func,
- onFullscreenAsync: PropTypes.func
- }
-
- async toggleFullscreenAsync() {
- const fullscreen = !this.state.fullscreen;
- this.setState({
- fullscreen
- });
- await this.props.onFullscreenAsync(fullscreen);
- }
-
- async togglePreviewAsync() {
- const preview = !this.state.preview;
- this.setState({
- preview
- });
-
- await this.contentNode.ask('setPreview', preview);
- }
-
- async toggleWrapAsync() {
- const wrap = !this.state.wrap;
- this.setState({
- wrap
- });
-
- await this.contentNode.ask('setWrap', wrap);
- }
-
- async exportState() {
- return await this.contentNode.ask('exportState');
- }
-
- render() {
- const t = this.props.t;
-
- const editorData = {
- entityTypeId: this.props.entityTypeId,
- entityId: this.props.entity.id,
- initialSource: this.props.initialSource,
- sourceType: this.props.sourceType,
- initialPreview: this.state.preview,
- initialWrap: this.state.wrap
- };
-
- const tokenData = {
- entityTypeId: this.props.entityTypeId,
- entityId: this.props.entity.id
- };
-
- return (
-
-
-
- {this.state.fullscreen &&
}
-
{this.props.title}
-
-
-
-
-
- );
- }
-}
diff --git a/client/src/lib/sandboxed-codeeditor.scss b/client/src/lib/sandboxed-codeeditor.scss
deleted file mode 100644
index 7fcf5604..00000000
--- a/client/src/lib/sandboxed-codeeditor.scss
+++ /dev/null
@@ -1,35 +0,0 @@
-@import "sandbox-common";
-
-.sandbox {
-}
-
-.aceEditorWithPreview, .aceEditorWithoutPreview, .preview {
- position: absolute;
- height: 100%;
-}
-
-.aceEditorWithPreview {
- border-right: #e8e8e8 solid 2px;
- width: 50%;
-}
-
-.aceEditorWithoutPreview {
- width: 100%;
-}
-
-.preview {
- border-left: #e8e8e8 solid 2px;
- width: 50%;
- left: 50%;
- overflow: hidden;
-
- iframe {
- width: 100%;
- height: 100%;
- border: 0px none;
-
- body {
- margin: 0px;
- }
- }
-}
\ No newline at end of file
diff --git a/client/src/lib/sandboxed-grapesjs-root.js b/client/src/lib/sandboxed-grapesjs-root.js
deleted file mode 100644
index f9c2f76d..00000000
--- a/client/src/lib/sandboxed-grapesjs-root.js
+++ /dev/null
@@ -1,635 +0,0 @@
-'use strict';
-
-import './public-path';
-
-import React, {Component} from 'react';
-import ReactDOM from 'react-dom';
-import {TranslationRoot, withTranslation} from './i18n';
-import {parentRPC, UntrustedContentRoot} from './untrusted';
-import PropTypes from "prop-types";
-import {getPublicUrl, getSandboxUrl, getTrustedUrl} from "./urls";
-import {base, unbase} from "../../../shared/templates";
-import mjml2html from "./mjml";
-
-import 'grapesjs/dist/css/grapes.min.css';
-import grapesjs from 'grapesjs';
-
-import 'grapesjs-mjml';
-
-import 'grapesjs-preset-newsletter';
-import 'grapesjs-preset-newsletter/dist/grapesjs-preset-newsletter.css';
-
-import "./sandboxed-grapesjs.scss";
-
-import axios from './axios';
-import {GrapesJSSourceType} from "./sandboxed-grapesjs-shared";
-import {withComponentMixins} from "./decorator-helpers";
-
-
-grapesjs.plugins.add('mailtrain-remove-buttons', (editor, opts = {}) => {
- // This needs to be done in on-load and after gjs plugin because grapesjs-preset-newsletter tries to set titles to all buttons (including those we remove)
- // see https://github.com/artf/grapesjs-preset-newsletter/blob/e0a91636973a5a1481e9d7929e57a8869b1db72e/src/index.js#L248
- editor.on('load', () => {
- const panelManager = editor.Panels;
- panelManager.removeButton('options','fullscreen');
- panelManager.removeButton('options','export-template');
- });
-});
-
-
-@withComponentMixins([
- withTranslation
-])
-export class GrapesJSSandbox extends Component {
- constructor(props) {
- super(props);
-
- this.initialized = false;
-
- this.state = {
- assets: null
- };
- }
-
- static propTypes = {
- entityTypeId: PropTypes.string,
- entityId: PropTypes.number,
- initialSource: PropTypes.string,
- initialStyle: PropTypes.string,
- sourceType: PropTypes.string
- }
-
- async exportState(method, params) {
- const props = this.props;
-
- const editor = this.editor;
-
- // If exportState comes during text editing (via RichTextEditor), we need to cancel the editing, so that the
- // text being edited is stored in the model
- const sel = editor.getSelected();
- if (sel && sel.view && sel.view.disableEditing) {
- sel.view.disableEditing();
- }
-
- const trustedUrlBase = getTrustedUrl();
- const sandboxUrlBase = getSandboxUrl();
- const publicUrlBase = getPublicUrl();
-
- const source = unbase(editor.getHtml(), trustedUrlBase, sandboxUrlBase, publicUrlBase, true);
- const style = unbase(editor.getCss(), trustedUrlBase, sandboxUrlBase, publicUrlBase, true);
-
- let html;
-
- if (props.sourceType === GrapesJSSourceType.MJML) {
- const preMjml = '';
- const postMjml = ' ';
- const mjml = preMjml + source + postMjml;
-
- const mjmlRes = mjml2html(mjml);
- html = mjmlRes.html;
-
- } else if (props.sourceType === GrapesJSSourceType.HTML) {
- const commandManager = editor.Commands;
-
- const cmdGetCode = commandManager.get('gjs-get-inlined-html');
- const htmlBody = cmdGetCode.run(editor);
-
- const preHtml = ' ';
- const postHtml = '';
- html = preHtml + unbase(htmlBody, trustedUrlBase, sandboxUrlBase, publicUrlBase, true) + postHtml;
- }
-
-
- return {
- html,
- style: style,
- source: source
- };
- }
-
- async fetchAssets() {
- const props = this.props;
- const resp = await axios.get(getSandboxUrl(`rest/files-list/${props.entityTypeId}/file/${props.entityId}`));
- this.setState({
- assets: resp.data.map( f => ({type: 'image', src: getPublicUrl(`files/${props.entityTypeId}/file/${props.entityId}/${f.filename}`)}) )
- });
- }
-
- componentDidMount() {
- // noinspection JSIgnoredPromiseFromCall
- this.fetchAssets();
- }
-
- componentDidUpdate() {
- if (!this.initialized && this.state.assets !== null) {
- this.initGrapesJs();
- this.initialized = true;
- }
- }
-
- initGrapesJs() {
- const props = this.props;
-
- parentRPC.setMethodHandler('exportState', ::this.exportState);
-
- const trustedUrlBase = getTrustedUrl();
- const sandboxUrlBase = getSandboxUrl();
- const publicUrlBase = getPublicUrl();
-
- const config = {
- noticeOnUnload: false,
- container: this.canvasNode,
- height: '100%',
- width: '100%',
- storageManager:{
- type: 'none'
- },
- assetManager: {
- assets: this.state.assets,
- upload: getSandboxUrl(`grapesjs/upload/${this.props.entityTypeId}/${this.props.entityId}`),
- uploadText: 'Drop images here or click to upload',
- headers: {
- 'X-CSRF-TOKEN': '{{csrfToken}}',
- },
- autoAdd: true
- },
- styleManager: {
- clearProperties: true,
- },
- fromElement: false,
- components: '',
- style: '',
- plugins: [
- ],
- pluginsOpts: {
- }
- };
-
- let defaultSource, defaultStyle;
-
- if (props.sourceType === GrapesJSSourceType.MJML) {
- defaultSource =
- '\n' +
- ' \n' +
- ' \n' +
- ' Lorem Ipsum... \n' +
- ' \n' +
- ' \n' +
- ' ';
-
- defaultStyle = '';
-
- config.plugins.push('gjs-mjml');
- config.pluginsOpts['gjs-mjml'] = {
- preMjml: '',
- postMjml: ' '
- };
-
- } else if (props.sourceType === GrapesJSSourceType.HTML) {
- defaultSource =
- '\n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' View in browser\n' +
- ' \n' +
- ' \n' +
- ' \n' +
- '
\n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' GrapesJS Newsletter Builder\n' +
- ' \n' +
- '
\n' +
- ' \n' +
- ' \n' +
- '
\n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' Build your newsletters faster than ever\n' +
- ' \n' +
- ' \n' +
- ' Import, build, test and export responsive newsletter templates faster than ever using the GrapesJS Newsletter Builder.\n' +
- '
\n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- '
\n' +
- ' \n' +
- ' \n' +
- '
\n' +
- ' \n' +
- ' \n' +
- '
\n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' Built-in Blocks\n' +
- ' \n' +
- ' Drag and drop built-in blocks from the right panel and style them in a matter of seconds\n' +
- '
\n' +
- ' \n' +
- ' \n' +
- '
\n' +
- ' \n' +
- ' \n' +
- '
\n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' Toggle images\n' +
- ' \n' +
- ' Build a good looking newsletter even without images enabled by the email clients\n' +
- '
\n' +
- ' \n' +
- ' \n' +
- '
\n' +
- ' \n' +
- ' \n' +
- '
\n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' Test it\n' +
- ' \n' +
- ' You can send email tests directly from the editor and check how are looking on your email clients\n' +
- '
\n' +
- ' \n' +
- ' \n' +
- '
\n' +
- ' \n' +
- ' \n' +
- '
\n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' Responsive\n' +
- ' \n' +
- ' Using the device manager you\'ll always send a fully responsive contents\n' +
- '
\n' +
- ' \n' +
- ' \n' +
- '
\n' +
- ' \n' +
- ' \n' +
- '
\n' +
- ' \n' +
- ' \n' +
- '
\n' +
- ' \n' +
- ' \n' +
- ' \n' +
- '
\n' +
- ' \n' +
- ' \n' +
- '
';
-
- defaultStyle =
- '.link {\n' +
- ' color: rgb(217, 131, 166);\n' +
- ' }\n' +
- ' .row{\n' +
- ' vertical-align:top;\n' +
- ' }\n' +
- ' .main-body{\n' +
- ' min-height:150px;\n' +
- ' padding: 5px;\n' +
- ' width:100%;\n' +
- ' height:100%;\n' +
- ' background-color:rgb(234, 236, 237);\n' +
- ' }\n' +
- ' .c926{\n' +
- ' color:rgb(158, 83, 129);\n' +
- ' width:100%;\n' +
- ' font-size:50px;\n' +
- ' }\n' +
- ' .cell.c849{\n' +
- ' width:11%;\n' +
- ' }\n' +
- ' .c1144{\n' +
- ' padding: 10px;\n' +
- ' font-size:17px;\n' +
- ' font-weight: 300;\n' +
- ' }\n' +
- ' .card{\n' +
- ' min-height:150px;\n' +
- ' padding: 5px;\n' +
- ' margin-bottom:20px;\n' +
- ' height:0px;\n' +
- ' }\n' +
- ' .card-cell{\n' +
- ' background-color:rgb(255, 255, 255);\n' +
- ' overflow:hidden;\n' +
- ' border-radius: 3px;\n' +
- ' padding: 0;\n' +
- ' text-align:center;\n' +
- ' }\n' +
- ' .card.sector{\n' +
- ' background-color:rgb(255, 255, 255);\n' +
- ' border-radius: 3px;\n' +
- ' border-collapse:separate;\n' +
- ' }\n' +
- ' .c1271{\n' +
- ' width:100%;\n' +
- ' margin: 0 0 15px 0;\n' +
- ' font-size:50px;\n' +
- ' color:rgb(120, 197, 214);\n' +
- ' line-height:250px;\n' +
- ' text-align:center;\n' +
- ' }\n' +
- ' .table100{\n' +
- ' width:100%;\n' +
- ' }\n' +
- ' .c1357{\n' +
- ' min-height:150px;\n' +
- ' padding: 5px;\n' +
- ' margin: auto;\n' +
- ' height:0px;\n' +
- ' }\n' +
- ' .darkerfont{\n' +
- ' color:rgb(65, 69, 72);\n' +
- ' }\n' +
- ' .button{\n' +
- ' font-size:12px;\n' +
- ' padding: 10px 20px;\n' +
- ' background-color:rgb(217, 131, 166);\n' +
- ' color:rgb(255, 255, 255);\n' +
- ' text-align:center;\n' +
- ' border-radius: 3px;\n' +
- ' font-weight:300;\n' +
- ' }\n' +
- ' .table100.c1437{\n' +
- ' text-align:left;\n' +
- ' }\n' +
- ' .cell.cell-bottom{\n' +
- ' text-align:center;\n' +
- ' height:51px;\n' +
- ' }\n' +
- ' .card-title{\n' +
- ' font-size:25px;\n' +
- ' font-weight:300;\n' +
- ' color:rgb(68, 68, 68);\n' +
- ' }\n' +
- ' .card-content{\n' +
- ' font-size:13px;\n' +
- ' line-height:20px;\n' +
- ' color:rgb(111, 119, 125);\n' +
- ' padding: 10px 20px 0 20px;\n' +
- ' vertical-align:top;\n' +
- ' }\n' +
- ' .container{\n' +
- ' font-family: Helvetica, serif;\n' +
- ' min-height:150px;\n' +
- ' padding: 5px;\n' +
- ' margin:auto;\n' +
- ' height:0px;\n' +
- ' width:90%;\n' +
- ' max-width:550px;\n' +
- ' }\n' +
- ' .cell.c856{\n' +
- ' vertical-align:middle;\n' +
- ' }\n' +
- ' .container-cell{\n' +
- ' vertical-align:top;\n' +
- ' font-size:medium;\n' +
- ' padding-bottom:50px;\n' +
- ' }\n' +
- ' .c1790{\n' +
- ' min-height:150px;\n' +
- ' padding: 5px;\n' +
- ' margin:auto;\n' +
- ' height:0px;\n' +
- ' }\n' +
- ' .table100.c1790{\n' +
- ' min-height:30px;\n' +
- ' border-collapse:separate;\n' +
- ' margin: 0 0 10px 0;\n' +
- ' }\n' +
- ' .browser-link{\n' +
- ' font-size:12px;\n' +
- ' }\n' +
- ' .top-cell{\n' +
- ' text-align:right;\n' +
- ' color:rgb(152, 156, 165);\n' +
- ' }\n' +
- ' .table100.c1357{\n' +
- ' margin: 0;\n' +
- ' border-collapse:collapse;\n' +
- ' }\n' +
- ' .c1769{\n' +
- ' width:30%;\n' +
- ' }\n' +
- ' .c1776{\n' +
- ' width:70%;\n' +
- ' }\n' +
- ' .c1766{\n' +
- ' margin: 0 auto 10px 0;\n' +
- ' padding: 5px;\n' +
- ' width:100%;\n' +
- ' min-height:30px;\n' +
- ' }\n' +
- ' .cell.c1769{\n' +
- ' width:11%;\n' +
- ' }\n' +
- ' .cell.c1776{\n' +
- ' vertical-align:middle;\n' +
- ' }\n' +
- ' .c1542{\n' +
- ' margin: 0 auto 10px auto;\n' +
- ' padding:5px;\n' +
- ' width:100%;\n' +
- ' }\n' +
- ' .card-footer{\n' +
- ' padding: 20px 0;\n' +
- ' text-align:center;\n' +
- ' }\n' +
- ' .c2280{\n' +
- ' height:150px;\n' +
- ' margin:0 auto 10px auto;\n' +
- ' padding:5px 5px 5px 5px;\n' +
- ' width:100%;\n' +
- ' }\n' +
- ' .c2421{\n' +
- ' padding:10px;\n' +
- ' }\n' +
- ' .c2577{\n' +
- ' padding:10px;\n' +
- ' }\n' +
- ' .footer{\n' +
- ' margin-top: 50px;\n' +
- ' color:rgb(152, 156, 165);\n' +
- ' text-align:center;\n' +
- ' font-size:11px;\n' +
- ' padding: 5px;\n' +
- ' }\n' +
- ' .quote {\n' +
- ' font-style: italic;\n' +
- ' }\n' +
- ' .list-item{\n' +
- ' height:auto;\n' +
- ' width:100%;\n' +
- ' margin: 0 auto 10px auto;\n' +
- ' padding: 5px;\n' +
- ' }\n' +
- ' .list-item-cell{\n' +
- ' background-color:rgb(255, 255, 255);\n' +
- ' border-radius: 3px;\n' +
- ' overflow: hidden;\n' +
- ' padding: 0;\n' +
- ' }\n' +
- ' .list-cell-left{\n' +
- ' width:30%;\n' +
- ' padding: 0;\n' +
- ' }\n' +
- ' .list-cell-right{\n' +
- ' width:70%;\n' +
- ' color:rgb(111, 119, 125);\n' +
- ' font-size:13px;\n' +
- ' line-height:20px;\n' +
- ' padding: 10px 20px 0px 20px;\n' +
- ' }\n' +
- ' .list-item-content{\n' +
- ' border-collapse: collapse;\n' +
- ' margin: 0 auto;\n' +
- ' padding: 5px;\n' +
- ' height:150px;\n' +
- ' width:100%;\n' +
- ' }\n' +
- ' .list-item-image{\n' +
- ' color:rgb(217, 131, 166);\n' +
- ' font-size:45px;\n' +
- ' width: 100%;\n' +
- ' }\n' +
- ' .grid-item-image{\n' +
- ' line-height:150px;\n' +
- ' font-size:50px;\n' +
- ' color:rgb(120, 197, 214);\n' +
- ' margin-bottom:15px;\n' +
- ' width:100%;\n' +
- ' }\n' +
- ' .grid-item-row {\n' +
- ' margin: 0 auto 10px;\n' +
- ' padding: 5px 0;\n' +
- ' width: 100%;\n' +
- ' }\n' +
- ' .grid-item-card {\n' +
- ' width:100%;\n' +
- ' padding: 5px 0;\n' +
- ' margin-bottom: 10px;\n' +
- ' }\n' +
- ' .grid-item-card-cell{\n' +
- ' background-color:rgb(255, 255, 255);\n' +
- ' overflow: hidden;\n' +
- ' border-radius: 3px;\n' +
- ' text-align:center;\n' +
- ' padding: 0;\n' +
- ' }\n' +
- ' .grid-item-card-content{\n' +
- ' font-size:13px;\n' +
- ' color:rgb(111, 119, 125);\n' +
- ' padding: 0 10px 20px 10px;\n' +
- ' width:100%;\n' +
- ' line-height:20px;\n' +
- ' }\n' +
- ' .grid-item-cell2-l{\n' +
- ' vertical-align:top;\n' +
- ' padding-right:10px;\n' +
- ' width:50%;\n' +
- ' }\n' +
- ' .grid-item-cell2-r{\n' +
- ' vertical-align:top;\n' +
- ' padding-left:10px;\n' +
- ' width:50%;\n' +
- ' }';
-
- config.plugins.push('gjs-preset-newsletter');
- }
-
- config.components = props.initialSource ? base(props.initialSource, trustedUrlBase, sandboxUrlBase, publicUrlBase) : defaultSource;
- config.style = props.initialStyle ? base(props.initialStyle, trustedUrlBase, sandboxUrlBase, publicUrlBase) : defaultStyle;
-
- config.plugins.push('mailtrain-remove-buttons');
-
- this.editor = grapesjs.init(config);
- }
-
- render() {
- return (
-
-
this.canvasNode = node}/>
-
- );
- }
-}
-
-
-export default function() {
- parentRPC.init();
-
- ReactDOM.render(
-
- } />
- ,
- document.getElementById('root')
- );
-};
-
-
diff --git a/client/src/lib/sandboxed-grapesjs-shared.js b/client/src/lib/sandboxed-grapesjs-shared.js
deleted file mode 100644
index 282283dc..00000000
--- a/client/src/lib/sandboxed-grapesjs-shared.js
+++ /dev/null
@@ -1,11 +0,0 @@
-'use strict';
-
-export const GrapesJSSourceType = {
- MJML: 'mjml',
- HTML: 'html'
-};
-
-export const getGrapesJSSourceTypeOptions = t => [
- {key: GrapesJSSourceType.MJML, label: t('mjml')},
- {key: GrapesJSSourceType.HTML, label: t('html')}
-];
diff --git a/client/src/lib/sandboxed-grapesjs.js b/client/src/lib/sandboxed-grapesjs.js
deleted file mode 100644
index 42fa23a4..00000000
--- a/client/src/lib/sandboxed-grapesjs.js
+++ /dev/null
@@ -1,89 +0,0 @@
-'use strict';
-
-import React, {Component} from 'react';
-import {withTranslation} from './i18n';
-import PropTypes from "prop-types";
-import styles from "./sandboxed-grapesjs.scss";
-
-import {UntrustedContentHost} from './untrusted';
-import {Icon} from "./bootstrap-components";
-import {getTrustedUrl} from "./urls";
-import {withComponentMixins} from "./decorator-helpers";
-import {GrapesJSSourceType} from "./sandboxed-grapesjs-shared";
-
-@withComponentMixins([
- withTranslation
-], ['exportState'])
-export class GrapesJSHost extends Component {
- constructor(props) {
- super(props);
-
- this.state = {
- fullscreen: false
- };
-
- this.contentNodeRefHandler = node => this.contentNode = node;
- }
-
- static propTypes = {
- entityTypeId: PropTypes.string,
- entity: PropTypes.object,
- initialSource: PropTypes.string,
- initialStyle: PropTypes.string,
- sourceType: PropTypes.string,
- title: PropTypes.string,
- onSave: PropTypes.func,
- canSave: PropTypes.bool,
- onTestSend: PropTypes.func,
- onShowExport: PropTypes.func,
- onFullscreenAsync: PropTypes.func
- }
-
- async toggleFullscreenAsync() {
- const fullscreen = !this.state.fullscreen;
- this.setState({
- fullscreen
- });
- await this.props.onFullscreenAsync(fullscreen);
- }
-
- async exportState() {
- return await this.contentNode.ask('exportState');
- }
-
- render() {
- const t = this.props.t;
-
- const editorData = {
- entityTypeId: this.props.entityTypeId,
- entityId: this.props.entity.id,
- initialSource: this.props.initialSource,
- initialStyle: this.props.initialStyle,
- sourceType: this.props.sourceType
- };
-
- const tokenData = {
- entityTypeId: this.props.entityTypeId,
- entityId: this.props.entity.id
- };
-
- return (
-
-
-
- {this.state.fullscreen &&
}
-
{this.props.title}
-
-
-
-
-
- );
- }
-}
diff --git a/client/src/lib/sandboxed-grapesjs.scss b/client/src/lib/sandboxed-grapesjs.scss
deleted file mode 100644
index 1e85d731..00000000
--- a/client/src/lib/sandboxed-grapesjs.scss
+++ /dev/null
@@ -1,18 +0,0 @@
-@import "sandbox-common";
-
-:global .grapesjs-body {
- margin: 0px;
-}
-
-:global .gjs-editor-cont {
- position: absolute;
-}
-
-:global .gjs-devices-c .gjs-devices {
- padding-right: 15px;
-}
-
-:global .gjs-pn-devices-c, :global .gjs-pn-views {
- padding: 4px;
-}
-
diff --git a/client/src/lib/sandboxed-mosaico-root.js b/client/src/lib/sandboxed-mosaico-root.js
deleted file mode 100644
index 4a3dfd5a..00000000
--- a/client/src/lib/sandboxed-mosaico-root.js
+++ /dev/null
@@ -1,158 +0,0 @@
-'use strict';
-
-import './public-path';
-
-import React, {Component} from 'react';
-import ReactDOM from 'react-dom';
-import {TranslationRoot, withTranslation} from './i18n';
-import {parentRPC, UntrustedContentRoot} from './untrusted';
-import PropTypes from "prop-types";
-import {getPublicUrl, getSandboxUrl, getTrustedUrl} from "./urls";
-import {base, unbase} from "../../../shared/templates";
-import {withComponentMixins} from "./decorator-helpers";
-import juice from "juice";
-
-
-@withComponentMixins([
- withTranslation
-])
-class MosaicoSandbox extends Component {
- constructor(props) {
- super(props);
- this.viewModel = null;
- this.state = {
- };
- }
-
- static propTypes = {
- entityTypeId: PropTypes.string,
- entityId: PropTypes.number,
- templateId: PropTypes.number,
- templatePath: PropTypes.string,
- initialModel: PropTypes.string,
- initialMetadata: PropTypes.string
- }
-
- async exportState(method, params) {
- const trustedUrlBase = getTrustedUrl();
- const sandboxUrlBase = getSandboxUrl();
- const publicUrlBase = getPublicUrl();
-
-
- /* juice is called to inline css styles of situations like this
-
-
- ...
-
-
- */
- let html = this.viewModel.export();
- html = juice(html);
-
- return {
- html: unbase(html, trustedUrlBase, sandboxUrlBase, publicUrlBase, true),
- model: unbase(this.viewModel.exportJSON(), trustedUrlBase, sandboxUrlBase, publicUrlBase),
- metadata: unbase(this.viewModel.exportMetadata(), trustedUrlBase, sandboxUrlBase, publicUrlBase)
- };
- }
-
- componentDidMount() {
- parentRPC.setMethodHandler('exportState', ::this.exportState);
-
- if (!Mosaico.isCompatible()) {
- alert('Update your browser!');
- return;
- }
-
- const plugins = [...window.mosaicoPlugins];
-
- plugins.push(viewModel => {
- this.viewModel = viewModel;
- });
-
- // (Custom) HTML postRenderers
- plugins.push(viewModel => {
- viewModel.originalExportHTML = viewModel.exportHTML;
- viewModel.exportHTML = () => {
- let html = viewModel.originalExportHTML();
-
- // Chrome workaround begin -----------------------------------------------------------------------------------
- // Chrome v. 74 (and likely other versions too) has problem with how KO sets data during export.
- // As the result, the images that have been in the template from previous editing (i.e. before page refresh)
- // get lost. The code below refreshes the KO binding, thus effectively reloading the images.
- const isChrome = !!window.chrome && (!!window.chrome.webstore || !!window.chrome.runtime);
- if (isChrome) {
- ko.cleanNode(document.body);
- ko.applyBindings(viewModel, document.body);
- }
- // Chrome workaround end -------------------------------------------------------------------------------------
-
- for (const portRender of window.mosaicoHTMLPostRenderers) {
- html = postRender(html);
- }
- return html;
- };
- });
-
- // Custom convertedUrl (https://github.com/voidlabs/mosaico/blob/a359e263f1af5cf05e2c2d56c771732f2ef6c8c6/src/js/app.js#L42)
- // which does not complain about mismatch of domains between TRUSTED and PUBLIC
- plugins.push(viewModel => {
- ko.bindingHandlers.wysiwygSrc.convertedUrl = (src, method, width, height) => getTrustedUrl(`mosaico/img?src=${encodeURIComponent(src)}&method=${encodeURIComponent(method)}¶ms=${width},${height}`);
- });
-
- plugins.unshift(vm => {
- // This is an override of the default paths in Mosaico
- vm.logoPath = getTrustedUrl('static/mosaico/rs/img/mosaico32.png');
- vm.logoUrl = '#';
- });
-
- const config = {
- imgProcessorBackend: getTrustedUrl('mosaico/img'),
- emailProcessorBackend: getSandboxUrl('mosaico/dl'),
- fileuploadConfig: {
- url: getSandboxUrl(`mosaico/upload/${this.props.entityTypeId}/${this.props.entityId}`)
- },
- strings: window.mosaicoLanguageStrings
- };
-
- const trustedUrlBase = getTrustedUrl();
- const sandboxUrlBase = getSandboxUrl();
- const publicUrlBase = getPublicUrl();
- const metadata = this.props.initialMetadata && JSON.parse(base(this.props.initialMetadata, trustedUrlBase, sandboxUrlBase, publicUrlBase));
- const model = this.props.initialModel && JSON.parse(base(this.props.initialModel, trustedUrlBase, sandboxUrlBase, publicUrlBase));
- const template = this.props.templateId ? getSandboxUrl(`mosaico/templates/${this.props.templateId}/index.html`) : this.props.templatePath;
-
- const allPlugins = plugins.concat(window.mosaicoPlugins);
-
- Mosaico.start(config, template, metadata, model, allPlugins);
- }
-
- render() {
- return
;
- }
-}
-
-
-
-export default function() {
- parentRPC.init();
-
- ReactDOM.render(
-
- } />
- ,
- document.getElementById('root')
- );
-};
-
-
diff --git a/client/src/lib/sandboxed-mosaico.js b/client/src/lib/sandboxed-mosaico.js
deleted file mode 100644
index 7704c0d1..00000000
--- a/client/src/lib/sandboxed-mosaico.js
+++ /dev/null
@@ -1,90 +0,0 @@
-'use strict';
-
-import React, {Component} from 'react';
-import {withTranslation} from './i18n';
-import PropTypes from "prop-types";
-import styles from "./sandboxed-mosaico.scss";
-
-import {UntrustedContentHost} from './untrusted';
-import {Icon} from "./bootstrap-components";
-import {getTrustedUrl} from "./urls";
-import {withComponentMixins} from "./decorator-helpers";
-
-
-@withComponentMixins([
- withTranslation
-], ['exportState'])
-export class MosaicoHost extends Component {
- constructor(props) {
- super(props);
-
- this.state = {
- fullscreen: false
- };
-
- this.contentNodeRefHandler = node => this.contentNode = node;
- }
-
- static propTypes = {
- entityTypeId: PropTypes.string,
- entity: PropTypes.object,
- title: PropTypes.string,
- onSave: PropTypes.func,
- canSave: PropTypes.bool,
- onTestSend: PropTypes.func,
- onShowExport: PropTypes.func,
- onFullscreenAsync: PropTypes.func,
- templateId: PropTypes.number,
- templatePath: PropTypes.string,
- initialModel: PropTypes.string,
- initialMetadata: PropTypes.string
- }
-
- async toggleFullscreenAsync() {
- const fullscreen = !this.state.fullscreen;
- this.setState({
- fullscreen
- });
- await this.props.onFullscreenAsync(fullscreen);
- }
-
- async exportState() {
- return await this.contentNode.ask('exportState');
- }
-
- render() {
- const t = this.props.t;
-
- const editorData = {
- entityTypeId: this.props.entityTypeId,
- entityId: this.props.entity.id,
- templateId: this.props.templateId,
- templatePath: this.props.templatePath,
- initialModel: this.props.initialModel,
- initialMetadata: this.props.initialMetadata
- };
-
- const tokenData = {
- entityTypeId: this.props.entityTypeId,
- entityId: this.props.entity.id
- };
-
- return (
-
-
-
- {this.state.fullscreen &&
}
-
{this.props.title}
-
-
-
-
-
- );
- }
-}
diff --git a/client/src/lib/sandboxed-mosaico.scss b/client/src/lib/sandboxed-mosaico.scss
deleted file mode 100644
index 35c5933f..00000000
--- a/client/src/lib/sandboxed-mosaico.scss
+++ /dev/null
@@ -1,8 +0,0 @@
-@import "sandbox-common";
-
-:global .mo-standalone {
- top: 0px;
- bottom: 0px;
- width: 100%;
- position: absolute;
-}
diff --git a/client/src/lib/styles.scss b/client/src/lib/styles.scss
deleted file mode 100644
index 8d2daff0..00000000
--- a/client/src/lib/styles.scss
+++ /dev/null
@@ -1,185 +0,0 @@
-@import "../scss/variables.scss";
-
-.toolbar {
- float: right;
- margin-bottom: 15px;
-}
-
-.form { // This is here to give the styles below higher priority than Bootstrap has
- :global .DayPicker {
- border: $input-border-width solid $input-border-color;
- border-radius: $input-border-radius;
- padding: $input-padding-y $input-padding-x;
- }
-
- :global .form-horizontal .control-label {
- display: block;
- }
-
- :global .form-control[disabled] {
- cursor: default;
- background-color: #eeeeee;
- opacity: 1;
- }
-
- :global .ace_editor {
- border: 1px solid #ccc;
- }
-
- .buttonRow:last-child {
- // This is to move Save/Delete buttons a bit down
- margin-top: 15px;
- }
-}
-
-.staticFormGroup {
- margin-bottom: 15px;
-}
-
-.dayPickerWrapper {
- text-align: right;
-}
-
-.buttonRow {
-}
-
-.buttonRow > * {
- margin-right: 15px;
-}
-
-.buttonRow > *:last-child {
- margin-right: 0px;
-}
-
-.formDisabled {
- background-color: #eeeeee;
- opacity: 1;
-}
-
-.formStatus {
- padding-top: 5px;
- padding-bottom: 5px;
-}
-
-.dataTableTable {
- overflow-x: auto;
-}
-
-.actionLinks > * {
- margin-right: 8px;
-}
-
-.actionLinks > *:last-child {
- margin-right: 0px;
-}
-
-.tableSelectDropdown {
- margin-bottom: 15px;
-}
-
-.tableSelectTable.tableSelectTableHidden {
- display: none;
- height: 0px;
- margin-top: -15px;
-}
-
-.tableSelectDropdown input[readonly] {
- background-color: white;
-}
-
-:global h3.legend {
- font-size: 21px;
- margin-bottom: 20px;
-}
-
-.tertiaryNav {
- justify-content: flex-end;
- flex-grow: 1;
- align-self: center;
-
- margin-left: 5px;
- margin-right: 5px;
-
- :global .nav-item .nav-link {
- padding: 3px 10px;
- }
-}
-
-.colorPickerSwatchWrapper {
- padding: 7px;
- background: #fff;
- border: 1px solid #AAB2BD;
- border-radius: 4px;
- display: inline-block;
- cursor: pointer;
-
- .colorPickerSwatchColor {
- width: 60px;
- height: 18px;
- borderRadius: 2px;
- }
-}
-
-.colorPickerWrapper {
- text-align: right;
-}
-
-.checkboxText{
- padding-top: 3px;
-}
-
-.dropZone{
- padding-top: 20px;
- padding-bottom: 20px;
- margin-bottom: 3px;
- margin-top: 3px;
- border: 2px solid #E6E9ED;
- border-radius: 5px;
- background-color: #FAFAD2;
- text-align: center;
- font-size: 20px;
- color: #808080;
-
- p:last-child {
- margin-bottom: 0px;
- }
-}
-
-.dropZoneActive{
- border-color: #90EE90;
- color: #000;
- background-color: #DDFFDD;
-}
-
-
-.untrustedContent {
- border: 0px none;
- width: 100%;
- overflow: hidden;
-}
-
-.withElementInFullscreen {
- height: 0px;
- overflow: hidden;
-}
-
-.iconDisabled {
- color: $link-color;
- text-decoration: $link-decoration;
-}
-
-.errorsList {
- margin-bottom: 0px;
-}
-
-
-:global .modal-dialog {
- @media (min-width: 768px) {
- max-width: 700px;
- }
-
- @media (min-width: 1000px) {
- max-width: 900px;
- }
-}
-
diff --git a/client/src/lib/table.js b/client/src/lib/table.js
deleted file mode 100644
index 0d11194f..00000000
--- a/client/src/lib/table.js
+++ /dev/null
@@ -1,424 +0,0 @@
-'use strict';
-
-import React, {Component} from 'react';
-import ReactDOMServer from 'react-dom/server';
-import PropTypes from 'prop-types';
-import {withTranslation} from './i18n';
-
-import jQuery from 'jquery';
-
-import 'datatables.net';
-import 'datatables.net-bs4';
-import 'datatables.net-bs4/css/dataTables.bootstrap4.css';
-
-import axios from './axios';
-
-import {withPageHelpers} from './page'
-import {withAsyncErrorHandler, withErrorHandling} from './error-handling';
-import styles from "./styles.scss";
-import {getUrl} from "./urls";
-import {withComponentMixins} from "./decorator-helpers";
-
-//dtFactory();
-//dtSelectFactory();
-
-
-const TableSelectMode = {
- NONE: 0,
- SINGLE: 1,
- MULTI: 2
-};
-
-@withComponentMixins([
- withTranslation,
- withErrorHandling,
- withPageHelpers
-], ['refresh'])
-class Table extends Component {
- constructor(props) {
- super(props);
- this.mounted = false;
- this.selectionMap = this.getSelectionMap(props);
- }
-
- static propTypes = {
- dataUrl: PropTypes.string,
- data: PropTypes.array,
- columns: PropTypes.array,
- selectMode: PropTypes.number,
- selection: PropTypes.oneOfType([PropTypes.array, PropTypes.string, PropTypes.number]),
- selectionKeyIndex: PropTypes.number,
- selectionAsArray: PropTypes.bool,
- onSelectionChangedAsync: PropTypes.func,
- onSelectionDataAsync: PropTypes.func,
- withHeader: PropTypes.bool,
- refreshInterval: PropTypes.number,
- pageLength: PropTypes.number
- }
-
- static defaultProps = {
- selectMode: TableSelectMode.NONE,
- selectionKeyIndex: 0,
- pageLength: 50
- }
-
- refresh() {
- if (this.table) {
- this.table.rows().draw('page');
- }
- }
-
- getSelectionMap(props) {
- let selArray = [];
- if (props.selectMode === TableSelectMode.SINGLE && !this.props.selectionAsArray) {
- if (props.selection !== null && props.selection !== undefined) {
- selArray = [props.selection];
- } else {
- selArray = [];
- }
- } else if ((props.selectMode === TableSelectMode.SINGLE && this.props.selectionAsArray) || props.selectMode === TableSelectMode.MULTI) {
- selArray = props.selection || [];
- }
-
- const selMap = new Map();
-
- for (const elem of selArray) {
- selMap.set(elem, undefined);
- }
-
- if (props.data) {
- for (const rowData of props.data) {
- const key = rowData[props.selectionKeyIndex];
- if (selMap.has(key)) {
- selMap.set(key, rowData);
- }
- }
-
- } else if (this.table) {
- this.table.rows().every(function() {
- const rowData = this.data();
- const key = rowData[props.selectionKeyIndex];
- if (selMap.has(key)) {
- selMap.set(key, rowData);
- }
- });
- }
-
- return selMap;
- }
-
- updateSelectInfo() {
- if (!this.jqSelectInfo) {
- return; // If the table is updated very quickly after mounting, the datatable may not be initialized yet.
- }
-
- const t = this.props.t;
-
- const count = this.selectionMap.size;
- if (this.selectionMap.size > 0) {
- const jqInfo = jQuery('
' + t('countEntriesSelected', { count }) + ' ');
- const jqDeselectLink = jQuery('
Deselect all. ').on('click', ::this.deselectAll);
-
- this.jqSelectInfo.empty().append(jqInfo).append(jqDeselectLink);
- } else {
- this.jqSelectInfo.empty();
- }
- }
-
- @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(getUrl(this.props.dataUrl), data);
- callback(response.data);
- }
-
- @withAsyncErrorHandler
- async fetchAndNotifySelectionData() {
- if (this.props.onSelectionDataAsync) {
- if (!this.props.data) {
- const keysToFetch = [];
- for (const pair of this.selectionMap.entries()) {
- if (!pair[1]) {
- keysToFetch.push(pair[0]);
- }
- }
-
- if (keysToFetch.length > 0) {
- const response = await axios.post(getUrl(this.props.dataUrl), {
- operation: 'getBy',
- column: this.props.selectionKeyIndex,
- values: keysToFetch
- });
-
- for (const row of response.data) {
- const key = row[this.props.selectionKeyIndex];
- if (this.selectionMap.has(key)) {
- this.selectionMap.set(key, row);
- }
- }
- }
- }
-
- // noinspection JSIgnoredPromiseFromCall
- this.notifySelection(this.props.onSelectionDataAsync, this.selectionMap);
- }
- }
-
- shouldComponentUpdate(nextProps, nextState) {
- const nextSelectionMap = this.getSelectionMap(nextProps);
-
- let updateDueToSelectionChange = false;
- if (nextSelectionMap.size !== this.selectionMap.size) {
- updateDueToSelectionChange = true;
- } else {
- for (const key of this.selectionMap.keys()) {
- if (!nextSelectionMap.has(key)) {
- updateDueToSelectionChange = true;
- break;
- }
- }
- }
-
- this.selectionMap = nextSelectionMap;
-
- return updateDueToSelectionChange || this.props.data !== nextProps.data || this.props.dataUrl !== nextProps.dataUrl;
- }
-
- componentDidMount() {
- this.mounted = true;
-
- const columns = this.props.columns.slice();
-
- // XSS protection and actions rendering
- for (const column of columns) {
- if (column.actions) {
- const createdCellFn = (td, data, rowData) => {
- const linksContainer = jQuery(`
`);
-
- let actions = column.actions(rowData);
- let options = {};
-
- if (!Array.isArray(actions)) {
- options = actions;
- actions = actions.actions;
- }
-
- for (const action of actions) {
- if (action.action) {
- const html = ReactDOMServer.renderToStaticMarkup(
{action.label} );
- const elem = jQuery(html);
- elem.click((evt) => { evt.preventDefault(); action.action(this) });
- linksContainer.append(elem);
-
- } else if (action.link) {
- const html = ReactDOMServer.renderToStaticMarkup(
{action.label} );
- const elem = jQuery(html);
- elem.click((evt) => { evt.preventDefault(); this.navigateTo(action.link) });
- linksContainer.append(elem);
-
- } else if (action.href) {
- const html = ReactDOMServer.renderToStaticMarkup(
{action.label} );
- const elem = jQuery(html);
- linksContainer.append(elem);
-
- } else {
- const html = ReactDOMServer.renderToStaticMarkup(
{action.label} );
- const elem = jQuery(html);
- linksContainer.append(elem);
- }
- }
-
- if (options.refreshTimeout) {
- const currentMS = Date.now();
-
- if (!this.refreshTimeoutAt || this.refreshTimeoutAt > currentMS + options.refreshTimeout) {
- clearTimeout(this.refreshTimeoutId);
-
- this.refreshTimeoutAt = currentMS + options.refreshTimeout;
-
- this.refreshTimeoutId = setTimeout(() => {
- this.refreshTimeoutAt = 0;
- this.refresh();
- }, options.refreshTimeout);
- }
- }
-
- jQuery(td).html(linksContainer);
- };
-
- column.type = 'html';
- column.createdCell = createdCellFn;
-
- if (!('data' in column)) {
- column.data = null;
- column.orderable = false;
- column.searchable = false;
- }
- } else {
- const originalRender = column.render;
- column.render = (data, ...rest) => {
- if (originalRender) {
- const markup = originalRender(data, ...rest);
- return ReactDOMServer.renderToStaticMarkup(
{markup}
);
- } else {
- return ReactDOMServer.renderToStaticMarkup(
{data}
)
- }
- };
- }
-
- column.title = ReactDOMServer.renderToStaticMarkup(
{column.title}
);
- }
-
- const dtOptions = {
- columns,
- autoWidth: false,
- pageLength: this.props.pageLength,
- dom: // This overrides Bootstrap 4 settings. It may need to be updated if there are updates in the DataTables Bootstrap 4 plugin.
- "<'row'<'col-sm-12 col-md-6'l><'col-sm-12 col-md-6'f>>" +
- "<'row'<'col-sm-12'<'" + styles.dataTableTable + "'tr>>>" +
- "<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>"
- };
-
- const self = this;
- dtOptions.createdRow = function(row, data) {
- const rowKey = data[self.props.selectionKeyIndex];
-
- if (self.selectionMap.has(rowKey)) {
- jQuery(row).addClass('selected');
- }
-
- jQuery(row).on('click', () => {
- const selectionMap = self.selectionMap;
-
- if (self.props.selectMode === TableSelectMode.SINGLE) {
- if (selectionMap.size !== 1 || !selectionMap.has(rowKey)) {
- // noinspection JSIgnoredPromiseFromCall
- self.notifySelection(self.props.onSelectionChangedAsync, new Map([[rowKey, data]]));
- }
-
- } else if (self.props.selectMode === TableSelectMode.MULTI) {
- const newSelMap = new Map(selectionMap);
-
- if (selectionMap.has(rowKey)) {
- newSelMap.delete(rowKey);
- } else {
- newSelMap.set(rowKey, data);
- }
-
- // noinspection JSIgnoredPromiseFromCall
- self.notifySelection(self.props.onSelectionChangedAsync, newSelMap);
- }
- });
- };
-
- dtOptions.initComplete = function() {
- self.jqSelectInfo = jQuery('
');
- const jqWrapper = jQuery(self.domTable).parents('.dataTables_wrapper');
- jQuery('.dataTables_info', jqWrapper).after(self.jqSelectInfo);
-
- self.updateSelectInfo();
- };
-
- if (this.props.data) {
- dtOptions.data = this.props.data;
- } else {
- dtOptions.serverSide = true;
- dtOptions.ajax = ::this.fetchData;
- }
-
- this.table = jQuery(this.domTable).DataTable(dtOptions);
-
- if (this.props.refreshInterval) {
- this.refreshIntervalId = setInterval(() => this.refresh(), this.props.refreshInterval);
- }
-
- this.table.on('destroy.dt', () => {
- clearInterval(this.refreshIntervalId);
- clearTimeout(this.refreshTimeoutId);
- });
-
- // noinspection JSIgnoredPromiseFromCall
- this.fetchAndNotifySelectionData();
- }
-
- componentDidUpdate(prevProps, prevState) {
- if (this.props.data) {
- this.table.clear();
- this.table.rows.add(this.props.data);
- } else {
- // XXX: Changing URL changing from data to dataUrl is not implemented
- this.refresh();
- }
-
- const self = this;
- this.table.rows().every(function() {
- const key = this.data()[self.props.selectionKeyIndex];
- if (self.selectionMap.has(key)) {
- jQuery(this.node()).addClass('selected');
- } else {
- jQuery(this.node()).removeClass('selected');
- }
- });
-
- this.updateSelectInfo();
-
- // noinspection JSIgnoredPromiseFromCall
- this.fetchAndNotifySelectionData();
- }
-
- componentWillUnmount() {
- this.mounted = false;
- clearInterval(this.refreshIntervalId);
- clearTimeout(this.refreshTimeoutId);
- }
-
- async notifySelection(eventCallback, newSelectionMap) {
- if (this.mounted && eventCallback) {
- const selPairs = Array.from(newSelectionMap).sort((l, r) => l[0] - r[0]);
-
- let data = selPairs.map(entry => entry[1]);
- let sel = selPairs.map(entry => entry[0]);
-
- if (this.props.selectMode === TableSelectMode.SINGLE && !this.props.selectionAsArray) {
- if (sel.length) {
- sel = sel[0];
- data = data[0];
- } else {
- sel = null;
- data = null;
- }
- }
-
- await eventCallback(sel, data);
- }
- }
-
- async deselectAll(evt) {
- evt.preventDefault();
-
- // noinspection JSIgnoredPromiseFromCall
- this.notifySelection(this.props.onSelectionChangedAsync, new Map());
- }
-
- render() {
- const t = this.props.t;
- const props = this.props;
-
- let className = 'table table-striped table-bordered';
-
- if (this.props.selectMode !== TableSelectMode.NONE) {
- className += ' table-hover';
- }
-
- return (
-
-
{ this.domTable = domElem; }} className={className} cellSpacing="0" width="100%" />
-
- );
- }
-}
-
-export {
- Table,
- TableSelectMode
-}
\ No newline at end of file
diff --git a/client/src/lib/tree.js b/client/src/lib/tree.js
deleted file mode 100644
index 1cc8f1d6..00000000
--- a/client/src/lib/tree.js
+++ /dev/null
@@ -1,392 +0,0 @@
-'use strict';
-
-import React, {Component} from 'react';
-import ReactDOMServer from 'react-dom/server';
-import {withTranslation} from './i18n';
-import PropTypes from 'prop-types';
-
-import jQuery from 'jquery';
-import '../../static/jquery/jquery-ui-1.12.1.min.js';
-import '../../static/fancytree/jquery.fancytree-all.min.js';
-import '../../static/fancytree/skin-bootstrap/ui.fancytree.min.css';
-import './tree.scss';
-import axios from './axios';
-
-import {withPageHelpers} from './page'
-import {withAsyncErrorHandler, withErrorHandling} from './error-handling';
-import styles from "./styles.scss";
-import {getUrl} from "./urls";
-import {withComponentMixins} from "./decorator-helpers";
-
-const TreeSelectMode = {
- NONE: 0,
- SINGLE: 1,
- MULTI: 2
-};
-
-@withComponentMixins([
- withTranslation,
- withErrorHandling,
- withPageHelpers
-], ['refresh'])
-class TreeTable extends Component {
- constructor(props) {
- super(props);
-
- this.mounted = false;
-
- this.state = {
- treeData: null
- };
-
- if (props.data) {
- this.state.treeData = props.data;
- }
-
- // Select Mode simply cannot be changed later. This is just to make sure we avoid inconsistencies if someone changes it anyway.
- this.selectMode = this.props.selectMode;
- }
-
- static defaultProps = {
- selectMode: TreeSelectMode.NONE
- }
-
- refresh() {
- if (this.tree && !this.props.data && this.props.dataUrl) {
- // noinspection JSIgnoredPromiseFromCall
- this.loadData();
- }
- }
-
- @withAsyncErrorHandler
- async loadData() {
- const response = await axios.get(getUrl(this.props.dataUrl));
- const treeData = response.data;
-
- for (const root of treeData) {
- root.expanded = true;
- for (const child of root.children) {
- child.expanded = true;
- }
- }
-
- if (this.mounted) {
- this.setState({
- treeData
- });
- }
- }
-
- static propTypes = {
- dataUrl: PropTypes.string,
- data: PropTypes.array,
- selectMode: PropTypes.number,
- selection: PropTypes.oneOfType([PropTypes.array, PropTypes.string, PropTypes.number]),
- onSelectionChangedAsync: PropTypes.func,
- actions: PropTypes.func,
- withHeader: PropTypes.bool,
- withDescription: PropTypes.bool,
- noTable: PropTypes.bool,
- withIcons: PropTypes.bool,
- className: PropTypes.string
- }
-
- shouldComponentUpdate(nextProps, nextState) {
- return this.props.selection !== nextProps.selection || this.props.data !== nextProps.data || this.props.dataUrl !== nextProps.dataUrl ||
- this.state.treeData != nextState.treeData || this.props.className !== nextProps.className;
- }
-
- // XSS protection
- sanitizeTreeData(unsafeData) {
- const data = [];
- if (unsafeData) {
- for (const unsafeEntry of unsafeData) {
- const entry = Object.assign({}, unsafeEntry);
- entry.unsanitizedTitle = entry.title;
- entry.title = ReactDOMServer.renderToStaticMarkup({entry.title}
);
- entry.description = ReactDOMServer.renderToStaticMarkup({entry.description}
);
- if (entry.children) {
- entry.children = this.sanitizeTreeData(entry.children);
- }
- data.push(entry);
- }
- }
-
- return data;
- }
-
- componentDidMount() {
- this.mounted = true;
-
- if (!this.props.data && this.props.dataUrl) {
- // noinspection JSIgnoredPromiseFromCall
- this.loadData();
- }
-
- let createNodeFn;
- createNodeFn = (event, data) => {
- const node = data.node;
- const tdList = jQuery(node.tr).find(">td");
-
- let tdIdx = 1;
-
- if (this.props.withDescription) {
- const descHtml = node.data.description; // This was already sanitized in sanitizeTreeData when the data was loaded
- tdList.eq(tdIdx).html(descHtml);
- tdIdx += 1;
- }
-
- if (this.props.actions) {
- const linksContainer = jQuery(` `);
-
- const actions = this.props.actions(node);
-
- for (const action of actions) {
- if (action.action) {
- const html = ReactDOMServer.renderToStaticMarkup({action.label} );
- const elem = jQuery(html);
- elem.click((evt) => { evt.preventDefault(); action.action(this) });
- linksContainer.append(elem);
-
- } else if (action.link) {
- const html = ReactDOMServer.renderToStaticMarkup({action.label} );
- const elem = jQuery(html);
- elem.click((evt) => { evt.preventDefault(); this.navigateTo(action.link) });
- linksContainer.append(elem);
-
- } else if (action.href) {
- const html = ReactDOMServer.renderToStaticMarkup({action.label} );
- const elem = jQuery(html);
- linksContainer.append(elem);
-
- } else {
- const html = ReactDOMServer.renderToStaticMarkup({action.label} );
- const elem = jQuery(html);
- linksContainer.append(elem);
- }
- }
-
- tdList.eq(tdIdx).html(linksContainer);
- tdIdx += 1;
- }
- };
-
- const treeOpts = {
- extensions: ['glyph'],
- glyph: {
- map: {
- expanderClosed: 'fas fa-angle-right',
- expanderLazy: 'fas fa-angle-right', // glyphicon-plus-sign
- expanderOpen: 'fas fa-angle-down', // glyphicon-collapse-down
- checkbox: 'fas fa-square',
- checkboxSelected: 'fas fa-check-square',
-
- folder: 'fas fa-folder',
- folderOpen: 'fas fa-folder-open',
- doc: 'fas fa-file',
- docOpen: 'fas fa-file'
- }
- },
- selectMode: (this.selectMode === TreeSelectMode.MULTI ? 2 : 1),
- icon: !!this.props.withIcons,
- autoScroll: true,
- scrollParent: jQuery(this.domTableContainer),
- source: this.sanitizeTreeData(this.state.treeData),
- toggleEffect: false,
- createNode: createNodeFn,
- checkbox: this.selectMode === TreeSelectMode.MULTI,
- activate: (this.selectMode === TreeSelectMode.SINGLE ? ::this.onActivate : null),
- deactivate: (this.selectMode === TreeSelectMode.SINGLE ? ::this.onActivate : null),
- select: (this.selectMode === TreeSelectMode.MULTI ? ::this.onSelect : null),
- };
-
- if (!this.props.noTable) {
- treeOpts.extensions.push('table');
- treeOpts.table = {
- nodeColumnIdx: 0
- };
- }
-
- this.tree = jQuery(this.domTable).fancytree(treeOpts).fancytree("getTree");
-
- this.updateSelection();
- }
-
- componentDidUpdate(prevProps, prevState) {
- if (this.props.data) {
- this.setState({
- treeData: this.props.data
- });
- } else if (this.props.dataUrl && prevProps.dataUrl !== this.props.dataUrl) {
- // noinspection JSIgnoredPromiseFromCall
- this.loadData();
- }
-
- if (this.props.selection !== prevProps.selection || this.state.treeData != prevState.treeData) {
- if (this.state.treeData != prevState.treeData) {
- this.tree.reload(this.sanitizeTreeData(this.state.treeData));
- }
-
- this.updateSelection();
- }
- }
-
- componentWillUnmount() {
- this.mounted = false;
- }
-
- updateSelection() {
- const tree = this.tree;
- if (this.selectMode === TreeSelectMode.MULTI) {
- const selectSet = new Set(this.props.selection.map(key => this.stringifyKey(key)));
-
- tree.enableUpdate(false);
- tree.visit(node => node.setSelected(selectSet.has(node.key)));
- tree.enableUpdate(true);
-
- } else if (this.selectMode === TreeSelectMode.SINGLE) {
- let selection = this.stringifyKey(this.props.selection);
-
- if (this.state.treeData) {
- if (!tree.getNodeByKey(selection)) {
- selection = null;
- }
-
- if (selection === null && !this.tree.getActiveNode()) {
- // This covers the case when we mount the tree and selection is not present in the tree.
- // At this point, nothing is selected, so the onActive event won't trigger. So we have to
- // call it manually, so that the form can update and set null instead of the invalid selection.
- this.onActivate();
- } else {
- tree.activateKey(selection);
- }
- }
- }
- }
-
- @withAsyncErrorHandler
- async onSelectionChanged(sel) {
- if (this.props.onSelectionChangedAsync) {
- await this.props.onSelectionChangedAsync(sel);
- }
- }
-
- stringifyKey(key) {
- if (key !== null && key !== undefined) {
- return key.toString();
- } else {
- return key;
- }
- }
-
- destringifyKey(key) {
- if (/^(\-|\+)?([0-9]+|Infinity)$/.test(key)) {
- return Number(key);
- } else {
- return key;
- }
- }
-
- // Single-select
- onActivate(event, data) {
- const activeNode = this.tree.getActiveNode();
- const selection = activeNode ? this.destringifyKey(activeNode.key) : null;
-
- if (selection !== this.props.selection) {
- // noinspection JSIgnoredPromiseFromCall
- this.onSelectionChanged(selection);
- }
- }
-
- // Multi-select
- onSelect(event, data) {
- const newSel = this.tree.getSelectedNodes().map(node => this.destringifyKey(node.key)).sort();
- const oldSel = this.props.selection;
-
- let updated = false;
- const length = oldSel.length;
- if (length === newSel.length) {
- for (let i = 0; i < length; i++) {
- if (oldSel[i] !== newSel[i]) {
- updated = true;
- break;
- }
- }
- } else {
- updated = true;
- }
-
- if (updated) {
- // noinspection JSIgnoredPromiseFromCall
- this.onSelectionChanged(newSel);
- }
- }
-
- render() {
- const t = this.props.t;
- const props = this.props;
- const actions = props.actions;
- const withHeader = props.withHeader;
- const withDescription = props.withDescription;
-
- let containerClass = 'mt-treetable-container ' + (this.props.className || '');
- if (this.selectMode === TreeSelectMode.NONE) {
- containerClass += ' mt-treetable-inactivable';
- } else {
- if (!props.noTable) {
- containerClass += ' table-hover';
- }
- }
-
- if (!this.withHeader) {
- containerClass += ' mt-treetable-noheader';
- }
-
- // FIXME: style={{ height: '100px', overflow: 'auto'}}
-
- if (props.noTable) {
- return (
- { this.domTableContainer = domElem; }} >
-
{ this.domTable = domElem; }}>
-
-
- );
-
- } else {
- let tableClass = 'table table-striped table-condensed';
- if (this.selectMode !== TreeSelectMode.NONE) {
- tableClass += ' table-hover';
- }
-
- return (
- { this.domTableContainer = domElem; }} >
-
{ this.domTable = domElem; }} className={tableClass}>
- {props.withHeader &&
-
-
- {t('name')}
- {withDescription && {t('description')} }
- {actions && }
-
-
- }
-
-
-
- {withDescription && }
- {actions && }
-
-
-
-
- );
- }
-
- }
-}
-
-
-export {
- TreeTable,
- TreeSelectMode
-}
\ No newline at end of file
diff --git a/client/src/lib/tree.scss b/client/src/lib/tree.scss
deleted file mode 100644
index 06aa0198..00000000
--- a/client/src/lib/tree.scss
+++ /dev/null
@@ -1,92 +0,0 @@
-@import "../scss/variables.scss";
-
-:global {
-
-.mt-treetable-container .fancytree-container {
- border: none;
-}
-
-.mt-treetable-container span.fancytree-expander {
- color: #333333;
-}
-
-.mt-treetable-container.mt-treetable-inactivable>table.fancytree-ext-table.fancytree-container>tbody>tr.fancytree-active>td,
-.mt-treetable-container.mt-treetable-inactivable>table.fancytree-ext-table.fancytree-container>tbody>tr.fancytree-active:hover>td,
-.mt-treetable-container.mt-treetable-inactivable .fancytree-container span.fancytree-node.fancytree-active span.fancytree-title,
-.mt-treetable-container.mt-treetable-inactivable .fancytree-container span.fancytree-node.fancytree-active span.fancytree-title:hover,
-.mt-treetable-container.mt-treetable-inactivable .fancytree-container span.fancytree-node.fancytree-active:hover span.fancytree-title,
-.mt-treetable-container.mt-treetable-inactivable .fancytree-container span.fancytree-node span.fancytree-title:hover {
- background-color: transparent;
-}
-
-.mt-treetable-container.mt-treetable-inactivable .fancytree-container span.fancytree-node span.fancytree-title {
- cursor: default;
-}
-
-.mt-treetable-container.mt-treetable-inactivable .fancytree-container span.fancytree-node.fancytree-active span.fancytree-title,
-.mt-treetable-container.mt-treetable-inactivable .fancytree-container span.fancytree-node.fancytree-active span.fancytree-title:hover {
- border-color: transparent;
-}
-
-.mt-treetable-container.mt-treetable-inactivable>table.fancytree-ext-table.fancytree-container>tbody>tr.fancytree-active>td span.fancytree-title,
-.mt-treetable-container.mt-treetable-inactivable>table.fancytree-ext-table.fancytree-container>tbody>tr.fancytree-active>td span.fancytree-expander,
-.mt-treetable-container.mt-treetable-inactivable .fancytree-container span.fancytree-node.fancytree-active span.fancytree-title,
-.mt-treetable-container.mt-treetable-inactivable .fancytree-container>tbody>tr.fancytree-active>td {
- outline: 0px none;
- color: #333333;
-}
-
-.mt-treetable-container span.fancytree-node span.fancytree-expander:hover {
- color: inherit;
-}
-
-.mt-treetable-container {
- padding-top: 9px;
- padding-bottom: 9px;
-}
-
-.mt-treetable-container>table.fancytree-ext-table {
- margin-bottom: 0px;
-}
-
-.mt-treetable-container.mt-treetable-noheader>.table>tbody>tr>td {
- border-top: 0px none;
-}
-
-.mt-treetable-container .mt-treetable-title {
- min-width: 150px;
-}
-
-
-
-.form-group .mt-treetable-container {
- border: $input-border-width solid $input-border-color;
- border-radius: $input-border-radius;
- padding-top: $input-padding-y;
- padding-bottom: $input-padding-y;
-
- -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,0.075);
- box-shadow: inset 0 1px 1px rgba(0,0,0,0.075);
- -webkit-transition: border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s;
- -o-transition: border-color ease-in-out .15s,box-shadow ease-in-out .15s;
- transition: border-color ease-in-out .15s,box-shadow ease-in-out .15s;
-}
-
-.form-group .mt-treetable-container.is-valid {
- border-color: $form-feedback-valid-color;
- -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,0.075);
- box-shadow: inset 0 1px 1px rgba(0,0,0,0.075);
-}
-
-.form-group .mt-treetable-container.is-invalid {
- border-color: $form-feedback-invalid-color;
- -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,0.075);
- box-shadow: inset 0 1px 1px rgba(0,0,0,0.075);
-}
-
-.mt-treetable-container .table td {
- padding-top: 3px;
- padding-bottom: 3px;
-}
-
-}
diff --git a/client/src/lib/untrusted.js b/client/src/lib/untrusted.js
deleted file mode 100644
index 6948f9c7..00000000
--- a/client/src/lib/untrusted.js
+++ /dev/null
@@ -1,331 +0,0 @@
-'use strict';
-
-import React, {Component} from "react";
-import PropTypes from "prop-types";
-import {withTranslation} from './i18n';
-import {requiresAuthenticatedUser, withPageHelpers} from "./page";
-import {withAsyncErrorHandler, withErrorHandling} from "./error-handling";
-import axios from "./axios";
-import styles from "./styles.scss";
-import {getSandboxUrl, getUrl, setRestrictedAccessToken} from "./urls";
-import {withComponentMixins} from "./decorator-helpers";
-
-@withComponentMixins([
- withErrorHandling,
- withPageHelpers,
- requiresAuthenticatedUser
-], ['ask'])
-export class UntrustedContentHost extends Component {
- constructor(props) {
- super(props);
-
- this.refreshAccessTokenTimeout = null;
- this.accessToken = null;
- this.contentNodeIsLoaded = false;
-
- this.state = {
- hasAccessToken: false
- };
-
- this.receiveMessageHandler = ::this.receiveMessage;
- this.contentNodeRefHandler = node => this.contentNode = node;
-
- this.rpcCounter = 0;
- this.rpcResolves = new Map();
- }
-
- static propTypes = {
- contentSrc: PropTypes.string,
- contentProps: PropTypes.object,
- tokenMethod: PropTypes.string,
- tokenParams: PropTypes.object,
- className: PropTypes.string,
- singleToken: PropTypes.bool,
- onMethodAsync: PropTypes.func
- }
-
- isInitialized() {
- return !!this.accessToken && !!this.props.contentProps;
- }
-
- async receiveMessage(evt) {
- const msg = evt.data;
-
- if (msg.type === 'initNeeded') {
- // It seems that sometime the message that the content node does not arrive. However if the content root notifies us, we just proceed
- this.contentNodeIsLoaded = true;
-
- if (this.isInitialized()) {
- this.sendMessage('init', {
- accessToken: this.accessToken,
- contentProps: this.props.contentProps
- });
- }
- } else if (msg.type === 'rpcResponse') {
- const resolve = this.rpcResolves.get(msg.data.msgId);
- resolve(msg.data.ret);
- } else if (msg.type === 'rpcRequest') {
- const ret = await this.props.onMethodAsync(msg.data.method, msg.data.params);
- this.sendMessage('rpcResponse', {msgId: msg.data.msgId, ret});
- } else if (msg.type === 'clientHeight') {
- const newHeight = msg.data;
- this.contentNode.height = newHeight;
- }
- }
-
- sendMessage(type, data) {
- if (this.contentNodeIsLoaded && this.contentNode) { // This is to avoid errors: Failed to execute 'postMessage' on 'DOMWindow': The target origin provided ('http://localhost:8081') does not match the recipient window's origin ('http://localhost:3000')"
- // When the child window is closed during processing of the message, the this.contentNode becomes null and we can't deliver the response
- this.contentNode.contentWindow.postMessage({type, data}, getSandboxUrl());
- }
- }
-
- async ask(method, params) {
- if (this.contentNodeIsLoaded) {
- this.rpcCounter += 1;
- const msgId = this.rpcCounter;
-
- this.sendMessage('rpcRequest', {
- method,
- params,
- msgId
- });
-
- return await (new Promise((resolve, reject) => {
- this.rpcResolves.set(msgId, resolve);
- }));
- }
- }
-
- @withAsyncErrorHandler
- async refreshAccessToken() {
- 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;
-
- if (!this.state.hasAccessToken) {
- this.setState({
- hasAccessToken: true
- })
- }
-
- this.sendMessage('accessToken', this.accessToken);
- }
- }
-
- scheduleRefreshAccessToken() {
- this.refreshAccessTokenTimeout = setTimeout(() => {
- // noinspection JSIgnoredPromiseFromCall
- this.refreshAccessToken();
- this.scheduleRefreshAccessToken();
- }, 30 * 1000);
- }
-
- handleUpdate() {
- if (this.isInitialized()) {
- this.sendMessage('initAvailable');
- }
-
- if (!this.state.hasAccessToken) {
- // noinspection JSIgnoredPromiseFromCall
- this.refreshAccessToken();
- }
- }
-
- componentDidMount() {
- this.scheduleRefreshAccessToken();
- window.addEventListener('message', this.receiveMessageHandler, false);
-
- this.handleUpdate();
- }
-
- componentDidUpdate() {
- this.handleUpdate();
- }
-
- componentWillUnmount() {
- clearTimeout(this.refreshAccessTokenTimeout);
- window.removeEventListener('message', this.receiveMessageHandler, false);
- }
-
- contentNodeLoaded() {
- this.contentNodeIsLoaded = true;
- }
-
- render() {
- return (
- // The 40 px below corresponds to the height in .sandbox-loading-message
-
- );
- }
-}
-
-
-@withComponentMixins([
- withTranslation
-])
-export class UntrustedContentRoot extends Component {
- constructor(props) {
- super(props);
-
- this.state = {
- initialized: false,
- };
-
- this.receiveMessageHandler = ::this.receiveMessage;
-
- this.periodicTimeoutHandler = ::this.onPeriodicTimeout;
- this.periodicTimeoutId = 0;
-
- this.clientHeight = 0;
- }
-
- static propTypes = {
- render: PropTypes.func
- }
-
-
- onPeriodicTimeout() {
- const newHeight = document.body.clientHeight;
- if (this.clientHeight !== newHeight) {
- this.clientHeight = newHeight;
- this.sendMessage('clientHeight', newHeight);
- }
- this.periodicTimeoutId = setTimeout(this.periodicTimeoutHandler, 250);
- }
-
-
- async receiveMessage(evt) {
- const msg = evt.data;
-
- if (msg.type === 'initAvailable') {
- this.sendMessage('initNeeded');
-
- } else if (msg.type === 'init') {
- setRestrictedAccessToken(msg.data.accessToken);
- this.setState({
- initialized: true,
- contentProps: msg.data.contentProps
- });
-
- } else if (msg.type === 'accessToken') {
- setRestrictedAccessToken(msg.data);
- }
- }
-
- sendMessage(type, data) {
- window.parent.postMessage({type, data}, '*');
- }
-
- componentDidMount() {
- window.addEventListener('message', this.receiveMessageHandler, false);
- this.periodicTimeoutId = setTimeout(this.periodicTimeoutHandler, 0);
- this.sendMessage('initNeeded');
- }
-
- componentWillUnmount() {
- window.removeEventListener('message', this.receiveMessageHandler, false);
- clearTimeout(this.periodicTimeoutId);
- }
-
- render() {
- const t = this.props.t;
-
- if (this.state.initialized) {
- return this.props.render(this.state.contentProps);
- } else {
- return (
-
- {t('loading')}
-
- );
- }
- }
-}
-
-class ParentRPC {
- constructor(props) {
- this.receiveMessageHandler = ::this.receiveMessage;
-
- this.rpcCounter = 0;
- this.rpcResolves = new Map();
- this.methodHandlers = new Map();
-
- this.initialized = false;
- }
-
- init() {
- window.addEventListener('message', this.receiveMessageHandler, false);
- this.initialized = true;
- }
-
- setMethodHandler(method, handler) {
- this.enforceInitialized();
- this.methodHandlers.set(method, handler);
- }
-
- clearMethodHandler(method) {
- this.enforceInitialized();
- this.methodHandlers.delete(method);
- }
-
- async ask(method, params) {
- this.enforceInitialized();
- this.rpcCounter += 1;
- const msgId = this.rpcCounter;
-
- this.sendMessage('rpcRequest', {
- method,
- params,
- msgId
- });
-
- return await (new Promise((resolve, reject) => {
- this.rpcResolves.set(msgId, resolve);
- }));
- }
-
-
- // ---------------------------------------------------------------------------
- // Private methods
-
- enforceInitialized() {
- if (!this.initialized) {
- throw new Error('ParentRPC not initialized');
- }
- }
-
- async receiveMessage(evt) {
- const msg = evt.data;
-
- if (msg.type === 'rpcResponse') {
- const resolve = this.rpcResolves.get(msg.data.msgId);
- resolve(msg.data.ret);
-
- } else if (msg.type === 'rpcRequest') {
- let ret;
-
- const method = msg.data.method;
- if (this.methodHandlers.has(method)) {
- const handler = this.methodHandlers.get(method);
- ret = await handler(method, msg.data.params);
- }
-
- this.sendMessage('rpcResponse', {msgId: msg.data.msgId, ret});
- }
- }
-
- sendMessage(type, data) {
- window.parent.postMessage({type, data}, '*');
- }
-}
-
-export const parentRPC = new ParentRPC();
\ No newline at end of file
diff --git a/client/src/lib/urls.js b/client/src/lib/urls.js
deleted file mode 100644
index 242a2eaa..00000000
--- a/client/src/lib/urls.js
+++ /dev/null
@@ -1,60 +0,0 @@
-'use strict';
-
-import {anonymousRestrictedAccessToken} from '../../../shared/urls';
-import {AppType} from '../../../shared/app';
-import mailtrainConfig from "mailtrainConfig";
-import i18n from './i18n';
-
-let restrictedAccessToken = anonymousRestrictedAccessToken;
-
-function setRestrictedAccessToken(token) {
- restrictedAccessToken = token;
-}
-
-function getTrustedUrl(path) {
- return mailtrainConfig.trustedUrlBase + (path || '');
-}
-
-function getSandboxUrl(path, customRestrictedAccessToken) {
- const localRestrictedAccessToken = customRestrictedAccessToken || restrictedAccessToken;
- return mailtrainConfig.sandboxUrlBase + localRestrictedAccessToken + '/' + (path || '');
-}
-
-function getPublicUrl(path, opts) {
- const url = new URL(path || '', mailtrainConfig.publicUrlBase);
-
- if (opts && opts.withLocale) {
- url.searchParams.append('locale', i18n.language);
- }
-
- return url.toString();
-}
-
-function getUrl(path) {
- if (mailtrainConfig.appType === AppType.TRUSTED) {
- return getTrustedUrl(path);
- } else if (mailtrainConfig.appType === AppType.SANDBOXED) {
- return getSandboxUrl(path);
- } else if (mailtrainConfig.appType === AppType.PUBLIC) {
- return getPublicUrl(path);
- }
-}
-
-function getBaseDir() {
- if (mailtrainConfig.appType === AppType.TRUSTED) {
- return mailtrainConfig.trustedUrlBaseDir;
- } else if (mailtrainConfig.appType === AppType.SANDBOXED) {
- return mailtrainConfig.sandboxUrlBaseDir + restrictedAccessToken;
- } else if (mailtrainConfig.appType === AppType.PUBLIC) {
- return mailtrainConfig.publicUrlBaseDir;
- }
-}
-
-export {
- getTrustedUrl,
- getSandboxUrl,
- getPublicUrl,
- getUrl,
- getBaseDir,
- setRestrictedAccessToken
-}
\ No newline at end of file
diff --git a/client/src/lists/CUD.js b/client/src/lists/CUD.js
deleted file mode 100644
index 588282fa..00000000
--- a/client/src/lists/CUD.js
+++ /dev/null
@@ -1,296 +0,0 @@
-'use strict';
-
-import React, {Component} from 'react';
-import PropTypes from 'prop-types';
-import {Trans} from 'react-i18next';
-import {withTranslation} from '../lib/i18n';
-import {LinkButton, requiresAuthenticatedUser, Title, withPageHelpers} from '../lib/page';
-import {
- Button,
- ButtonRow,
- CheckBox,
- Dropdown,
- filterData,
- Form,
- FormSendMethod,
- InputField,
- StaticField,
- TableSelect,
- TextArea,
- withForm,
- withFormErrorHandlers
-} from '../lib/form';
-import {withErrorHandling} from '../lib/error-handling';
-import {DeleteModalDialog} from '../lib/modals';
-import {getDefaultNamespace, NamespaceSelect, validateNamespace} from '../lib/namespace';
-import {FieldWizard, UnsubscriptionMode} from '../../../shared/lists';
-import styles from "../lib/styles.scss";
-import {getMailerTypes} from "../send-configurations/helpers";
-import {withComponentMixins} from "../lib/decorator-helpers";
-
-@withComponentMixins([
- withTranslation,
- withForm,
- withErrorHandling,
- withPageHelpers,
- requiresAuthenticatedUser
-])
-export default class CUD extends Component {
- constructor(props) {
- super(props);
-
- this.state = {};
-
- this.initForm();
-
- this.mailerTypes = getMailerTypes(props.t);
- }
-
- static propTypes = {
- action: PropTypes.string.isRequired,
- entity: PropTypes.object,
- permissions: PropTypes.object
- }
-
- getFormValuesMutator(data) {
- data.form = data.default_form ? 'custom' : 'default';
- data.listunsubscribe_disabled = !!data.listunsubscribe_disabled;
- }
-
- submitFormValuesMutator(data) {
- if (data.form === 'default') {
- data.default_form = null;
- }
-
- if (data.fieldWizard === FieldWizard.FIRST_LAST_NAME || data.fieldWizard === FieldWizard.NAME) {
- data.to_name = null;
- }
-
- return filterData(data, ['name', 'description', 'default_form', 'public_subscribe', 'unsubscription_mode',
- 'contact_email', 'homepage', 'namespace', 'to_name', 'listunsubscribe_disabled', 'send_configuration',
- 'fieldWizard'
- ]);
- }
-
- componentDidMount() {
- if (this.props.entity) {
- this.getFormValuesFromEntity(this.props.entity);
-
- } else {
- this.populateFormValues({
- name: '',
- description: '',
- form: 'default',
- default_form: 'default',
- public_subscribe: true,
- contact_email: '',
- homepage: '',
- unsubscription_mode: UnsubscriptionMode.ONE_STEP,
- namespace: getDefaultNamespace(this.props.permissions),
- to_name: '',
- fieldWizard: FieldWizard.FIRST_LAST_NAME,
- send_configuration: null,
- listunsubscribe_disabled: false
- });
- }
- }
-
- localValidateFormValues(state) {
- const t = this.props.t;
-
- if (!state.getIn(['name', 'value'])) {
- state.setIn(['name', 'error'], t('nameMustNotBeEmpty'));
- } else {
- state.setIn(['name', 'error'], null);
- }
-
- if (!state.getIn(['send_configuration', 'value'])) {
- state.setIn(['send_configuration', 'error'], t('sendConfigurationMustBeSelected'));
- } else {
- state.setIn(['send_configuration', 'error'], null);
- }
-
- if (state.getIn(['form', 'value']) === 'custom' && !state.getIn(['default_form', 'value'])) {
- state.setIn(['default_form', 'error'], t('customFormMustBeSelected'));
- } else {
- state.setIn(['default_form', 'error'], null);
- }
-
- validateNamespace(t, state);
- }
-
- @withFormErrorHandlers
- async submitHandler(submitAndLeave) {
- const t = this.props.t;
-
- let sendMethod, url;
- if (this.props.entity) {
- sendMethod = FormSendMethod.PUT;
- url = `rest/lists/${this.props.entity.id}`
- } else {
- sendMethod = FormSendMethod.POST;
- url = 'rest/lists'
- }
-
- this.disableForm();
- this.setFormStatusMessage('info', t('saving'));
-
- const submitResult = await this.validateAndSendFormValuesToURL(sendMethod, url);
-
- if (submitResult) {
- if (this.props.entity) {
- if (submitAndLeave) {
- this.navigateToWithFlashMessage('/lists', 'success', t('listUpdated'));
- } else {
- await this.getFormValuesFromURL(`rest/lists/${this.props.entity.id}`);
- this.enableForm();
- this.setFormStatusMessage('success', t('listUpdated'));
- }
- } else {
- if (submitAndLeave) {
- this.navigateToWithFlashMessage('/lists', 'success', t('listCreated'));
- } else {
- this.navigateToWithFlashMessage(`/lists/${submitResult}/edit`, 'success', t('listCreated'));
- }
- }
- } else {
- this.enableForm();
- this.setFormStatusMessage('warning', t('thereAreErrorsInTheFormPleaseFixThemAnd'));
- }
- }
-
- render() {
- const t = this.props.t;
- const isEdit = !!this.props.entity;
- const canDelete = isEdit && this.props.entity.permissions.includes('delete');
-
- const unsubcriptionModeOptions = [
- {
- key: UnsubscriptionMode.ONE_STEP,
- label: t('onestepIeNoEmailWithConfirmationLink')
- },
- {
- key: UnsubscriptionMode.ONE_STEP_WITH_FORM,
- label: t('onestepWithUnsubscriptionFormIeNoEmail')
- },
- {
- key: UnsubscriptionMode.TWO_STEP,
- label: t('twostepIeAnEmailWithConfirmationLinkWill')
- },
- {
- key: UnsubscriptionMode.TWO_STEP_WITH_FORM,
- label: t('twostepWithUnsubscriptionFormIeAnEmail')
- },
- {
- key: UnsubscriptionMode.MANUAL,
- label: t('manualIeUnsubscriptionHasToBePerformedBy')
- }
- ];
-
- const formsOptions = [
- {
- key: 'default',
- label: t('defaultMailtrainForms')
- },
- {
- key: 'custom',
- label: t('customFormsSelectFormBelow')
- }
- ];
-
- const customFormsColumns = [
- {data: 0, title: "#"},
- {data: 1, title: t('name')},
- {data: 2, title: t('description')},
- {data: 3, title: t('namespace')}
- ];
-
- const sendConfigurationsColumns = [
- { data: 1, title: t('name') },
- { data: 2, title: t('id'), render: data => {data}
},
- { data: 3, title: t('description') },
- { data: 4, title: t('type'), render: data => this.mailerTypes[data].typeName },
- { data: 6, title: t('namespace') }
- ];
-
- let toNameFields;
- if (isEdit) {
- toNameFields = ;
- } else {
- const fieldWizardOptions = [
- {key: FieldWizard.NONE, label: t('emptyCustomNoFields')},
- {key: FieldWizard.NAME, label: t('nameOneField')},
- {key: FieldWizard.FIRST_LAST_NAME, label: t('firstNameAndLastNameTwoFields')},
- ];
-
- const fieldWizardValue = this.getFormValue('fieldWizard');
-
- const fieldWizardSelector =
-
- if (fieldWizardValue === FieldWizard.NONE) {
- toNameFields = (
- <>
- {fieldWizardSelector}
-
- >
- );
- } else {
- toNameFields = fieldWizardSelector;
- }
- }
-
- return (
-
- {canDelete &&
-
- }
-
- {isEdit ? t('editList') : t('createList')}
-
-
-
- );
- }
-}
diff --git a/client/src/lists/List.js b/client/src/lists/List.js
deleted file mode 100644
index f7751ebd..00000000
--- a/client/src/lists/List.js
+++ /dev/null
@@ -1,137 +0,0 @@
-'use strict';
-
-import React, {Component} from 'react';
-import {withTranslation} from '../lib/i18n';
-import {LinkButton, requiresAuthenticatedUser, Title, Toolbar, withPageHelpers} from '../lib/page';
-import {withErrorHandling} from '../lib/error-handling';
-import {Table} from '../lib/table';
-import {Icon} from "../lib/bootstrap-components";
-import {tableAddDeleteButton, tableRestActionDialogInit, tableRestActionDialogRender} from "../lib/modals";
-import {withComponentMixins} from "../lib/decorator-helpers";
-import {withForm} from "../lib/form";
-import PropTypes from 'prop-types';
-
-@withComponentMixins([
- withTranslation,
- withForm,
- withErrorHandling,
- withPageHelpers,
- requiresAuthenticatedUser
-])
-export default class List extends Component {
- constructor(props) {
- super(props);
-
- this.state = {};
- tableRestActionDialogInit(this);
- }
-
- static propTypes = {
- permissions: PropTypes.object
- }
-
- render() {
- const t = this.props.t;
-
- const permissions = this.props.permissions;
- const createPermitted = permissions.createList;
- const customFormsPermitted = permissions.createCustomForm || permissions.viewCustomForm;
-
- const columns = [
- {
- data: 1,
- title: t('name'),
- actions: data => {
- const perms = data[7];
- if (perms.includes('viewSubscriptions')) {
- return [{label: data[1], link: `/lists/${data[0]}/subscriptions`}];
- } else {
- return [{label: data[1]}];
- }
- }
- },
- { data: 2, title: t('id'), render: data => {data}
},
- { data: 3, title: t('subscribers') },
- { data: 4, title: t('description') },
- { data: 5, title: t('namespace') },
- {
- actions: data => {
- const actions = [];
- const triggersCount = data[6];
- const perms = data[7];
-
- if (perms.includes('viewSubscriptions')) {
- actions.push({
- label: ,
- link: `/lists/${data[0]}/subscriptions`
- });
- }
-
- if (perms.includes('edit')) {
- actions.push({
- label: ,
- link: `/lists/${data[0]}/edit`
- });
- }
-
- if (perms.includes('viewFields')) {
- actions.push({
- label: ,
- link: `/lists/${data[0]}/fields`
- });
- }
-
- if (perms.includes('viewSegments')) {
- actions.push({
- label: ,
- link: `/lists/${data[0]}/segments`
- });
- }
-
- if (perms.includes('viewImports')) {
- actions.push({
- label: ,
- link: `/lists/${data[0]}/imports`
- });
- }
-
- if (triggersCount > 0) {
- actions.push({
- label: ,
- link: `/lists/${data[0]}/triggers`
- });
- }
-
- if (perms.includes('share')) {
- actions.push({
- label: ,
- link: `/lists/${data[0]}/share`
- });
- }
-
- tableAddDeleteButton(actions, this, perms, `rest/lists/${data[0]}`, data[1], t('deletingList'), t('listDeleted'));
-
- return actions;
- }
- }
- ];
-
- return (
-
- {tableRestActionDialogRender(this)}
-
- { createPermitted &&
-
- }
- { customFormsPermitted &&
-
- }
-
-
-
{t('lists')}
-
-
this.table = node} withHeader dataUrl="rest/lists-table" columns={columns} />
-
- );
- }
-}
\ No newline at end of file
diff --git a/client/src/lists/TriggersList.js b/client/src/lists/TriggersList.js
deleted file mode 100644
index 075ba940..00000000
--- a/client/src/lists/TriggersList.js
+++ /dev/null
@@ -1,82 +0,0 @@
-'use strict';
-
-import React, {Component} from 'react';
-import PropTypes from 'prop-types';
-import {withTranslation} from '../lib/i18n';
-import {requiresAuthenticatedUser, Title, withPageHelpers} from '../lib/page';
-import {withErrorHandling} from '../lib/error-handling';
-import {Table} from '../lib/table';
-import {getTriggerTypes} from '../campaigns/triggers/helpers';
-import {Icon} from "../lib/bootstrap-components";
-import mailtrainConfig from 'mailtrainConfig';
-import {tableAddDeleteButton, tableRestActionDialogInit, tableRestActionDialogRender} from "../lib/modals";
-import {withComponentMixins} from "../lib/decorator-helpers";
-
-@withComponentMixins([
- withTranslation,
- withErrorHandling,
- withPageHelpers,
- requiresAuthenticatedUser
-])
-export default class List extends Component {
- constructor(props) {
- super(props);
-
- const {entityLabels, eventLabels} = getTriggerTypes(props.t);
- this.entityLabels = entityLabels;
- this.eventLabels = eventLabels;
-
- this.state = {};
- tableRestActionDialogInit(this);
- }
-
- static propTypes = {
- list: PropTypes.object
- }
-
- componentDidMount() {
- }
-
- render() {
- const t = this.props.t;
-
- const columns = [
- { data: 1, title: t('name') },
- { data: 2, title: t('description') },
- { data: 3, title: t('campaign') },
- { data: 4, title: t('entity'), render: data => this.entityLabels[data], searchable: false },
- { data: 5, title: t('event'), render: (data, cmd, rowData) => this.eventLabels[rowData[4]][data], searchable: false },
- { data: 6, title: t('daysAfter'), render: data => Math.round(data / (3600 * 24)) },
- { data: 7, title: t('enabled'), render: data => data ? t('yes') : t('no'), searchable: false},
- {
- actions: data => {
- const actions = [];
- const perms = data[9];
- const campaignId = data[8];
-
- if (mailtrainConfig.globalPermissions.setupAutomation && perms.includes('manageTriggers')) {
- actions.push({
- label: ,
- link: `/campaigns/${campaignId}/triggers/${data[0]}/edit`
- });
- }
-
- if (perms.includes('manageTriggers')) {
- tableAddDeleteButton(actions, this, null, `rest/triggers/${campaignId}/${data[0]}`, data[1], t('deletingTrigger'), t('triggerDeleted'));
- }
-
- return actions;
- }
- }
- ];
-
- return (
-
- {tableRestActionDialogRender(this)}
-
{t('triggers')}
-
-
this.table = node} withHeader dataUrl={`rest/triggers-by-list-table/${this.props.list.id}`} columns={columns} />
-
- );
- }
-}
\ No newline at end of file
diff --git a/client/src/lists/fields/CUD.js b/client/src/lists/fields/CUD.js
deleted file mode 100644
index 84b40bd5..00000000
--- a/client/src/lists/fields/CUD.js
+++ /dev/null
@@ -1,535 +0,0 @@
-'use strict';
-
-import React, {Component} from 'react';
-import PropTypes from 'prop-types';
-import {Trans} from 'react-i18next';
-import {withTranslation} from '../../lib/i18n';
-import {LinkButton, requiresAuthenticatedUser, Title, withPageHelpers} from '../../lib/page';
-import {
- ACEEditor,
- Button,
- ButtonRow,
- CheckBox,
- Dropdown,
- Fieldset,
- filterData,
- Form,
- FormSendMethod,
- InputField,
- StaticField,
- TableSelect,
- TextArea,
- withForm,
- withFormErrorHandlers
-} from '../../lib/form';
-import {withErrorHandling} from '../../lib/error-handling';
-import {DeleteModalDialog} from "../../lib/modals";
-import {getFieldTypes} from './helpers';
-import validators from '../../../../shared/validators';
-import slugify from 'slugify';
-import {DateFormat, parseBirthday, parseDate} from '../../../../shared/date';
-import styles from "../../lib/styles.scss";
-import 'brace/mode/json';
-import 'brace/mode/handlebars';
-import {withComponentMixins} from "../../lib/decorator-helpers";
-
-@withComponentMixins([
- withTranslation,
- withForm,
- withErrorHandling,
- withPageHelpers,
- requiresAuthenticatedUser
-])
-export default class CUD extends Component {
- constructor(props) {
- super(props);
-
- this.state = {};
-
- this.fieldTypes = getFieldTypes(props.t);
-
- this.initForm({
- serverValidation: {
- url: `rest/fields-validate/${this.props.list.id}`,
- changed: ['key'],
- extra: ['id']
- },
- onChangeBeforeValidation: {
- name: ::this.onChangeName
- }
- });
- }
-
- static propTypes = {
- action: PropTypes.string.isRequired,
- list: PropTypes.object,
- fields: PropTypes.array,
- entity: PropTypes.object
- }
-
- onChangeName(mutStateData, attr, oldValue, newValue) {
- const oldComputedKey = ('MERGE_' + slugify(oldValue, '_')).toUpperCase().replace(/[^A-Z0-9_]/g, '');
- const oldKey = mutStateData.getIn(['key', 'value']);
-
- if (oldKey === '' || oldKey === oldComputedKey) {
- const newKey = ('MERGE_' + slugify(newValue, '_')).toUpperCase().replace(/[^A-Z0-9_]/g, '');
- mutStateData.setIn(['key', 'value'], newKey);
- }
- }
-
- getFormValuesMutator(data) {
- data.settings = data.settings || {};
-
- if (data.default_value === null) {
- data.default_value = '';
- }
-
- if (data.help === null) {
- data.help = '';
- }
-
- data.isInGroup = data.group !== null;
-
- data.enumOptions = '';
- data.dateFormat = DateFormat.EUR;
- data.renderTemplate = '';
-
- switch (data.type) {
- case 'checkbox-grouped':
- case 'radio-grouped':
- case 'dropdown-grouped':
- case 'json':
- data.renderTemplate = data.settings.renderTemplate;
- break;
-
- case 'radio-enum':
- case 'dropdown-enum':
- data.enumOptions = this.renderEnumOptions(data.settings.options);
- data.renderTemplate = data.settings.renderTemplate;
- break;
-
- case 'date':
- case 'birthday':
- data.dateFormat = data.settings.dateFormat;
- break;
-
- case 'option':
- data.checkedLabel = data.isInGroup ? '' : data.settings.checkedLabel;
- data.uncheckedLabel = data.isInGroup ? '' : data.settings.uncheckedLabel;
- break;
- }
-
- data.orderListBefore = data.orderListBefore.toString();
- data.orderSubscribeBefore = data.orderSubscribeBefore.toString();
- data.orderManageBefore = data.orderManageBefore.toString();
- }
-
- submitFormValuesMutator(data) {
- if (data.default_value.trim() === '') {
- data.default_value = null;
- }
-
- if (data.help.trim() === '') {
- data.help = null;
- }
-
- if (!data.isInGroup) {
- data.group = null;
- }
-
- data.settings = {};
- switch (data.type) {
- case 'checkbox-grouped':
- case 'radio-grouped':
- case 'dropdown-grouped':
- case 'json':
- data.settings.renderTemplate = data.renderTemplate;
- break;
-
- case 'radio-enum':
- case 'dropdown-enum':
- data.settings.options = this.parseEnumOptions(data.enumOptions).options;
- data.settings.renderTemplate = data.renderTemplate;
- break;
-
- case 'date':
- case 'birthday':
- data.settings.dateFormat = data.dateFormat;
- break;
-
- case 'option':
- if (!data.isInGroup) {
- data.settings.checkedLabel = data.checkedLabel;
- data.settings.uncheckedLabel = data.uncheckedLabel;
- }
- break;
- }
-
- if (data.group !== null) {
- data.orderListBefore = data.orderSubscribeBefore = data.orderManageBefore = 'none';
- } else {
- data.orderListBefore = Number.parseInt(data.orderListBefore) || data.orderListBefore;
- data.orderSubscribeBefore = Number.parseInt(data.orderSubscribeBefore) || data.orderSubscribeBefore;
- data.orderManageBefore = Number.parseInt(data.orderManageBefore) || data.orderManageBefore;
- }
-
- return filterData(data, ['name', 'help', 'key', 'default_value', 'type', 'group', 'settings',
- 'orderListBefore', 'orderSubscribeBefore', 'orderManageBefore']);
- }
-
- componentDidMount() {
- if (this.props.entity) {
- this.getFormValuesFromEntity(this.props.entity);
-
- } else {
- this.populateFormValues({
- name: '',
- type: 'text',
- key: '',
- default_value: '',
- help: '',
- group: null,
- isInGroup: false,
- renderTemplate: '',
- enumOptions: '',
- dateFormat: 'eur',
- checkedLabel: '',
- uncheckedLabel: '',
- orderListBefore: 'end', // possible values are / 'end' / 'none'
- orderSubscribeBefore: 'end',
- orderManageBefore: 'end'
- });
- }
- }
-
- localValidateFormValues(state) {
- const t = this.props.t;
-
- if (!state.getIn(['name', 'value'])) {
- state.setIn(['name', 'error'], t('nameMustNotBeEmpty'));
- } else {
- state.setIn(['name', 'error'], null);
- }
-
- const keyServerValidation = state.getIn(['key', 'serverValidation']);
- if (!validators.mergeTagValid(state.getIn(['key', 'value']))) {
- state.setIn(['key', 'error'], t('mergeTagIsInvalidMayMustBeUppercaseAnd'));
- } else if (!keyServerValidation) {
- state.setIn(['key', 'error'], t('validationIsInProgress'));
- } else if (keyServerValidation.exists) {
- state.setIn(['key', 'error'], t('anotherFieldWithTheSameMergeTagExists'));
- } else {
- state.setIn(['key', 'error'], null);
- }
-
- const type = state.getIn(['type', 'value']);
-
- const group = state.getIn(['group', 'value']);
- const isInGroup = state.getIn(['isInGroup', 'value']);
- if (isInGroup && !group) {
- state.setIn(['group', 'error'], t('groupHasToBeSelected'));
- } else {
- state.setIn(['group', 'error'], null);
- }
-
- const defaultValue = state.getIn(['default_value', 'value']);
- if (defaultValue === '') {
- state.setIn(['default_value', 'error'], null);
- } else if (type === 'number' && !/^[0-9]*$/.test(defaultValue.trim())) {
- state.setIn(['default_value', 'error'], t('defaultValueIsNotIntegerNumber'));
- } else if (type === 'date' && !parseDate(state.getIn(['dateFormat', 'value']), defaultValue)) {
- state.setIn(['default_value', 'error'], t('defaultValueIsNotAProperlyFormattedDate'));
- } else if (type === 'birthday' && !parseBirthday(state.getIn(['dateFormat', 'value']), defaultValue)) {
- state.setIn(['default_value', 'error'], t('defaultValueIsNotAProperlyFormatted'));
- } else {
- state.setIn(['default_value', 'error'], null);
- }
-
- if (type === 'radio-enum' || type === 'dropdown-enum') {
- const enumOptions = this.parseEnumOptions(state.getIn(['enumOptions', 'value']));
- if (enumOptions.errors) {
- state.setIn(['enumOptions', 'error'], {enumOptions.errors.map((err, idx) =>
{err}
)}
);
- } else {
- state.setIn(['enumOptions', 'error'], null);
-
- if (defaultValue !== '' && !(enumOptions.options.find(x => x.key === defaultValue))) {
- state.setIn(['default_value', 'error'], t('defaultValueIsNotOneOfTheAllowedOptions'));
- }
- }
- } else {
- state.setIn(['enumOptions', 'error'], null);
- }
- }
-
- parseEnumOptions(text) {
- const t = this.props.t;
- const errors = [];
- const options = [];
-
- const lines = text.split('\n');
- for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
- const line = lines[lineIdx].trim();
-
- if (line != '') {
- const matches = line.match(/^([^|]*)[|](.*)$/);
- if (matches) {
- const key = matches[1].trim();
- const label = matches[2].trim();
- options.push({ key, label });
- } else {
- errors.push(t('errrorOnLineLine', { line: lineIdx + 1}));
- }
- }
- }
-
- if (errors.length) {
- return {
- errors
- };
- } else {
- return {
- options
- };
- }
- }
-
- renderEnumOptions(options) {
- return options.map(opt => `${opt.key}|${opt.label}`).join('\n');
- }
-
-
- @withFormErrorHandlers
- async submitHandler(submitAndLeave) {
- const t = this.props.t;
-
- let sendMethod, url;
- if (this.props.entity) {
- sendMethod = FormSendMethod.PUT;
- url = `rest/fields/${this.props.list.id}/${this.props.entity.id}`
- } else {
- sendMethod = FormSendMethod.POST;
- url = `rest/fields/${this.props.list.id}`
- }
-
- try {
- this.disableForm();
- this.setFormStatusMessage('info', t('saving'));
-
- const submitResult = await this.validateAndSendFormValuesToURL(sendMethod, url);
-
- if (submitResult) {
- if (this.props.entity) {
- if (submitAndLeave) {
- this.navigateToWithFlashMessage(`/lists/${this.props.list.id}/fields`, 'success', t('fieldUpdated'));
- } else {
- await this.getFormValuesFromURL(`rest/fields/${this.props.list.id}/${this.props.entity.id}`);
- this.enableForm();
- this.setFormStatusMessage('success', t('fieldUpdated'));
- }
- } else {
- if (submitAndLeave) {
- this.navigateToWithFlashMessage(`/lists/${this.props.list.id}/fields`, 'success', t('fieldCreated'));
- } else {
- this.navigateToWithFlashMessage(`/lists/${this.props.list.id}/fields/${submitResult}/edit`, 'success', t('fieldCreated'));
- }
- }
- } else {
- this.enableForm();
- this.setFormStatusMessage('warning', t('thereAreErrorsInTheFormPleaseFixThemAnd'));
- }
- } catch (error) {
- throw error;
- }
- }
-
- render() {
- const t = this.props.t;
- const isEdit = !!this.props.entity;
-
-
- const getOrderOptions = fld => {
- return [
- {key: 'none', label: t('notVisible')},
- ...this.props.fields.filter(x => (!this.props.entity || x.id !== this.props.entity.id) && x[fld] !== null && x.group === null).sort((x, y) => x[fld] - y[fld]).map(x => ({ key: x.id.toString(), label: `${x.name} (${this.fieldTypes[x.type].label})`})),
- {key: 'end', label: t('endOfList')}
- ];
- };
-
-
- const typeOptions = Object.keys(this.fieldTypes).map(key => ({key, label: this.fieldTypes[key].label}));
-
- const type = this.getFormValue('type');
- const isInGroup = this.getFormValue('isInGroup');
-
- let fieldSettings = null;
- switch (type) {
- case 'text':
- case 'website':
- case 'longtext':
- case 'gpg':
- case 'number':
- fieldSettings =
-
-
- ;
- break;
-
- case 'checkbox-grouped':
- case 'radio-grouped':
- case 'dropdown-grouped':
- fieldSettings =
-
- You can control the appearance of the merge tag with this template. The template
- uses handlebars syntax and you can find all values from {'{{values}}'}
array, for
- example {'{{#each values}} {{this}} {{/each}}'}
. If template is not defined then
- multiple values are joined with commas.}
- />
- ;
- break;
-
- case 'radio-enum':
- case 'dropdown-enum':
- fieldSettings =
-
- Specify the options to select from in the following format:key|label
. For example:
- au|Australia
at|Austria
}
- />
- Default key (e.g. au
used when the field is empty.')}/>
- You can control the appearance of the merge tag with this template. The template
- uses handlebars syntax and you can find all values from {'{{values}}'}
array.
- Each entry in the array is an object with attributes key
and label
.
- For example {'{{#each values}} {{this.value}} {{/each}}'}
. If template is not defined then
- multiple values are joined with commas.}
- />
- ;
- break;
-
- case 'date':
- fieldSettings =
-
-
- Default value used when the field is empty.}/>
- ;
- break;
-
- case 'birthday':
- fieldSettings =
-
-
- Default value used when the field is empty.}/>
- ;
- break;
-
- case 'json':
- fieldSettings =
- Default key (e.g. au
used when the field is empty.')}/>
- You can use this template to render JSON values (if the JSON is an array then the array is
- exposed as values
, otherwise you can access the JSON keys directly).}
- />
- ;
- break;
-
- case 'option':
- const fieldsGroupedColumns = [
- { data: 4, title: "#" },
- { data: 1, title: t('name') },
- { data: 2, title: t('type'), render: data => this.fieldTypes[data].label, sortable: false, searchable: false },
- { data: 3, title: t('mergeTag') }
- ];
-
- fieldSettings =
-
-
- {isInGroup &&
-
- }
- {!isInGroup &&
- <>
-
-
- >
- }
-
- ;
- break;
- }
-
-
- return (
-
- {isEdit &&
-
- }
-
- {isEdit ? t('editField') : t('createField')}
-
-
-
-
- {isEdit ?
- {(this.fieldTypes[this.getFormValue('type')] || {}).label}
- :
-
- }
-
-
-
-
-
- {fieldSettings}
-
- {type !== 'option' &&
-
-
-
-
-
- }
-
-
-
- await this.submitHandler(true)}/>
- {isEdit && }
-
-
-
- );
- }
-}
\ No newline at end of file
diff --git a/client/src/lists/fields/List.js b/client/src/lists/fields/List.js
deleted file mode 100644
index 33461a41..00000000
--- a/client/src/lists/fields/List.js
+++ /dev/null
@@ -1,80 +0,0 @@
-'use strict';
-
-import React, {Component} from 'react';
-import PropTypes from 'prop-types';
-import {withTranslation} from '../../lib/i18n';
-import {LinkButton, requiresAuthenticatedUser, Title, Toolbar, withPageHelpers} from '../../lib/page';
-import {withErrorHandling} from '../../lib/error-handling';
-import {Table} from '../../lib/table';
-import {getFieldTypes} from './helpers';
-import {Icon} from "../../lib/bootstrap-components";
-import {tableAddDeleteButton, tableRestActionDialogInit, tableRestActionDialogRender} from "../../lib/modals";
-import {withComponentMixins} from "../../lib/decorator-helpers";
-
-@withComponentMixins([
- withTranslation,
- withErrorHandling,
- withPageHelpers,
- requiresAuthenticatedUser
-])
-export default class List extends Component {
- constructor(props) {
- super(props);
-
- this.state = {};
- tableRestActionDialogInit(this);
-
- this.fieldTypes = getFieldTypes(props.t);
- }
-
- static propTypes = {
- list: PropTypes.object
- }
-
- componentDidMount() {
- }
-
- render() {
- const t = this.props.t;
-
- const columns = [
- { data: 4, title: "#" },
- { data: 1, title: t('name'),
- render: (data, cmd, rowData) => rowData[5] !== null ? {data} : data
- },
- { data: 2, title: t('type'), render: data => this.fieldTypes[data].label, sortable: false, searchable: false },
- { data: 3, title: t('mergeTag') },
- {
- actions: data => {
- const actions = [];
-
- if (this.props.list.permissions.includes('manageFields')) {
- actions.push({
- label: ,
- link: `/lists/${this.props.list.id}/fields/${data[0]}/edit`
- });
-
- tableAddDeleteButton(actions, this, null, `rest/fields/${this.props.list.id}/${data[0]}`, data[1], t('deletingField'), t('fieldDeleted'));
- }
-
- return actions;
- }
- }
- ];
-
- return (
-
- {tableRestActionDialogRender(this)}
- {this.props.list.permissions.includes('manageFields') &&
-
-
-
- }
-
-
{t('fields')}
-
-
this.table = node} withHeader dataUrl={`rest/fields-table/${this.props.list.id}`} columns={columns} />
-
- );
- }
-}
\ No newline at end of file
diff --git a/client/src/lists/fields/helpers.js b/client/src/lists/fields/helpers.js
deleted file mode 100644
index 07c84ba7..00000000
--- a/client/src/lists/fields/helpers.js
+++ /dev/null
@@ -1,54 +0,0 @@
-'use strict';
-
-import React from 'react';
-
-export function getFieldTypes(t) {
-
- const fieldTypes = {
- text: {
- label: t('text'),
- },
- website: {
- label: t('website'),
- },
- longtext: {
- label: t('multilineText'),
- },
- gpg: {
- label: t('gpgPublicKey'),
- },
- number: {
- label: t('number'),
- },
- 'checkbox-grouped': {
- label: t('checkboxesFromOptionFields'),
- },
- 'radio-grouped': {
- label: t('radioButtonsFromOptionFields')
- },
- 'dropdown-grouped': {
- label: t('dropDownFromOptionFields')
- },
- 'radio-enum': {
- label: t('radioButtonsEnumerated')
- },
- 'dropdown-enum': {
- label: t('dropDownEnumerated')
- },
- 'date': {
- label: t('date')
- },
- 'birthday': {
- label: t('birthday')
- },
- json: {
- label: t('jsonValueForCustomRendering')
- },
- option: {
- label: t('option')
- }
- };
-
- return fieldTypes;
-}
-
diff --git a/client/src/lists/forms/CUD.js b/client/src/lists/forms/CUD.js
deleted file mode 100644
index 6604ecba..00000000
--- a/client/src/lists/forms/CUD.js
+++ /dev/null
@@ -1,592 +0,0 @@
-'use strict';
-
-import React, {Component} from 'react';
-import PropTypes from 'prop-types';
-import {Trans} from 'react-i18next';
-import {withTranslation} from '../../lib/i18n';
-import {LinkButton, requiresAuthenticatedUser, Title, withPageHelpers} from '../../lib/page';
-import {
- ACEEditor,
- AlignedRow,
- Button,
- ButtonRow,
- CheckBox,
- Dropdown,
- Fieldset,
- filterData,
- Form,
- FormSendMethod,
- InputField,
- TableSelect,
- TextArea,
- withForm,
- withFormErrorHandlers
-} from '../../lib/form';
-import {withErrorHandling} from '../../lib/error-handling';
-import {getDefaultNamespace, NamespaceSelect, validateNamespace} from '../../lib/namespace';
-import {DeleteModalDialog} from "../../lib/modals";
-import mailtrainConfig from 'mailtrainConfig';
-import {getTrustedUrl, getUrl} from "../../lib/urls";
-import {ActionLink, Icon} from "../../lib/bootstrap-components";
-import styles from "../../lib/styles.scss";
-import formsStyles from "./styles.scss";
-import axios from "../../lib/axios";
-import {withComponentMixins} from "../../lib/decorator-helpers";
-
-@withComponentMixins([
- withTranslation,
- withForm,
- withErrorHandling,
- withPageHelpers,
- requiresAuthenticatedUser
-])
-export default class CUD extends Component {
- constructor(props) {
- super(props);
-
- this.state = {
- previewContents: null,
- previewFullscreen: false
- };
-
- this.serverValidatedFields = [
- 'layout',
- 'web_subscribe',
- 'web_confirm_subscription_notice',
- 'mail_confirm_subscription_html',
- 'mail_confirm_subscription_text',
- 'mail_already_subscribed_html',
- 'mail_already_subscribed_text',
- 'web_subscribed_notice',
- 'mail_subscription_confirmed_html',
- 'mail_subscription_confirmed_text',
- 'web_manage',
- 'web_manage_address',
- 'web_updated_notice',
- 'web_unsubscribe',
- 'web_confirm_unsubscription_notice',
- 'mail_confirm_unsubscription_html',
- 'mail_confirm_unsubscription_text',
- 'mail_confirm_address_change_html',
- 'mail_confirm_address_change_text',
- 'web_unsubscribed_notice',
- 'mail_unsubscription_confirmed_html',
- 'mail_unsubscription_confirmed_text',
- 'web_manual_unsubscribe_notice',
- 'web_privacy_policy_notice'
- ];
-
- this.initForm({
- serverValidation: {
- url: 'rest/forms-validate',
- changed: this.serverValidatedFields
- },
- onChange: {
- previewList: (newState, key, oldValue, newValue) => {
- newState.formState.setIn(['data', 'previewContents', 'value'], null);
- }
- }
- });
-
-
- const t = props.t;
-
- const helpEmailText = t('thePlaintextVersionForThisEmail');
- const helpMjmlGeneral = Custom forms use MJML for formatting. See the MJML documentation here ;
-
- this.templateSettings = {
- layout: {
- label: t('layout'),
- mode: 'html',
- help: helpMjmlGeneral,
- isLayout: true
- },
- form_input_style: {
- label: t('formInputStyle'),
- mode: 'css',
- help: t('thisCssStylesheetDefinesTheAppearanceOf')
- },
- web_subscribe: {
- label: t('webSubscribe'),
- mode: 'html',
- help: helpMjmlGeneral
- },
- web_confirm_subscription_notice: {
- label: t('webConfirmSubscriptionNotice'),
- mode: 'html',
- help: helpMjmlGeneral
- },
- mail_confirm_subscription_html: {
- label: t('mailConfirmSubscriptionMjml'),
- mode: 'html',
- help: helpMjmlGeneral
- },
- mail_confirm_subscription_text: {
- label: t('mailConfirmSubscriptionText'),
- mode: 'text',
- help: helpEmailText
- },
- mail_already_subscribed_html: {
- label: t('mailAlreadySubscribedMjml'),
- mode: 'html',
- help: helpMjmlGeneral
- },
- mail_already_subscribed_text: {
- label: t('mailAlreadySubscribedText'),
- mode: 'text',
- help: helpEmailText
- },
- web_subscribed_notice: {
- label: t('webSubscribedNotice'),
- mode: 'html',
- help: helpMjmlGeneral
- },
- mail_subscription_confirmed_html: {
- label: t('mailSubscriptionConfirmedMjml'),
- mode: 'html',
- help: helpMjmlGeneral
- },
- mail_subscription_confirmed_text: {
- label: t('mailSubscriptionConfirmedText'),
- mode: 'text',
- help: helpEmailText
- },
- web_manage: {
- label: t('webManagePreferences'),
- mode: 'html',
- help: helpMjmlGeneral
- },
- web_manage_address: {
- label: t('webManageAddress'),
- mode: 'html',
- help: helpMjmlGeneral
- },
- mail_confirm_address_change_html: {
- label: t('mailConfirmAddressChangeMjml'),
- mode: 'html',
- help: helpMjmlGeneral
- },
- mail_confirm_address_change_text: {
- label: t('mailConfirmAddressChangeText'),
- mode: 'text',
- help: helpEmailText
- },
- web_updated_notice: {
- label: t('webUpdatedNotice'),
- mode: 'html',
- help: helpMjmlGeneral
- },
- web_unsubscribe: {
- label: t('webUnsubscribe'),
- mode: 'html',
- help: helpMjmlGeneral
- },
- web_confirm_unsubscription_notice: {
- label: t('webConfirmUnsubscriptionNotice'),
- mode: 'html',
- help: helpMjmlGeneral
- },
- mail_confirm_unsubscription_html: {
- label: t('mailConfirmUnsubscriptionMjml'),
- mode: 'html',
- help: helpMjmlGeneral
- },
- mail_confirm_unsubscription_text: {
- label: t('mailConfirmUnsubscriptionText'),
- mode: 'text',
- help: helpEmailText
- },
- web_unsubscribed_notice: {
- label: t('webUnsubscribedNotice'),
- mode: 'html',
- help: helpMjmlGeneral
- },
- mail_unsubscription_confirmed_html: {
- label: t('mailUnsubscriptionConfirmedMjml'),
- mode: 'html',
- help: helpMjmlGeneral
- },
- mail_unsubscription_confirmed_text: {
- label: t('mailUnsubscriptionConfirmedText'),
- mode: 'text',
- help: helpEmailText
- },
- web_manual_unsubscribe_notice: {
- label: t('webManualUnsubscribeNotice'),
- mode: 'html',
- help: helpMjmlGeneral
- },
- web_privacy_policy_notice: {
- label: t('privacyPolicy'),
- mode: 'html',
- help: helpMjmlGeneral
- }
- };
-
- this.templateGroups = {
- general: {
- label: t('general'),
- options: [
- 'layout',
- 'form_input_style'
- ]
- },
- subscribe: {
- label: t('subscribe'),
- options: [
- 'web_subscribe',
- 'web_confirm_subscription_notice',
- 'mail_confirm_subscription_html',
- 'mail_confirm_subscription_text',
- 'mail_already_subscribed_html',
- 'mail_already_subscribed_text',
- 'web_subscribed_notice',
- 'mail_subscription_confirmed_html',
- 'mail_subscription_confirmed_text'
- ]
- },
- manage: {
- label: t('manage'),
- options: [
- 'web_manage',
- 'web_manage_address',
- 'mail_confirm_address_change_html',
- 'mail_confirm_address_change_text',
- 'web_updated_notice'
- ]
- },
- unsubscribe: {
- label: t('unsubscribe'),
- options: [
- 'web_unsubscribe',
- 'web_confirm_unsubscription_notice',
- 'mail_confirm_unsubscription_html',
- 'mail_confirm_unsubscription_text',
- 'web_unsubscribed_notice',
- 'mail_unsubscription_confirmed_html',
- 'mail_unsubscription_confirmed_text',
- 'web_manual_unsubscribe_notice'
- ]
- },
- gdpr: {
- label: t('dataProtection'),
- options: [
- 'web_privacy_policy_notice'
- ]
- },
- };
-
- }
-
- static propTypes = {
- action: PropTypes.string.isRequired,
- entity: PropTypes.object,
- permissions: PropTypes.object
- }
-
-
- supplyDefaults(data) {
- for (const key in mailtrainConfig.defaultCustomFormValues) {
- if (!data[key]) {
- data[key] = mailtrainConfig.defaultCustomFormValues[key];
- }
- }
- }
-
- getFormValuesMutator(data, originalData) {
- this.supplyDefaults(data);
- data.selectedTemplate = (originalData && originalData.selectedTemplate) || 'layout';
- }
-
- submitFormValuesMutator(data) {
- return filterData(data, ['name', 'description', 'namespace',
- 'fromExistingEntity', 'existingEntity',
-
- 'layout', 'form_input_style',
- 'web_subscribe',
- 'web_confirm_subscription_notice',
- 'mail_confirm_subscription_html',
- 'mail_confirm_subscription_text',
- 'mail_already_subscribed_html',
- 'mail_already_subscribed_text',
- 'web_subscribed_notice',
- 'mail_subscription_confirmed_html',
- 'mail_subscription_confirmed_text',
- 'web_manage',
- 'web_manage_address',
- 'web_updated_notice',
- 'web_unsubscribe',
- 'web_confirm_unsubscription_notice',
- 'mail_confirm_unsubscription_html',
- 'mail_confirm_unsubscription_text',
- 'mail_confirm_address_change_html',
- 'mail_confirm_address_change_text',
- 'web_unsubscribed_notice',
- 'mail_unsubscription_confirmed_html',
- 'mail_unsubscription_confirmed_text', 'web_manual_unsubscribe_notice', 'web_privacy_policy_notice'
- ]);
- }
-
- componentDidMount() {
- if (this.props.entity) {
- this.getFormValuesFromEntity(this.props.entity);
-
- } else {
- const data = {
- name: '',
- description: '',
- fromExistingEntity: false,
- existingEntity: null,
- selectedTemplate: 'layout',
- namespace: getDefaultNamespace(this.props.permissions)
- };
- this.supplyDefaults(data);
-
- this.populateFormValues(data);
- }
- }
-
- localValidateFormValues(state) {
- const t = this.props.t;
-
- if (!state.getIn(['name', 'value'])) {
- state.setIn(['name', 'error'], t('nameMustNotBeEmpty'));
- } else {
- state.setIn(['name', 'error'], null);
- }
-
- validateNamespace(t, state);
-
- if (state.getIn(['fromExistingEntity', 'value']) && !state.getIn(['existingEntity', 'value'])) {
- state.setIn(['existingEntity', 'error'], t('sourceCustomFormsMustNotBeEmpty'));
- } else {
- state.setIn(['existingEntity', 'error'], null);
- }
-
-
- let formsServerValidationRunning = false;
- const formsErrors = [];
-
- for (const fld of this.serverValidatedFields) {
- const serverValidation = state.getIn([fld, 'serverValidation']);
-
- if (serverValidation && serverValidation.errors) {
- formsErrors.push(...serverValidation.errors.map(x => {this.templateSettings[fld].label} {' '}–{' '}{x}
));
- } else if (!serverValidation) {
- formsServerValidationRunning = true;
- }
- }
-
- if (!formsErrors.length && formsServerValidationRunning) {
- formsErrors.push(t('validationIsInProgress'));
- }
-
- if (formsErrors.length) {
- state.setIn(['selectedTemplate', 'error'],
- {t('listOfErrorsInTemplates') + ':'}
-
- {formsErrors.map((msg, idx) => {msg} )}
-
-
);
- } else {
- state.setIn(['selectedTemplate', 'error'], null);
- }
- }
-
- @withFormErrorHandlers
- async submitHandler(submitAndLeave) {
- const t = this.props.t;
-
- let sendMethod, url;
- if (this.props.entity) {
- sendMethod = FormSendMethod.PUT;
- url = `rest/forms/${this.props.entity.id}`;
- } else {
- sendMethod = FormSendMethod.POST;
- url = 'rest/forms';
- }
-
- this.disableForm();
- this.setFormStatusMessage('info', t('saving'));
-
- const submitResult = await this.validateAndSendFormValuesToURL(sendMethod, url);
-
- if (submitResult) {
- if (this.props.entity) {
- if (submitAndLeave) {
- this.navigateToWithFlashMessage('/lists/forms', 'success', t('customFormsUpdated'));
- } else {
- await this.getFormValuesFromURL(`rest/forms/${this.props.entity.id}`);
- this.enableForm();
- this.setFormStatusMessage('success', t('customFormsUpdated'));
- }
- } else {
- if (submitAndLeave) {
- this.navigateToWithFlashMessage('/lists/forms', 'success', t('customFormsCreated'));
- } else {
- this.navigateToWithFlashMessage(`/lists/forms/${submitResult}/edit`, 'success', t('customFormsCreated'));
- }
- }
- } else {
- this.enableForm();
- this.setFormStatusMessage('warning', t('thereAreErrorsInTheFormPleaseFixThemAnd'));
- }
- }
-
- async preview(formKey) {
- const data = {
- formKey,
- template: this.getFormValue(formKey),
- layout: this.getFormValue('layout'),
- formInputStyle: this.getFormValue('form_input_style'),
- listId: this.getFormValue('previewList')
- }
-
- const response = await axios.post(getUrl('rest/forms-preview'), data);
-
- this.setState({
- previewKey: formKey,
- previewContents: response.data.content,
- previewLabel: this.templateSettings[formKey].label
- });
- }
-
- render() {
- const t = this.props.t;
- const isEdit = !!this.props.entity;
- const canDelete = isEdit && this.props.entity.permissions.includes('delete');
-
- const templateOptGroups = [];
-
- for (const grpKey in this.templateGroups) {
- const grp = this.templateGroups[grpKey];
- templateOptGroups.push({
- key: grpKey,
- label: grp.label,
- options: grp.options.map(opt => ({
- key: opt,
- label: this.templateSettings[opt].label
- }))
- });
- }
-
- const customFormsColumns = [
- { data: 1, title: t('name') },
- { data: 2, title: t('description') },
- { data: 3, title: t('namespace') }
- ];
-
- const listsColumns = [
- { data: 0, title: "#" },
- { data: 1, title: t('name') },
- { data: 2, title: t('id'), render: data => {data}
},
- { data: 5, title: t('namespace') }
- ];
-
- const previewListId = this.getFormValue('previewList');
- const selectedTemplate = this.getFormValue('selectedTemplate');
-
- return (
-
- {canDelete &&
-
- }
-
- {isEdit ? t('editCustomForms') : t('createCustomForms')}
-
-
-
-
-
-
-
-
- {!isEdit &&
-
- }
-
- {this.getFormValue('fromExistingEntity') ?
-
- :
- <>
-
-
-
- { previewListId &&
-
-
-
-
- {t('noteTheseLinksAreSolelyForAQuickPreview')}
-
-
-
- await this.preview('web_subscribe')}>Subscribe
- {' | '}
- await this.preview('web_confirm_subscription_notice')}>Confirm Subscription Notice
- {' | '}
- await this.preview('web_confirm_unsubscription_notice')}>Confirm Unsubscription Notice
- {' | '}
- await this.preview('web_subscribed_notice')}>Subscribed Notice
- {' | '}
- await this.preview('web_updated_notice')}>Updated Notice
- {' | '}
- await this.preview('web_unsubscribed_notice')}>Unsubscribed Notice
- {' | '}
- await this.preview('web_manual_unsubscribe_notice')}>Manual Unsubscribe Notice
- {' | '}
- await this.preview('web_unsubscribe')}>Unsubscribe
- {' | '}
- await this.preview('web_manage')}>Manage
- {' | '}
- await this.preview('web_manage_address')}>Manage Address
- {' | '}
- await this.preview('web_privacy_policy_notice')}>Privacy Policy
-
-
- {this.state.previewContents &&
-
-
-
- {this.state.fullscreen &&
}
-
{t('formPreview') + ' ' + this.state.previewLabel}
-
-
-
-
-
- }
-
- }
-
-
- { selectedTemplate &&
-
-
-
-
- }
- >
- }
-
-
-
- await this.submitHandler(true)}/>
- {canDelete && }
-
-
-
- );
- }
-}
\ No newline at end of file
diff --git a/client/src/lists/forms/List.js b/client/src/lists/forms/List.js
deleted file mode 100644
index 5b128ae8..00000000
--- a/client/src/lists/forms/List.js
+++ /dev/null
@@ -1,81 +0,0 @@
-'use strict';
-
-import React, {Component} from 'react';
-import {withTranslation} from '../../lib/i18n';
-import {LinkButton, requiresAuthenticatedUser, Title, Toolbar, withPageHelpers} from '../../lib/page';
-import {withErrorHandling} from '../../lib/error-handling';
-import {Table} from '../../lib/table';
-import {Icon} from "../../lib/bootstrap-components";
-import {tableAddDeleteButton, tableRestActionDialogInit, tableRestActionDialogRender} from "../../lib/modals";
-import {withComponentMixins} from "../../lib/decorator-helpers";
-import PropTypes from 'prop-types';
-
-@withComponentMixins([
- withTranslation,
- withErrorHandling,
- withPageHelpers,
- requiresAuthenticatedUser
-])
-export default class List extends Component {
- constructor(props) {
- super(props);
-
- this.state = {};
- tableRestActionDialogInit(this);
- }
-
- static propTypes = {
- permissions: PropTypes.object
- }
-
- render() {
- const t = this.props.t;
-
- const permissions = this.props.permissions;
- const createPermitted = permissions.createCustomForm;
-
- const columns = [
- { data: 1, title: t('name') },
- { data: 2, title: t('description') },
- { data: 3, title: t('namespace') },
- {
- actions: data => {
- const actions = [];
- const perms = data[4];
-
- if (perms.includes('edit')) {
- actions.push({
- label: ,
- link: `/lists/forms/${data[0]}/edit`
- });
- }
- if (perms.includes('share')) {
- actions.push({
- label: ,
- link: `/lists/forms/${data[0]}/share`
- });
- }
-
- tableAddDeleteButton(actions, this, perms, `rest/forms/${data[0]}`, data[1], t('deletingForm'), t('formDeleted'));
-
- return actions;
- }
- }
- ];
-
- return (
-
- {tableRestActionDialogRender(this)}
- {createPermitted &&
-
-
-
- }
-
-
{t('forms')}
-
-
this.table = node} withHeader dataUrl="rest/forms-table" columns={columns} />
-
- );
- }
-}
\ No newline at end of file
diff --git a/client/src/lists/forms/styles.scss b/client/src/lists/forms/styles.scss
deleted file mode 100644
index 45d818cb..00000000
--- a/client/src/lists/forms/styles.scss
+++ /dev/null
@@ -1,11 +0,0 @@
-$editorNormalHeight: 400px;
-@import "../../lib/sandbox-common";
-
-.editor {
- margin-bottom: 15px;
-}
-
-.host {
- border: none;
- width: 100%;
-}
diff --git a/client/src/lists/imports/CUD.js b/client/src/lists/imports/CUD.js
deleted file mode 100644
index 887df141..00000000
--- a/client/src/lists/imports/CUD.js
+++ /dev/null
@@ -1,472 +0,0 @@
-'use strict';
-
-import React, {Component} from 'react';
-import PropTypes from 'prop-types';
-import {withTranslation} from '../../lib/i18n';
-import {LinkButton, requiresAuthenticatedUser, Title, withPageHelpers} from '../../lib/page';
-import {
- AlignedRow,
- Button,
- ButtonRow,
- CheckBox,
- Dropdown,
- Fieldset,
- filterData,
- Form,
- FormSendMethod,
- InputField,
- StaticField,
- TextArea,
- withForm,
- withFormErrorHandlers
-} from '../../lib/form';
-import {withAsyncErrorHandler, withErrorHandling} from '../../lib/error-handling';
-import {DeleteModalDialog} from "../../lib/modals";
-import {getImportLabels} from './helpers';
-import {ImportSource, inProgress, MappingType, prepInProgress, prepFinished} from '../../../../shared/imports';
-import axios from "../../lib/axios";
-import {getUrl} from "../../lib/urls";
-import listStyles from "../styles.scss";
-import styles from "../../lib/styles.scss";
-import interoperableErrors from "../../../../shared/interoperable-errors";
-import {withComponentMixins} from "../../lib/decorator-helpers";
-
-
-function truncate(str, len, ending = '...') {
- str = str.trim();
-
- if (str.length > len) {
- return str.substring(0, len - ending.length) + ending;
- } else {
- return str;
- }
-}
-
-
-@withComponentMixins([
- withTranslation,
- withForm,
- withErrorHandling,
- withPageHelpers,
- requiresAuthenticatedUser
-])
-export default class CUD extends Component {
- constructor(props) {
- super(props);
-
- this.state = {};
-
- const {importSourceLabels, mappingTypeLabels} = getImportLabels(props.t);
-
- this.importSourceLabels = importSourceLabels;
-
- this.importSourceOptions = [
- {key: ImportSource.CSV_FILE, label: importSourceLabels[ImportSource.CSV_FILE]},
- // {key: ImportSource.LIST, label: importSourceLabels[ImportSource.LIST]}
- ];
-
- this.mappingOptions = [
- {key: MappingType.BASIC_SUBSCRIBE, label: mappingTypeLabels[MappingType.BASIC_SUBSCRIBE]},
- {key: MappingType.BASIC_UNSUBSCRIBE, label: mappingTypeLabels[MappingType.BASIC_UNSUBSCRIBE]},
- ];
-
- this.refreshTimeoutHandler = ::this.refreshEntity;
- this.refreshTimeoutId = 0;
-
- this.initForm();
- }
-
- static propTypes = {
- action: PropTypes.string.isRequired,
- list: PropTypes.object,
- fieldsGrouped: PropTypes.array,
- entity: PropTypes.object
- }
-
- getFormValuesMutator(data) {
- data.settings = data.settings || {};
- const mapping = data.mapping || {};
-
- if (data.source === ImportSource.CSV_FILE) {
- data.csvFileName = data.settings.csv.originalname;
- data.csvDelimiter = data.settings.csv.delimiter;
- }
-
- const mappingSettings = mapping.settings || {};
- data.mapping_settings_checkEmails = 'checkEmails' in mappingSettings ? !!mappingSettings.checkEmails : true;
-
- const mappingFlds = mapping.fields || {};
- for (const field of this.props.fieldsGrouped) {
- if (field.column) {
- const colMapping = mappingFlds[field.column] || {};
- data['mapping_fields_' + field.column + '_column'] = colMapping.column || '';
- } else {
- for (const option of field.settings.options) {
- const col = field.groupedOptions[option.key].column;
- const colMapping = mappingFlds[col] || {};
- data['mapping_fields_' + col + '_column'] = colMapping.column || '';
- }
- }
- }
-
- const emailMapping = mappingFlds.email || {};
- data.mapping_fields_email_column = emailMapping.column || '';
- }
-
- submitFormValuesMutator(data, isSubmit) {
- const isEdit = !!this.props.entity;
-
- data.source = Number.parseInt(data.source);
- data.settings = {};
-
- let formData, csvFileSelected = false;
- if (isSubmit) {
- formData = new FormData();
-
- }
-
- if (!isEdit) {
- if (data.source === ImportSource.CSV_FILE) {
- data.settings.csv = {};
-
- // This test needs to be here because this function is also called by the form change detection mechanism
- if (this.csvFile && this.csvFile.files && this.csvFile.files.length > 0) {
- if (isSubmit) {
- formData.append('csvFile', this.csvFile.files[0]);
- } else {
- csvFileSelected = true;
- }
- }
-
- data.settings.csv.delimiter = data.csvDelimiter.trim();
- }
-
- } else {
- data.mapping_type = Number.parseInt(data.mapping_type);
- const mapping = {
- fields: {},
- settings: {}
- };
-
- if (data.mapping_type === MappingType.BASIC_SUBSCRIBE) {
- mapping.settings.checkEmails = data.mapping_settings_checkEmails;
-
- for (const field of this.props.fieldsGrouped) {
- if (field.column) {
- const colMapping = data['mapping_fields_' + field.column + '_column'];
- if (colMapping) {
- mapping.fields[field.column] = {
- column: colMapping
- };
- }
- } else {
- for (const option of field.settings.options) {
- const col = field.groupedOptions[option.key].column;
- const colMapping = data['mapping_fields_' + col + '_column'];
- if (colMapping) {
- mapping.fields[col] = {
- column: colMapping
- };
- }
- }
- }
- }
- }
-
- if (data.mapping_type === MappingType.BASIC_SUBSCRIBE || data.mapping_type === MappingType.BASIC_UNSUBSCRIBE) {
- mapping.fields.email = {
- column: data.mapping_fields_email_column
- };
- }
-
-
- data.mapping = mapping;
- }
-
- if (isSubmit) {
- formData.append('entity', JSON.stringify(
- filterData(data, ['name', 'description', 'source', 'settings', 'mapping_type', 'mapping'])
- ));
-
- return formData;
-
- } else {
- const filteredData = filterData(data, ['name', 'description', 'source', 'settings', 'mapping_type', 'mapping']);
- if (csvFileSelected) {
- filteredData.csvFileSelected = true;
- }
-
- return filteredData;
- }
- }
-
- initFromEntity(entity) {
- this.getFormValuesFromEntity(entity);
-
- if (inProgress(entity.status)) {
- this.refreshTimeoutId = setTimeout(this.refreshTimeoutHandler, 1000);
- }
- }
-
- componentDidMount() {
- if (this.props.entity) {
- this.initFromEntity(this.props.entity);
- } else {
- this.populateFormValues({
- name: '',
- description: '',
- source: ImportSource.CSV_FILE,
- csvFileName: '',
- csvDelimiter: ',',
- });
- }
- }
-
- componentWillUnmount() {
- clearTimeout(this.refreshTimeoutId);
- }
-
- @withAsyncErrorHandler
- async refreshEntity() {
- const resp = await axios.get(getUrl(`rest/imports/${this.props.list.id}/${this.props.entity.id}`));
- this.initFromEntity(resp.data);
- }
-
- localValidateFormValues(state) {
- const t = this.props.t;
- const isEdit = !!this.props.entity;
- const source = Number.parseInt(state.getIn(['source', 'value']));
-
- for (const key of state.keys()) {
- state.setIn([key, 'error'], null);
- }
-
- if (!state.getIn(['name', 'value'])) {
- state.setIn(['name', 'error'], t('nameMustNotBeEmpty'));
- }
-
- if (!isEdit) {
- if (source === ImportSource.CSV_FILE) {
- if (!this.csvFile || this.csvFile.files.length === 0) {
- state.setIn(['csvFileName', 'error'], t('fileMustBeSelected'));
- }
-
- if (!state.getIn(['csvDelimiter', 'value']).trim()) {
- state.setIn(['csvDelimiter', 'error'], t('csvDelimiterMustNotBeEmpty'));
- }
- }
- } else {
- const mappingType = Number.parseInt(state.getIn(['mapping_type', 'value']));
-
- if (mappingType === MappingType.BASIC_SUBSCRIBE || mappingType === MappingType.BASIC_UNSUBSCRIBE) {
- if (!state.getIn(['mapping_fields_email_column', 'value'])) {
- state.setIn(['mapping_fields_email_column', 'error'], t('emailMappingHasToBeProvided'));
- }
- }
- }
- }
-
- async submitHandler() {
- await this.save();
- }
-
- @withFormErrorHandlers
- async save(runAfterSave) {
- const t = this.props.t;
- const isEdit = !!this.props.entity;
-
-
- let sendMethod, url;
- if (this.props.entity) {
- sendMethod = FormSendMethod.PUT;
- url = `rest/imports/${this.props.list.id}/${this.props.entity.id}`
- } else {
- sendMethod = FormSendMethod.POST;
- url = `rest/imports/${this.props.list.id}`
- }
-
- try {
- this.disableForm();
- this.setFormStatusMessage('info', t('saving'));
-
- const submitResponse = await this.validateAndSendFormValuesToURL(sendMethod, url);
-
- if (submitResponse) {
- if (!isEdit) {
- this.navigateTo(`/lists/${this.props.list.id}/imports/${submitResponse}/edit`);
- } else {
- if (runAfterSave) {
- try {
- await axios.post(getUrl(`rest/import-start/${this.props.list.id}/${this.props.entity.id}`));
- } catch (err) {
- if (err instanceof interoperableErrors.InvalidStateError) {
- // Just mask the fact that it's not possible to start anything and refresh instead.
- } else {
- throw err;
- }
- }
- }
-
- this.navigateToWithFlashMessage(`/lists/${this.props.list.id}/imports/${this.props.entity.id}/status`, 'success', t('importSaved'));
- }
-
- } else {
- this.enableForm();
- this.setFormStatusMessage('warning', t('thereAreErrorsInTheFormPleaseFixThemAnd'));
- }
- } catch (error) {
- throw error;
- }
- }
-
- onFileSelected(evt, x) {
- if (!this.getFormValue('name') && this.csvFile.files.length > 0) {
- this.updateFormValue('name', this.csvFile.files[0].name);
- }
-
- this.scheduleFormRevalidate();
- }
-
- render() {
- const t = this.props.t;
- const isEdit = !!this.props.entity;
-
- const source = Number.parseInt(this.getFormValue('source'));
- const status = this.getFormValue('status');
- const settings = this.getFormValue('settings');
-
- let settingsEdit = null;
- if (source === ImportSource.CSV_FILE) {
- if (isEdit) {
- settingsEdit =
-
- {this.getFormValue('csvFileName')}
- {this.getFormValue('csvDelimiter')}
-
;
- } else {
- settingsEdit =
- ;
- }
- }
-
- let mappingEdit;
- if (isEdit) {
- if (prepInProgress(status)) {
- mappingEdit = (
- {t('preparationInProgressPleaseWaitTillItIs')}
- );
-
- } else {
- let mappingSettings = null;
- const mappingType = Number.parseInt(this.getFormValue('mapping_type'));
-
- if (mappingType === MappingType.BASIC_SUBSCRIBE || mappingType === MappingType.BASIC_UNSUBSCRIBE) {
- const sampleRow = this.getFormValue('sampleRow');
- const sourceOpts = [];
- sourceOpts.push({key: '', label: t('––Select ––')});
- if (source === ImportSource.CSV_FILE) {
- for (const csvCol of settings.csv.columns) {
- let help = '';
- if (sampleRow) {
- help = ' (' + t('eg', {keySeparator: '>', nsSeparator: '|'}) + ' ' + truncate(sampleRow[csvCol.column], 50) + ')';
- }
-
- sourceOpts.push({key: csvCol.column, label: csvCol.name + help});
- }
- }
-
- const settingsRows = [];
- const mappingRows = [
-
- ];
-
- if (mappingType === MappingType.BASIC_SUBSCRIBE) {
- settingsRows.push()
-
- for (const field of this.props.fieldsGrouped) {
- if (field.column) {
- mappingRows.push(
-
- );
- } else {
- for (const option of field.settings.options) {
- const col = field.groupedOptions[option.key].column;
- mappingRows.push(
-
- );
- }
- }
- }
- }
-
- mappingSettings = (
-
- {settingsRows}
-
- {mappingRows}
-
-
- );
- }
-
- mappingEdit = (
-
-
- {mappingSettings}
-
- );
- }
- }
-
- const saveButtons = []
- if (!isEdit) {
- saveButtons.push();
- } else {
- if (prepFinished(status)) {
- saveButtons.push();
- saveButtons.push( await this.save(true)}/>);
- }
- }
-
- return (
-
- {isEdit &&
-
- }
-
- {isEdit ? t('editImport') : t('createImport')}
-
-
-
-
-
- {isEdit ?
- {this.importSourceLabels[this.getFormValue('source')]}
- :
-
- }
-
- {settingsEdit}
-
- {mappingEdit}
-
-
-
- {saveButtons}
- {isEdit && }
-
-
-
- );
- }
-}
\ No newline at end of file
diff --git a/client/src/lists/imports/List.js b/client/src/lists/imports/List.js
deleted file mode 100644
index 7e077748..00000000
--- a/client/src/lists/imports/List.js
+++ /dev/null
@@ -1,98 +0,0 @@
-'use strict';
-
-import React, {Component} from 'react';
-import PropTypes from 'prop-types';
-import {withTranslation} from '../../lib/i18n';
-import {LinkButton, requiresAuthenticatedUser, Title, Toolbar, withPageHelpers} from '../../lib/page';
-import {withErrorHandling} from '../../lib/error-handling';
-import {Table} from '../../lib/table';
-import {getImportLabels} from './helpers';
-import {Icon} from "../../lib/bootstrap-components";
-import mailtrainConfig from 'mailtrainConfig';
-import moment from "moment";
-import {inProgress} from '../../../../shared/imports';
-import {tableAddDeleteButton, tableRestActionDialogInit, tableRestActionDialogRender} from "../../lib/modals";
-import {withComponentMixins} from "../../lib/decorator-helpers";
-
-@withComponentMixins([
- withTranslation,
- withErrorHandling,
- withPageHelpers,
- requiresAuthenticatedUser
-])
-export default class List extends Component {
- constructor(props) {
- super(props);
-
- this.state = {};
- tableRestActionDialogInit(this);
-
- const {importSourceLabels, importStatusLabels} = getImportLabels(props.t);
- this.importSourceLabels = importSourceLabels;
- this.importStatusLabels = importStatusLabels;
- }
-
- static propTypes = {
- list: PropTypes.object
- }
-
- componentDidMount() {
- }
-
- render() {
- const t = this.props.t;
-
- const columns = [
- { data: 1, title: t('name') },
- { data: 2, title: t('description') },
- { data: 3, title: t('source'), render: data => this.importSourceLabels[data], sortable: false, searchable: false },
- { data: 4, title: t('status'), render: data => this.importStatusLabels[data], sortable: false, searchable: false },
- { data: 5, title: t('lastRun'), render: data => data ? moment(data).fromNow() : t('never') },
- {
- actions: data => {
- const actions = [];
- const status = data[4];
-
- let refreshTimeout;
-
- if (inProgress(status)) {
- refreshTimeout = 1000;
- }
-
- if (mailtrainConfig.globalPermissions.setupAutomation && this.props.list.permissions.includes('manageImports')) {
- actions.push({
- label: ,
- link: `/lists/${this.props.list.id}/imports/${data[0]}/edit`
- });
- }
-
- actions.push({
- label: ,
- link: `/lists/${this.props.list.id}/imports/${data[0]}/status`
- });
-
- if (this.props.list.permissions.includes('manageImports')) {
- tableAddDeleteButton(actions, this, null, `rest/imports/${this.props.list.id}/${data[0]}`, data[1], t('deletingImport'), t('importDeleted'));
- }
-
- return { refreshTimeout, actions };
- }
- }
- ];
-
- return (
-
- {tableRestActionDialogRender(this)}
- {mailtrainConfig.globalPermissions.setupAutomation && this.props.list.permissions.includes('manageImports') &&
-
-
-
- }
-
-
{t('imports')}
-
-
this.table = node} withHeader dataUrl={`rest/imports-table/${this.props.list.id}`} columns={columns} />
-
- );
- }
-}
\ No newline at end of file
diff --git a/client/src/lists/imports/RunStatus.js b/client/src/lists/imports/RunStatus.js
deleted file mode 100644
index 60add547..00000000
--- a/client/src/lists/imports/RunStatus.js
+++ /dev/null
@@ -1,109 +0,0 @@
-'use strict';
-
-import React, {Component} from 'react';
-import PropTypes from 'prop-types';
-import {withTranslation} from '../../lib/i18n';
-import {requiresAuthenticatedUser, Title, withPageHelpers} from '../../lib/page';
-import {AlignedRow} from '../../lib/form';
-import {withAsyncErrorHandler, withErrorHandling} from '../../lib/error-handling';
-import {getImportLabels} from './helpers';
-import axios from "../../lib/axios";
-import {getUrl} from "../../lib/urls";
-import moment from "moment";
-import {runStatusInProgress} from "../../../../shared/imports";
-import {Table} from "../../lib/table";
-import {withComponentMixins} from "../../lib/decorator-helpers";
-
-@withComponentMixins([
- withTranslation,
- withErrorHandling,
- withPageHelpers,
- requiresAuthenticatedUser
-])
-export default class Status extends Component {
- constructor(props) {
- super(props);
-
- this.state = {
- entity: props.entity
- };
-
- const {importSourceLabels, importStatusLabels, runStatusLabels} = getImportLabels(props.t);
- this.importSourceLabels = importSourceLabels;
- this.importStatusLabels = importStatusLabels;
- this.runStatusLabels = runStatusLabels;
-
- this.refreshTimeoutHandler = ::this.periodicRefreshTask;
- this.refreshTimeoutId = 0;
- }
-
- static propTypes = {
- entity: PropTypes.object,
- imprt: PropTypes.object,
- list: PropTypes.object
- }
-
- @withAsyncErrorHandler
- async refreshEntity() {
- const resp = await axios.get(getUrl(`rest/import-runs/${this.props.list.id}/${this.props.imprt.id}/${this.props.entity.id}`));
- this.setState({
- entity: resp.data
- });
-
- if (this.failedTableNode) {
- this.failedTableNode.refresh();
- }
- }
-
- async periodicRefreshTask() {
- if (runStatusInProgress(this.state.entity.status)) {
- await this.refreshEntity();
- if (this.refreshTimeoutHandler) { // For some reason the task gets rescheduled if server is restarted while the page is shown. That why we have this check here.
- this.refreshTimeoutId = setTimeout(this.refreshTimeoutHandler, 2000);
- }
- }
- }
-
- componentDidMount() {
- // noinspection JSIgnoredPromiseFromCall
- this.periodicRefreshTask();
- }
-
- componentWillUnmount() {
- clearTimeout(this.refreshTimeoutId);
- this.refreshTimeoutHandler = null;
- }
-
- render() {
- const t = this.props.t;
- const entity = this.state.entity;
- const imprt = this.props.imprt;
-
- const columns = [
- { data: 1, title: t('row') },
- { data: 2, title: t('email') },
- { data: 3, title: t('reason'), render: data => t(...JSON.parse(data)) }
- ];
-
- return (
-
-
{t('importRunStatus')}
-
-
{imprt.name}
-
{this.importSourceLabels[imprt.source]}
-
{moment(entity.created).fromNow()}
- {entity.finished &&
{moment(entity.finished).fromNow()} }
-
{this.runStatusLabels[entity.status]}
-
{entity.processed}
-
{entity.new}
-
{entity.failed}
- {entity.error &&
{entity.error} }
-
-
-
{t('failedRows')}
-
this.failedTableNode = node} withHeader dataUrl={`rest/import-run-failed-table/${this.props.list.id}/${this.props.imprt.id}/${this.props.entity.id}`} columns={columns} />
-
-
- );
- }
-}
\ No newline at end of file
diff --git a/client/src/lists/imports/Status.js b/client/src/lists/imports/Status.js
deleted file mode 100644
index 3e1af710..00000000
--- a/client/src/lists/imports/Status.js
+++ /dev/null
@@ -1,161 +0,0 @@
-'use strict';
-
-import React, {Component} from 'react';
-import PropTypes from 'prop-types';
-import {withTranslation} from '../../lib/i18n';
-import {requiresAuthenticatedUser, Title, withPageHelpers} from '../../lib/page';
-import {AlignedRow, ButtonRow} from '../../lib/form';
-import {withAsyncErrorHandler, withErrorHandling} from '../../lib/error-handling';
-import {getImportLabels} from './helpers';
-import {prepFinishedAndNotInProgress, runInProgress, runStatusInProgress} from '../../../../shared/imports';
-import {Table} from "../../lib/table";
-import {Button, Icon} from "../../lib/bootstrap-components";
-import axios from "../../lib/axios";
-import {getUrl} from "../../lib/urls";
-import moment from "moment";
-import interoperableErrors from '../../../../shared/interoperable-errors';
-import {withComponentMixins} from "../../lib/decorator-helpers";
-
-@withComponentMixins([
- withTranslation,
- withErrorHandling,
- withPageHelpers,
- requiresAuthenticatedUser
-])
-export default class Status extends Component {
- constructor(props) {
- super(props);
-
- this.state = {
- entity: props.entity
- };
-
- const {importSourceLabels, importStatusLabels, runStatusLabels} = getImportLabels(props.t);
- this.importSourceLabels = importSourceLabels;
- this.importStatusLabels = importStatusLabels;
- this.runStatusLabels = runStatusLabels;
-
- this.refreshTimeoutHandler = ::this.periodicRefreshTask;
- this.refreshTimeoutId = 0;
- }
-
- static propTypes = {
- entity: PropTypes.object,
- list: PropTypes.object
- }
-
- @withAsyncErrorHandler
- async refreshEntity() {
- const resp = await axios.get(getUrl(`rest/imports/${this.props.list.id}/${this.props.entity.id}`));
- this.setState({
- entity: resp.data
- });
- }
-
- async periodicRefreshTask() {
- // The periodic task runs all the time, so that we don't have to worry about starting/stopping it as a reaction to the buttons.
- await this.refreshEntity();
- if (this.refreshTimeoutHandler) { // For some reason the task gets rescheduled if server is restarted while the page is shown. That why we have this check here.
- this.refreshTimeoutId = setTimeout(this.refreshTimeoutHandler, 2000);
- }
- }
-
- componentDidMount() {
- // noinspection JSIgnoredPromiseFromCall
- this.periodicRefreshTask();
- }
-
- componentWillUnmount() {
- clearTimeout(this.refreshTimeoutId);
- this.refreshTimeoutHandler = null;
- }
-
- async startRunAsync() {
- try {
- await axios.post(getUrl(`rest/import-start/${this.props.list.id}/${this.props.entity.id}`));
- } catch (err) {
- if (err instanceof interoperableErrors.InvalidStateError) {
- // Just mask the fact that it's not possible to start anything and refresh instead.
- } else {
- throw err;
- }
- }
-
- await this.refreshEntity();
-
- if (this.runsTableNode) {
- this.runsTableNode.refresh();
- }
- }
-
- async stopRunAsync() {
- try {
- await axios.post(getUrl(`rest/import-stop/${this.props.list.id}/${this.props.entity.id}`));
- } catch (err) {
- if (err instanceof interoperableErrors.InvalidStateError) {
- // Just mask the fact that it's not possible to stop anything and refresh instead.
- } else {
- throw err;
- }
- }
-
- await this.refreshEntity();
-
- if (this.runsTableNode) {
- this.runsTableNode.refresh();
- }
- }
-
- render() {
- const t = this.props.t;
- const entity = this.state.entity;
-
- const columns = [
- { data: 1, title: t('started'), render: data => moment(data).fromNow() },
- { data: 2, title: t('finished'), render: data => data ? moment(data).fromNow() : '' },
- { data: 3, title: t('status'), render: data => this.runStatusLabels[data], sortable: false, searchable: false },
- { data: 4, title: t('processed') },
- { data: 5, title: t('new') },
- { data: 6, title: t('failed') },
- {
- actions: data => {
- const actions = [];
- const status = data[3];
-
- let refreshTimeout;
-
- if (runStatusInProgress(status)) {
- refreshTimeout = 1000;
- }
-
- actions.push({
- label: ,
- link: `/lists/${this.props.list.id}/imports/${this.props.entity.id}/status/${data[0]}`
- });
-
- return { refreshTimeout, actions };
- }
- }
- ];
-
- return (
-
-
{t('importStatus')}
-
-
{entity.name}
-
{this.importSourceLabels[entity.source]}
-
{this.importStatusLabels[entity.status]}
- {entity.error &&
{entity.error} }
-
-
- {prepFinishedAndNotInProgress(entity.status) && }
- {runInProgress(entity.status) && }
-
-
-
-
{t('importRuns')}
-
this.runsTableNode = node} withHeader dataUrl={`rest/import-runs-table/${this.props.list.id}/${this.props.entity.id}`} columns={columns} />
-
- );
- }
-}
\ No newline at end of file
diff --git a/client/src/lists/imports/helpers.js b/client/src/lists/imports/helpers.js
deleted file mode 100644
index b0bc5a1f..00000000
--- a/client/src/lists/imports/helpers.js
+++ /dev/null
@@ -1,46 +0,0 @@
-'use strict';
-
-import React from 'react';
-import {ImportSource, ImportStatus, MappingType, RunStatus} from '../../../../shared/imports';
-
-export function getImportLabels(t) {
-
- const importSourceLabels = {
- [ImportSource.CSV_FILE]: t('csvFile'),
- [ImportSource.LIST]: t('list'),
- };
-
- const importStatusLabels = {
- [ImportStatus.PREP_SCHEDULED]: t('created'),
- [ImportStatus.PREP_RUNNING]: t('preparing'),
- [ImportStatus.PREP_STOPPING]: t('stopping'),
- [ImportStatus.PREP_FINISHED]: t('ready'),
- [ImportStatus.PREP_FAILED]: t('preparationFailed'),
- [ImportStatus.RUN_SCHEDULED]: t('scheduled'),
- [ImportStatus.RUN_RUNNING]: t('running'),
- [ImportStatus.RUN_STOPPING]: t('stopping'),
- [ImportStatus.RUN_FINISHED]: t('finished'),
- [ImportStatus.RUN_FAILED]: t('failed')
- };
-
- const runStatusLabels = {
- [RunStatus.SCHEDULED]: t('starting'),
- [RunStatus.RUNNING]: t('running'),
- [RunStatus.STOPPING]: t('stopping'),
- [RunStatus.FINISHED]: t('finished'),
- [RunStatus.FAILED]: t('failed')
- };
-
- const mappingTypeLabels = {
- [MappingType.BASIC_SUBSCRIBE]: t('basicImportOfSubscribers'),
- [MappingType.BASIC_UNSUBSCRIBE]: t('unsubscribeEmails'),
- }
-
- return {
- importStatusLabels,
- mappingTypeLabels,
- importSourceLabels,
- runStatusLabels
- };
-}
-
diff --git a/client/src/lists/root.js b/client/src/lists/root.js
deleted file mode 100644
index 6a20410e..00000000
--- a/client/src/lists/root.js
+++ /dev/null
@@ -1,255 +0,0 @@
-'use strict';
-
-import React from 'react';
-import qs from 'querystringify';
-import ListsList from './List';
-import ListsCUD from './CUD';
-import FormsList from './forms/List';
-import FormsCUD from './forms/CUD';
-import FieldsList from './fields/List';
-import FieldsCUD from './fields/CUD';
-import SubscriptionsList from './subscriptions/List';
-import SubscriptionsCUD from './subscriptions/CUD';
-import SegmentsList from './segments/List';
-import SegmentsCUD from './segments/CUD';
-import ImportsList from './imports/List';
-import ImportsCUD from './imports/CUD';
-import ImportsStatus from './imports/Status';
-import ImportRunsStatus from './imports/RunStatus';
-import Share from '../shares/Share';
-import TriggersList from './TriggersList';
-import {ellipsizeBreadcrumbLabel} from "../lib/helpers";
-import {namespaceCheckPermissions} from "../lib/namespace";
-
-function getMenus(t) {
- return {
- 'lists': {
- title: t('lists'),
- link: '/lists',
- checkPermissions: {
- createList: {
- entityTypeId: 'namespace',
- requiredOperations: ['createList']
- },
- createCustomForm: {
- entityTypeId: 'namespace',
- requiredOperations: ['createCustomForm']
- },
- viewCustomForm: {
- entityTypeId: 'customForm',
- requiredOperations: ['view']
- },
- ...namespaceCheckPermissions('createList')
- },
- panelRender: props => ,
- children: {
- ':listId([0-9]+)': {
- title: resolved => t('listName', {name: ellipsizeBreadcrumbLabel(resolved.list.name)}),
- resolve: {
- list: params => `rest/lists/${params.listId}`
- },
- link: params => `/lists/${params.listId}/subscriptions`,
- navs: {
- subscriptions: {
- title: t('subscribers'),
- resolve: {
- segments: params => `rest/segments/${params.listId}`,
- },
- link: params => `/lists/${params.listId}/subscriptions`,
- visible: resolved => resolved.list.permissions.includes('viewSubscriptions'),
- panelRender: props => ,
- children: {
- ':subscriptionId([0-9]+)': {
- title: resolved => resolved.subscription.email,
- resolve: {
- 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: {
- ':action(edit|delete)': {
- title: t('edit'),
- link: params => `/lists/${params.listId}/subscriptions/${params.subscriptionId}/edit`,
- panelRender: props =>
- }
- }
- },
- create: {
- title: t('create'),
- resolve: {
- fieldsGrouped: params => `rest/fields-grouped/${params.listId}`
- },
- panelRender: props =>
- }
- } },
- ':action(edit|delete)': {
- title: t('edit'),
- link: params => `/lists/${params.listId}/edit`,
- visible: resolved => resolved.list.permissions.includes('edit'),
- panelRender: props =>
- },
- fields: {
- title: t('fields'),
- link: params => `/lists/${params.listId}/fields/`,
- visible: resolved => resolved.list.permissions.includes('viewFields'),
- panelRender: props => ,
- children: {
- ':fieldId([0-9]+)': {
- title: resolved => t('fieldName-1', {name: ellipsizeBreadcrumbLabel(resolved.field.name)}),
- resolve: {
- field: params => `rest/fields/${params.listId}/${params.fieldId}`,
- fields: params => `rest/fields/${params.listId}`
- },
- link: params => `/lists/${params.listId}/fields/${params.fieldId}/edit`,
- navs: {
- ':action(edit|delete)': {
- title: t('edit'),
- link: params => `/lists/${params.listId}/fields/${params.fieldId}/edit`,
- panelRender: props =>
- }
- }
- },
- create: {
- title: t('create'),
- resolve: {
- fields: params => `rest/fields/${params.listId}`
- },
- panelRender: props =>
- }
- }
- },
- segments: {
- title: t('segments'),
- link: params => `/lists/${params.listId}/segments`,
- visible: resolved => resolved.list.permissions.includes('viewSegments'),
- panelRender: props => ,
- children: {
- ':segmentId([0-9]+)': {
- title: resolved => t('segmentName', {name: ellipsizeBreadcrumbLabel(resolved.segment.name)}),
- resolve: {
- segment: params => `rest/segments/${params.listId}/${params.segmentId}`,
- fields: params => `rest/fields/${params.listId}`
- },
- link: params => `/lists/${params.listId}/segments/${params.segmentId}/edit`,
- navs: {
- ':action(edit|delete)': {
- title: t('edit'),
- link: params => `/lists/${params.listId}/segments/${params.segmentId}/edit`,
- panelRender: props =>
- }
- }
- },
- create: {
- title: t('create'),
- resolve: {
- fields: params => `rest/fields/${params.listId}`
- },
- panelRender: props =>
- }
- }
- },
- imports: {
- title: t('imports'),
- link: params => `/lists/${params.listId}/imports/`,
- visible: resolved => resolved.list.permissions.includes('viewImports'),
- panelRender: props => ,
- children: {
- ':importId([0-9]+)': {
- title: resolved => t('importName-1', {name: ellipsizeBreadcrumbLabel(resolved.import.name)}),
- resolve: {
- import: params => `rest/imports/${params.listId}/${params.importId}`,
- },
- link: params => `/lists/${params.listId}/imports/${params.importId}/status`,
- navs: {
- ':action(edit|delete)': {
- title: t('edit'),
- resolve: {
- fieldsGrouped: params => `rest/fields-grouped/${params.listId}`
- },
- link: params => `/lists/${params.listId}/imports/${params.importId}/edit`,
- panelRender: props =>
- },
- 'status': {
- title: t('status'),
- link: params => `/lists/${params.listId}/imports/${params.importId}/status`,
- panelRender: props => ,
- children: {
- ':importRunId([0-9]+)': {
- title: resolved => t('run'),
- resolve: {
- importRun: params => `rest/import-runs/${params.listId}/${params.importId}/${params.importRunId}`,
- },
- link: params => `/lists/${params.listId}/imports/${params.importId}/status/${params.importRunId}`,
- panelRender: props =>
- }
- }
- }
- }
- },
- create: {
- title: t('create'),
- panelRender: props =>
- }
- }
- },
- triggers: {
- title: t('triggers'),
- link: params => `/lists/${params.listId}/triggers`,
- panelRender: props =>
- },
- share: {
- title: t('share'),
- link: params => `/lists/${params.listId}/share`,
- visible: resolved => resolved.list.permissions.includes('share'),
- panelRender: props =>
- }
- }
- },
- create: {
- title: t('create'),
- panelRender: props =>
- },
- forms: {
- title: t('customForms-1'),
- link: '/lists/forms',
- checkPermissions: {
- ...namespaceCheckPermissions('createCustomForm')
- },
- panelRender: props => ,
- children: {
- ':formsId([0-9]+)': {
- title: resolved => t('customFormsName', {name: ellipsizeBreadcrumbLabel(resolved.forms.name)}),
- resolve: {
- forms: params => `rest/forms/${params.formsId}`
- },
- link: params => `/lists/forms/${params.formsId}/edit`,
- navs: {
- ':action(edit|delete)': {
- title: t('edit'),
- link: params => `/lists/forms/${params.formsId}/edit`,
- visible: resolved => resolved.forms.permissions.includes('edit'),
- panelRender: props =>
- },
- share: {
- title: t('share'),
- link: params => `/lists/forms/${params.formsId}/share`,
- visible: resolved => resolved.forms.permissions.includes('share'),
- panelRender: props =>
- }
- }
- },
- create: {
- title: t('create'),
- panelRender: props =>
- }
- }
- }
- }
- }
- };
-}
-
-
-export default {
- getMenus
-}
diff --git a/client/src/lists/segments/CUD.js b/client/src/lists/segments/CUD.js
deleted file mode 100644
index 8a04cc47..00000000
--- a/client/src/lists/segments/CUD.js
+++ /dev/null
@@ -1,404 +0,0 @@
-'use strict';
-
-import React, {Component} from "react";
-import PropTypes from "prop-types";
-import {withTranslation} from '../../lib/i18n';
-import {LinkButton, requiresAuthenticatedUser, Title, Toolbar, withPageHelpers} from "../../lib/page";
-import {
- ButtonRow,
- Dropdown,
- filterData,
- Form,
- FormSendMethod,
- InputField,
- withForm,
- withFormErrorHandlers
-} from "../../lib/form";
-import {withErrorHandling} from "../../lib/error-handling";
-import {DeleteModalDialog} from "../../lib/modals";
-
-import styles from "./CUD.scss";
-import {DragDropContext} from "react-dnd";
-import HTML5Backend from "react-dnd-html5-backend";
-import TouchBackend from "react-dnd-touch-backend";
-import SortableTree from "react-sortable-tree";
-import 'react-sortable-tree/style.css';
-import {ActionLink, Button, Icon} from "../../lib/bootstrap-components";
-import {getRuleHelpers} from "./helpers";
-import RuleSettingsPane from "./RuleSettingsPane";
-import {withComponentMixins} from "../../lib/decorator-helpers";
-import clone from "clone";
-
-// https://stackoverflow.com/a/4819886/1601953
-const isTouchDevice = !!('ontouchstart' in window || navigator.maxTouchPoints);
-
-@DragDropContext(isTouchDevice ? TouchBackend : HTML5Backend)
-@withComponentMixins([
- withTranslation,
- withForm,
- withErrorHandling,
- withPageHelpers,
- requiresAuthenticatedUser
-])
-export default class CUD extends Component {
- // The code below keeps the segment settings in form value. However, it uses it as a mutable datastructure.
- // After initilization, segment settings is never set using setState. This is OK since we update the state.rulesTree
- // from the segment settings on relevant events (changes in the tree and closing the rule settings pane).
-
- constructor(props) {
- super(props);
-
- this.ruleHelpers = getRuleHelpers(props.t, props.fields);
-
- this.state = {
- rulesTree: this.getTreeFromRules([])
- // There is no ruleOptionsVisible here. We have 3 state logic for the visibility:
- // Undef - not shown, True - shown with entry animation, False - hidden with exit animation
- };
-
- this.initForm();
-
- this.onRuleSettingsPaneUpdatedHandler = ::this.onRuleSettingsPaneUpdated;
- this.onRuleSettingsPaneCloseHandler = ::this.onRuleSettingsPaneClose;
- this.onRuleSettingsPaneDeleteHandler = ::this.onRuleSettingsPaneDelete;
- }
-
- static propTypes = {
- action: PropTypes.string.isRequired,
- list: PropTypes.object,
- fields: PropTypes.array,
- entity: PropTypes.object
- }
-
- getRulesFromTree(tree) {
- const rules = [];
-
- for (const node of tree) {
- const rule = node.rule;
-
- if (this.ruleHelpers.isCompositeRuleType(rule.type)) {
- rule.rules = this.getRulesFromTree(node.children);
- }
-
- rules.push(rule);
- }
-
- return rules;
- }
-
- getTreeFromRules(rules) {
- const ruleHelpers = this.ruleHelpers;
-
- const tree = [];
- for (const rule of rules) {
- const ruleTypeSettings = ruleHelpers.getRuleTypeSettings(rule);
- const title = ruleTypeSettings ? ruleTypeSettings.treeLabel(rule) : this.props.t('newRule');
-
- tree.push({
- rule,
- title,
- expanded: true,
- children: this.getTreeFromRules(rule.rules || [])
- });
- }
-
- return tree;
- }
-
- getFormValuesMutator(data, originalData) {
- data.rootRuleType = data.settings.rootRule.type;
- data.selectedRule = (originalData && originalData.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
-
- this.setState({
- rulesTree: this.getTreeFromRules(data.settings.rootRule.rules)
- });
- }
-
- submitFormValuesMutator(data) {
- data.settings.rootRule.type = data.rootRuleType;
-
- // We have to clone the data here otherwise the form change detection doesn't work. This is because we use the state as a mutable structure.
- data = clone(data);
-
- return filterData(data, ['name', 'settings']);
- }
-
- componentDidMount() {
- if (this.props.entity) {
- this.getFormValuesFromEntity(this.props.entity);
-
- } else {
- this.populateFormValues({
- name: '',
- settings: {
- rootRule: {
- type: 'all',
- rules: []
- }
- },
- rootRuleType: 'all',
- selectedRule: null
- });
- }
- }
-
- localValidateFormValues(state) {
- const t = this.props.t;
-
- if (!state.getIn(['name', 'value'])) {
- state.setIn(['name', 'error'], t('nameMustNotBeEmpty'));
- } else {
- state.setIn(['name', 'error'], null);
- }
-
- if (state.getIn(['selectedRule', 'value']) === null) {
- state.setIn(['selectedRule', 'error'], null);
- }
- }
-
- @withFormErrorHandlers
- async submitHandler(submitAndLeave) {
- const t = this.props.t;
-
- let sendMethod, url;
- if (this.props.entity) {
- sendMethod = FormSendMethod.PUT;
- url = `rest/segments/${this.props.list.id}/${this.props.entity.id}`
- } else {
- sendMethod = FormSendMethod.POST;
- url = `rest/segments/${this.props.list.id}`
- }
-
- try {
- this.disableForm();
- this.setFormStatusMessage('info', t('saving'));
-
- const submitResult = await this.validateAndSendFormValuesToURL(sendMethod, url);
-
- if (submitResult) {
- if (this.props.entity) {
- if (submitAndLeave) {
- this.navigateToWithFlashMessage(`/lists/${this.props.list.id}/segments`, 'success', t('segmentUpdated'));
- } else {
- await this.getFormValuesFromURL(`rest/segments/${this.props.list.id}/${this.props.entity.id}`);
-
- this.enableForm();
- this.setFormStatusMessage('success', t('segmentUpdated'));
- }
- } else {
- if (submitAndLeave) {
- this.navigateToWithFlashMessage(`/lists/${this.props.list.id}/segments`, 'success', t('segmentCreated'));
- } else {
- this.navigateToWithFlashMessage(`/lists/${this.props.list.id}/segments/${submitResult}/edit`, 'success', t('segmentCreated'));
- }
- }
- } else {
- this.enableForm();
- this.setFormStatusMessage('warning', t('thereAreErrorsInTheFormPleaseFixThemAnd'));
- }
- } catch (error) {
- throw error;
- }
- }
-
- onRulesChanged(rulesTree) {
- // This assumes that !this.state.ruleOptionsVisible
- this.getFormValue('settings').rootRule.rules = this.getRulesFromTree(rulesTree);
-
- this.setState({
- rulesTree
- })
- }
-
- showRuleOptions(rule) {
- this.updateFormValue('selectedRule', rule);
-
- this.setState({
- ruleOptionsVisible: true
- });
- }
-
- onRuleSettingsPaneClose() {
- this.updateFormValue('selectedRule', null);
-
- this.setState({
- ruleOptionsVisible: false,
- rulesTree: this.getTreeFromRules(this.getFormValue('settings').rootRule.rules)
- });
- }
-
- onRuleSettingsPaneDelete() {
- const selectedRule = this.getFormValue('selectedRule');
- this.updateFormValue('selectedRule', null);
-
- this.setState({
- ruleOptionsVisible: false,
- });
-
- this.deleteRule(selectedRule);
- }
-
- onRuleSettingsPaneUpdated(hasErrors) {
- this.setState(previousState => ({
- formState: previousState.formState.setIn(['data', 'selectedRule', 'error'], hasErrors)
- }));
- }
-
- addRule(rule) {
- if (!this.state.ruleOptionsVisible) {
- const rules = this.getFormValue('settings').rootRule.rules;
- rules.push(rule);
-
- this.updateFormValue('selectedRule', rule);
-
- this.setState({
- ruleOptionsVisible: true,
- rulesTree: this.getTreeFromRules(rules)
- });
- }
- }
-
- async addCompositeRule() {
- this.addRule({
- type: 'all',
- rules: []
- });
- }
-
- async addPrimitiveRule() {
- this.addRule({
- type: null // Null type means a primitive rule where the type has to be chosen based on the chosen column
- });
- }
-
- deleteRule(ruleToDelete) {
- let finishedSearching = false;
-
- function childrenWithoutRule(rules) {
- const newRules = [];
-
- for (const rule of rules) {
- if (finishedSearching) {
- newRules.push(rule);
-
- } else if (rule !== ruleToDelete) {
- const newRule = Object.assign({}, rule);
-
- if (rule.rules) {
- newRule.rules = childrenWithoutRule(rule.rules);
- }
-
- newRules.push(newRule);
-
- } else {
- finishedSearching = true;
- }
- }
-
- return newRules;
- }
-
- const rules = childrenWithoutRule(this.getFormValue('settings').rootRule.rules);
-
- this.getFormValue('settings').rootRule.rules = rules;
-
- this.setState({
- rulesTree: this.getTreeFromRules(rules)
- });
- }
-
-
- render() {
- const t = this.props.t;
- const isEdit = !!this.props.entity;
- const selectedRule = this.getFormValue('selectedRule');
- const ruleHelpers = this.ruleHelpers;
-
- let ruleOptionsVisibilityClass = '';
- if ('ruleOptionsVisible' in this.state) {
- if (this.state.ruleOptionsVisible) {
- ruleOptionsVisibilityClass = ' ' + styles.ruleOptionsVisible;
- } else {
- ruleOptionsVisibilityClass = ' ' + styles.ruleOptionsHidden;
- }
- }
-
- return (
-
-
- {isEdit &&
-
- }
-
- {isEdit ? t('editSegment') : t('createSegment')}
-
-
- {t('segmentOptions')}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
{t('rules')}
-
-
-
-
-
this.onRulesChanged(rulesTree)}
- isVirtualized={false}
- canDrop={ data => !data.nextParent || (ruleHelpers.isCompositeRuleType(data.nextParent.rule.type)) }
- generateNodeProps={data => ({
- buttons: [
- !this.state.ruleOptionsVisible && this.showRuleOptions(data.node.rule)} className={styles.ruleActionLink}> ,
- !this.state.ruleOptionsVisible && this.deleteRule(data.node.rule)} className={styles.ruleActionLink}>
- ]
- })}
- />
-
-
-
-
-
-
-
-
-
- {selectedRule &&
- }
-
-
-
-
-
-
- await this.submitHandler(false)}/>
- await this.submitHandler(true)}/>
-
- {isEdit && }
-
-
- );
- }
-}
\ No newline at end of file
diff --git a/client/src/lists/segments/CUD.scss b/client/src/lists/segments/CUD.scss
deleted file mode 100644
index 442b4ce4..00000000
--- a/client/src/lists/segments/CUD.scss
+++ /dev/null
@@ -1,152 +0,0 @@
-$desktopMinWidth: 768px;
-
-$mobileLeftPaneResidualWidth: 0px;
-$mobileAnimationStartPosition: 100px;
-
-$desktopLeftPaneResidualWidth: 200px;
-$desktopAnimationStartPosition: 300px;
-
-@mixin optionsHidden {
- transform: translateX($mobileAnimationStartPosition);
- @media (min-width: $desktopMinWidth) {
- transform: translateX($desktopAnimationStartPosition);
- }
-}
-
-@mixin optionsVisible {
- transform: translateX($mobileLeftPaneResidualWidth);
- @media (min-width: $desktopMinWidth) {
- transform: translateX($desktopLeftPaneResidualWidth);
- }
-}
-
-.toolbar {
- text-align: right;
-}
-
-.ruleActionLink {
- padding-right: 5px;
-}
-
-.rulePane {
- position: relative;
- width: 100%;
- overflow: hidden;
-
- .leftPane {
- display: inline-block;
- width: 100%;
- margin-right: -100%;
-
- .leftPaneInner {
- .ruleTree {
- background: #fbfbfb;
- border: #cfcfcf 1px solid;
- border-radius: 4px;
- padding: 10px 0px;
- margin-top: 15px;
- margin-bottom: 30px;
-
- // Without this, the placeholders when rearranging the tree are not shown
- position: relative;
- z-index: 0;
- }
- }
-
- .leftPaneOverlay {
- display: none;
- position: absolute;
- left: 0px;
- top: 0px;
- height: 100%;
- z-index: 1;
-
- width: $mobileLeftPaneResidualWidth;
- @media (min-width: $desktopMinWidth) {
- width: $desktopLeftPaneResidualWidth;
- }
- }
-
- .paneDivider {
- display: block;
- position: absolute;
- left: 0px;
- top: 0px;
- width: 100%;
- height: 100%;
- background: url('./divider.png') repeat-y;
-
- @include optionsHidden;
-
- padding-left: 50px;
- z-index: 1;
-
- opacity: 0;
- visibility: hidden;
-
- .paneDividerSolidBackground {
- position: absolute;
- width: 100%;
- height: 100%;
- background: white;
- }
- }
- }
-
- .rightPane {
- display: inline-block;
- width: 100%;
- vertical-align: top;
- z-index: 2;
- position: relative;
-
- @include optionsHidden;
-
- opacity: 0;
- visibility: hidden;
-
- .rightPaneInner {
- margin-right: $mobileLeftPaneResidualWidth;
- @media (min-width: $desktopMinWidth) {
- margin-right: $desktopLeftPaneResidualWidth;
- }
-
- .ruleOptions {
- margin-left: 60px;
- }
- }
- }
-
- &.ruleOptionsVisible {
- .leftPaneOverlay {
- display: block;
- }
-
- .paneDivider {
- transition: transform 300ms ease-out, opacity 100ms ease-out;
- opacity: 1;
- visibility: visible;
-
- @include optionsVisible;
- }
-
- .rightPane {
- transition: transform 300ms ease-out, opacity 100ms ease-out;
- opacity: 1;
- visibility: visible;
-
- @include optionsVisible;
- }
- }
-
- &.ruleOptionsHidden {
- .paneDivider {
- transition: visibility 0s linear 300ms, transform 300ms ease-in, opacity 100ms ease-in 200ms;
- }
-
- .rightPane {
- transition: visibility 0s linear 300ms, transform 300ms ease-in, opacity 100ms ease-in 200ms;
- }
- }
-
-}
diff --git a/client/src/lists/segments/List.js b/client/src/lists/segments/List.js
deleted file mode 100644
index b95c134c..00000000
--- a/client/src/lists/segments/List.js
+++ /dev/null
@@ -1,72 +0,0 @@
-'use strict';
-
-import React, {Component} from 'react';
-import PropTypes from 'prop-types';
-import {withTranslation} from '../../lib/i18n';
-import {LinkButton, requiresAuthenticatedUser, Title, Toolbar, withPageHelpers} from '../../lib/page';
-import {withErrorHandling} from '../../lib/error-handling';
-import {Table} from '../../lib/table';
-import {Icon} from "../../lib/bootstrap-components";
-import {tableAddDeleteButton, tableRestActionDialogInit, tableRestActionDialogRender} from "../../lib/modals";
-import {withComponentMixins} from "../../lib/decorator-helpers";
-
-@withComponentMixins([
- withTranslation,
- withErrorHandling,
- withPageHelpers,
- requiresAuthenticatedUser
-])
-export default class List extends Component {
- constructor(props) {
- super(props);
-
- this.state = {};
- tableRestActionDialogInit(this);
- }
-
- static propTypes = {
- list: PropTypes.object
- }
-
- componentDidMount() {
- }
-
- render() {
- const t = this.props.t;
-
- const columns = [
- { data: 1, title: t('name') },
- {
- actions: data => {
- const actions = [];
-
- if (this.props.list.permissions.includes('manageSegments')) {
- actions.push({
- label: ,
- link: `/lists/${this.props.list.id}/segments/${data[0]}/edit`
- });
-
- tableAddDeleteButton(actions, this, null, `rest/segments/${this.props.list.id}/${data[0]}`, data[1], t('deletingSegment'), t('segmentDeleted'));
- }
-
- return actions;
- }
- }
- ];
-
- return (
-
- {tableRestActionDialogRender(this)}
- {this.props.list.permissions.includes('manageSegments') &&
-
-
-
- }
-
-
{t('segments')}
-
-
this.table = node} withHeader dataUrl={`rest/segments-table/${this.props.list.id}`} columns={columns} />
-
- );
- }
-}
\ No newline at end of file
diff --git a/client/src/lists/segments/RuleSettingsPane.js b/client/src/lists/segments/RuleSettingsPane.js
deleted file mode 100644
index a1b2b754..00000000
--- a/client/src/lists/segments/RuleSettingsPane.js
+++ /dev/null
@@ -1,231 +0,0 @@
-'use strict';
-
-import React, {PureComponent} from "react";
-import PropTypes from "prop-types";
-import {withTranslation} from '../../lib/i18n';
-import {requiresAuthenticatedUser, withPageHelpers} from "../../lib/page";
-import {Button, ButtonRow, Dropdown, Form, TableSelect, withForm} from "../../lib/form";
-import {withErrorHandling} from "../../lib/error-handling";
-import {getRuleHelpers} from "./helpers";
-import {getFieldTypes} from "../fields/helpers";
-
-import styles from "./CUD.scss";
-import {withComponentMixins} from "../../lib/decorator-helpers";
-
-@withComponentMixins([
- withTranslation,
- withForm,
- withErrorHandling,
- withPageHelpers,
- requiresAuthenticatedUser
-])
-export default class RuleSettingsPane extends PureComponent {
- constructor(props) {
- super(props);
-
- const t = props.t;
- this.ruleHelpers = getRuleHelpers(t, props.fields);
- this.fieldTypes = getFieldTypes(t);
-
- this.state = {};
-
- this.initForm({
- leaveConfirmation: false,
- onChangeBeforeValidation: ::this.populateRuleDefaults
- });
- }
-
- static propTypes = {
- rule: PropTypes.object.isRequired,
- fields: PropTypes.array.isRequired,
- onChange: PropTypes.func.isRequired,
- onClose: PropTypes.func.isRequired,
- onDelete: PropTypes.func.isRequired,
- forceShowValidation: PropTypes.bool.isRequired
- }
-
- updateStateFromProps(populateForm) {
- const props = this.props;
- if (populateForm) {
- const rule = props.rule;
- const ruleHelpers = this.ruleHelpers;
-
- let data;
- if (!ruleHelpers.isCompositeRuleType(rule.type)) { // rule.type === null signifies primitive rule where the type has not been determined yet
- data = ruleHelpers.primitiveRuleTypesFormDataDefaults;
-
- const settings = ruleHelpers.getRuleTypeSettings(rule);
- if (settings) {
- Object.assign(data, settings.getFormData(rule));
- }
-
- data.type = rule.type || ''; // On '', we display label "--SELECT--" in the type dropdown. Null would not be accepted by React.
- data.column = rule.column;
-
- } else {
- data = {
- type: rule.type
- };
- }
-
- this.populateFormValues(data);
- }
-
- if (props.forceShowValidation) {
- this.showFormValidation();
- }
- }
-
- componentDidMount() {
- this.updateStateFromProps(true);
- }
-
- componentDidUpdate(prevProps) {
- this.updateStateFromProps(this.props.rule !== prevProps.rule);
-
- if (this.isFormWithoutErrors()) {
- const rule = this.props.rule;
- const ruleHelpers = this.ruleHelpers;
-
- rule.type = this.getFormValue('type');
-
- if (!ruleHelpers.isCompositeRuleType(rule.type)) {
- rule.column = this.getFormValue('column');
-
- const settings = this.ruleHelpers.getRuleTypeSettings(rule);
- settings.assignRuleSettings(rule, key => this.getFormValue(key));
- }
-
- this.props.onChange(false);
- } else {
- this.props.onChange(true);
- }
- }
-
- localValidateFormValues(state) {
- const t = this.props.t;
- const ruleHelpers = this.ruleHelpers;
-
- for (const key of state.keys()) {
- state.setIn([key, 'error'], null);
- }
-
- const ruleType = state.getIn(['type', 'value']);
- if (!ruleHelpers.isCompositeRuleType(ruleType)) {
- if (!ruleType) {
- state.setIn(['type', 'error'], t('typeMustBeSelected'));
- }
-
- const column = state.getIn(['column', 'value']);
- if (column) {
- const colType = ruleHelpers.getColumnType(column);
-
- if (ruleType) {
- const settings = ruleHelpers.primitiveRuleTypes[colType][ruleType];
- settings.validate(state);
- }
- } else {
- state.setIn(['column', 'error'], t('fieldMustBeSelected'));
- }
- }
- }
-
- populateRuleDefaults(mutStateData) {
- const ruleHelpers = this.ruleHelpers;
- const type = mutStateData.getIn(['type','value']);
-
- if (!ruleHelpers.isCompositeRuleType(type)) {
- const column = mutStateData.getIn(['column', 'value']);
-
- if (column) {
- const colType = ruleHelpers.getColumnType(column);
-
- if (type) {
- const settings = ruleHelpers.primitiveRuleTypes[colType][type];
- if (!settings) {
- // The existing rule type does not fit the newly changed column. This resets the rule type chooser to "--- Select ---"
- mutStateData.setIn(['type', 'value'], '');
- }
- }
- }
- }
- }
-
- async closeForm() {
- if (this.isFormWithoutErrors()) {
- this.props.onClose();
- } else {
- this.showFormValidation();
- }
- }
-
- async deleteRule() {
- this.props.onDelete();
- }
-
- render() {
- const t = this.props.t;
- const rule = this.props.rule;
- const ruleHelpers = this.ruleHelpers;
-
- let ruleOptions = null;
- if (ruleHelpers.isCompositeRuleType(rule.type)) {
- ruleOptions =
-
- } else {
- const ruleColumnOptionsColumns = [
- { data: 1, title: t('name') },
- { data: 2, title: t('type') },
- { data: 3, title: t('mergeTag') }
- ];
-
- const ruleColumnOptions = ruleHelpers.fields.map(fld => [ fld.column, fld.name, this.fieldTypes[fld.type].label, fld.key || '' ]);
-
- const ruleColumnSelect = ;
- let ruleTypeSelect = null;
- let ruleSettings = null;
-
- const ruleColumn = this.getFormValue('column');
- if (ruleColumn) {
- const colType = ruleHelpers.getColumnType(ruleColumn);
- if (colType) {
- const ruleTypeOptions = ruleHelpers.getPrimitiveRuleTypeOptions(colType);
- ruleTypeOptions.unshift({ key: '', label: t('select-1')});
-
- if (ruleTypeOptions) {
- ruleTypeSelect =
-
- const ruleType = this.getFormValue('type');
- if (ruleType) {
- ruleSettings = ruleHelpers.primitiveRuleTypes[colType][ruleType].getForm();
- }
- }
- }
- }
-
- ruleOptions =
-
- {ruleColumnSelect}
- {ruleTypeSelect}
- {ruleSettings}
-
;
- }
-
- return (
-
-
{t('ruleOptions')}
-
-
-
- {ruleOptions}
-
-
-
-
-
-
-
-
- );
- }
-}
\ No newline at end of file
diff --git a/client/src/lists/segments/divider.ai b/client/src/lists/segments/divider.ai
deleted file mode 100644
index 22388652..00000000
--- a/client/src/lists/segments/divider.ai
+++ /dev/null
@@ -1,1539 +0,0 @@
-%PDF-1.5
%âãÏÓ
-1 0 obj
<>/OCGs[5 0 R]>>/Pages 3 0 R/Type/Catalog>>
endobj
2 0 obj
<>stream
-
-
-
-
- application/pdf
-
-
- divider
-
-
-
-
- 2017-08-16T17:46:10+02:00
- 2017-08-16T17:46:10+02:00
- 2017-08-16T17:46:10+02:00
- Adobe Illustrator CS6 (Windows)
-
-
-
- 256
- 180
- JPEG
- /9j/4AAQSkZJRgABAgEASABIAAD/7QAsUGhvdG9zaG9wIDMuMAA4QklNA+0AAAAAABAASAAAAAEA
AQBIAAAAAQAB/+4ADkFkb2JlAGTAAAAAAf/bAIQABgQEBAUEBgUFBgkGBQYJCwgGBggLDAoKCwoK
DBAMDAwMDAwQDA4PEA8ODBMTFBQTExwbGxscHx8fHx8fHx8fHwEHBwcNDA0YEBAYGhURFRofHx8f
Hx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8f/8AAEQgAtAEAAwER
AAIRAQMRAf/EAaIAAAAHAQEBAQEAAAAAAAAAAAQFAwIGAQAHCAkKCwEAAgIDAQEBAQEAAAAAAAAA
AQACAwQFBgcICQoLEAACAQMDAgQCBgcDBAIGAnMBAgMRBAAFIRIxQVEGE2EicYEUMpGhBxWxQiPB
UtHhMxZi8CRygvElQzRTkqKyY3PCNUQnk6OzNhdUZHTD0uIIJoMJChgZhJRFRqS0VtNVKBry4/PE
1OT0ZXWFlaW1xdXl9WZ2hpamtsbW5vY3R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo+Ck5SVlpeYmZ
qbnJ2en5KjpKWmp6ipqqusra6voRAAICAQIDBQUEBQYECAMDbQEAAhEDBCESMUEFURNhIgZxgZEy
obHwFMHR4SNCFVJicvEzJDRDghaSUyWiY7LCB3PSNeJEgxdUkwgJChgZJjZFGidkdFU38qOzwygp
0+PzhJSktMTU5PRldYWVpbXF1eX1RlZmdoaWprbG1ub2R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo
+DlJWWl5iZmpucnZ6fkqOkpaanqKmqq6ytrq+v/aAAwDAQACEQMRAD8Anf5uflH6freYvLsP7veT
UNPjH2e7SxKO38y9uoxV4virsVdirsVdirsVeiflZ+adx5YuF0zU2abQZm92a3Zju6Dup/aX6Rv1
VfR1tc291bx3NtIs1vMoeKVCGVlYVBBHUHFVTFXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq
7FXYq7FXYq7FXYq7FXYq8R/Nz8o/T9bzF5dh/d7yahp8Y+z3aWJR2/mXt1GKvF8VdirsVdirsVdi
r0T8rPzTuPLFwumamzTaDM3uzW7Md3Qd1P7S/SN+qr6Otrm3ureO5tpFmt5lDxSoQysrCoII6g4q
qYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXiP5uflH6freYvLs
P7veTUNPjH2e7SxKO38y9uoxV4virsVdirsVdirsVeiflZ+adx5YuF0zU2abQZm92a3Zju6Dup/a
X6Rv1VfR1tc291bx3NtIs1vMoeKVCGVlYVBBHUHFVTFXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq
7FXYq7FXYq7FXYq7FXYq7FXYq8R/Nz8o/T9bzF5dh/d7yahp8Y+z3aWJR2/mXt1GKvF8VdirsVdi
rsVdir0T8rPzTuPLFwumamzTaDM3uzW7Md3Qd1P7S/SN+qr6Otrm3ureO5tpFmt5lDxSoQysrCoI
I6g4qqYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXiP5uflH6fr
eYvLsP7veTUNPjH2e7SxKO38y9uoxV4virsVdirsVdirsVeiflZ+adx5YuF0zU2abQZm92a3Zju6
Dup/aX6Rv1VfR1tc291bx3NtIs1vMoeKVCGVlYVBBHUHFVTFXYq7FXYq7FXYq7FXYq7FXYq7FXYq
7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq8R/Nz8o/T9bzF5dh/d7yahp8Y+z3aWJR2/mXt1GKvF8Vdi
rsVdirsVdir0T8rPzTuPLFwumamzTaDM3uzW7Md3Qd1P7S/SN+qr6Otrm3ureO5tpFmt5lDxSoQy
srCoII6g4qqYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXiP5uf
lH6freYvLsP7veTUNPjH2e7SxKO38y9uoxV4virsVdirsVdirsVeiflZ+adx5YuF0zU2abQZm92a
3Zju6Dup/aX6Rv1VfR1tc291bx3NtIs1vMoeKVCGVlYVBBHUHFVTFXYq7FXYq7FXYq7FXYq7FXYq
7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq8R/Nz8o/T9bzF5dh/d7yahp8Y+z3aWJR2/mXt1GKv
F8VdirsVdirsVdir0T8rPzTuPLFwumamzTaDM3uzW7Md3Qd1P7S/SN+qr6Otrm3ureO5tpFmt5lD
xSoQysrCoII6g4qqYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FX
iP5uflH6freYvLsP7veTUNPjH2e7SxKO38y9uoxV4virsVdirsVdirsVeiflZ+adx5YuF0zU2abQ
Zm92a3Zju6Dup/aX6Rv1VfR1tc291bx3NtIs1vMoeKVCGVlYVBBHUHFVTFXYq7FXYq7FXYq7FXYq
7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq8R/Nz8o/T9bzF5dh/d7yahp8Y+z3aWJR2/mX
t1GKvF8VdirsVdirsVdir0T8rPzTuPLFwumamzTaDM3uzW7Md3Qd1P7S/SN+qr6Otrm3ureO5tpF
mt5lDxSoQysrCoII6g4qqYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FX
Yq7FXiP5uflH6freYvLsP7veTUNPjH2e7SxKO38y9uoxV4virsVdirsVdirsVeiflZ+adx5YuF0z
U2abQZm92a3Zju6Dup/aX6Rv1VfR1tc291bx3NtIs1vMoeKVCGVlYVBBHUHFVTFXYq7FXYq7FXYq
7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXj35t/m39U9by95em/0veO/v4z/ddjFER+
3/M37PQb9FXhWKuxV2KuxV2KuxVnP5ZflleebLwXV0Gt9Dt2pPONmlYf7qi9/E9vnir6WsLCz0+z
hsrKFYLW3UJDCgoqqMVV8VdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirx
782/zb+qet5e8vTf6XvHf38Z/uuxiiI/b/mb9noN+irwrFXYq7FXYq7FXYqzn8svyyvPNl4Lq6DW
+h27UnnGzSsP91Re/ie3zxV9LWFhZ6fZw2VlCtva26hIYUFFVRiqvirsVdirsVdirsVdirsVdirs
VdirsVdirsVdirsVdirsVdirsVdirsVePfm3+bf1T1vL3l6b/S947+/jP912MURH7f8AM37PQb9F
XhWKuxV2KuxV2KuxVnP5ZflleebLwXV0Gt9Dt2pPONmlYf7qi9/E9vnir6WsLCz0+zhsrKFbe1t1
CQwoKKqjFVfFXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq8e/Nv8ANv6p
63l7y9N/pe8d/fxn+67GKIj9v+Zv2eg36KvCsVdirsVdirsVdirOfyy/LK882XguroNb6HbtSecb
NKw/3VF7+J7fPFX0tYWFnp9nDZWUK29rbqEhhQUVVGKq+KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2K
uxV2KuxV2KuxV2KuxV2KuxV49+bf5t/VPW8veXpv9L3jv7+M/wB12MURH7f8zfs9Bv0VeFYq7FXY
q7FXYq7FWc/ll+WV55svBdXQa30O3ak842aVh/uqL38T2+eKvpawsLPT7OGysoVt7W3UJDCgoqqM
VV8VdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirx782/zb+qet5e8vTf6X
vHf38Z/uuxiiI/b/AJm/Z6Dfoq8KxV2KuxV2KuxV2Ks5/LL8srzzZeC6ug1vodu1J5xs0rD/AHVF
7+J7fPFX0tYWFnp9nDZWUK29rbqEhhQUVVGKq+KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2Kux
V2KuxV2KuxV2KuxV49+bf5t/VPW8veXpv9L3jv7+M/3XYxREft/zN+z0G/RV4VirsVdirsVdirsV
Zz+WX5ZXnmy8F1dBrfQ7dqTzjZpWH+6ovfxPb54q+lrCws9Ps4bKyhW3tbdQkMKCiqoxVXxV2Kux
V2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KvHvzb/Nv6p63l7y9N/pe8d/fxn+6
7GKIj9v+Zv2eg36KvCsVdirsVdirsVdirOfyy/LK882XguroNb6HbtSecbNKw/3VF7+J7fPFX0tY
WFnp9nDZWUK29rbqEhhQUVVGKq+KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV
2KuxV49+bf5t/VPW8veXpv8AS947+/jP912MURH7f8zfs9Bv0VeFYq7FXYq7FXYq7FWc/ll+WV55
svBdXQa30O3ak842aVh/uqL38T2+eKvpawsLPT7OGysoVt7W3UJDCgoqqMVV8VdirsVdirsVdirs
VdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirx782/zb+qet5e8vTf6XvHf38Z/uuxiiI/b/mb
9noN+irwrFXYq7FXYq7FXYqzn8svyyvPNl4Lq6DW+h27UnnGzSsP91Re/ie3zxV9LWFhZ6fZw2Vl
Ctva26hIYUFFVRiqvirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVePfm3
+bf1T1vL3l6b/S947+/jP912MURH7f8AM37PQb9FXhWKuxV2KuxV2KuxVnP5ZflleebLwXV0Gt9D
t2pPONmlYf7qi9/E9vnir6WsLCz0+zhsrKFbe1t1CQwoKKqjFVfFXYq7FXYq7FXYq7FXYq7FXYq7
FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FWLn/lWNd/0JXv/vJirv8AkGH/AGpP+nTFXf8AIMP+1J/0
6Yq7/kGH/ak/6dMVd/yDD/tSf9OmKu/5Bh/2pP8Ap0xV3/IMP+1J/wBOmKsg079HfUov0b6P1Gh9
H6tx9KlTXjw+Hr4YqiMVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVd
ir//2Q==
-
-
-
-
-
- uuid:faafa340-73e9-4492-854f-531599fe8c9b
- xmp.did:D09A7EFD9982E711A911B3A23FD23E4E
- uuid:5D20892493BFDB11914A8590D31508C8
- proof:pdf
-
- uuid:1e9fe3cf-bac1-48ac-8341-771a4bda1c27
- xmp.did:0980117407206811822A897E387FE54C
- uuid:5D20892493BFDB11914A8590D31508C8
- proof:pdf
-
-
-
-
- saved
- xmp.iid:D09A7EFD9982E711A911B3A23FD23E4E
- 2017-08-16T17:45:55+02:00
- Adobe Illustrator CS6 (Windows)
- /
-
-
-
-
-
- Document
- Print
-
-
- False
- False
- 1
-
- 60.000000
- 40.000000
- Pixels
-
-
-
- Cyan
- Magenta
- Yellow
- Black
-
-
-
-
-
- Default Swatch Group
- 0
-
-
-
- White
- RGB
- PROCESS
- 255
- 255
- 255
-
-
- Black
- RGB
- PROCESS
- 29
- 29
- 27
-
-
- CMYK Red
- RGB
- PROCESS
- 227
- 6
- 19
-
-
- CMYK Yellow
- RGB
- PROCESS
- 255
- 237
- 0
-
-
- CMYK Green
- RGB
- PROCESS
- 0
- 150
- 64
-
-
- CMYK Cyan
- RGB
- PROCESS
- 0
- 159
- 227
-
-
- CMYK Blue
- RGB
- PROCESS
- 49
- 39
- 131
-
-
- CMYK Magenta
- RGB
- PROCESS
- 230
- 0
- 126
-
-
- C=15 M=100 Y=90 K=10
- RGB
- PROCESS
- 190
- 22
- 34
-
-
- C=0 M=90 Y=85 K=0
- RGB
- PROCESS
- 230
- 51
- 42
-
-
- C=0 M=80 Y=95 K=0
- RGB
- PROCESS
- 233
- 78
- 27
-
-
- C=0 M=50 Y=100 K=0
- RGB
- PROCESS
- 243
- 146
- 0
-
-
- C=0 M=35 Y=85 K=0
- RGB
- PROCESS
- 249
- 178
- 51
-
-
- C=5 M=0 Y=90 K=0
- RGB
- PROCESS
- 252
- 234
- 16
-
-
- C=20 M=0 Y=100 K=0
- RGB
- PROCESS
- 222
- 220
- 0
-
-
- C=50 M=0 Y=100 K=0
- RGB
- PROCESS
- 149
- 193
- 31
-
-
- C=75 M=0 Y=100 K=0
- RGB
- PROCESS
- 58
- 170
- 53
-
-
- C=85 M=10 Y=100 K=10
- RGB
- PROCESS
- 0
- 141
- 54
-
-
- C=90 M=30 Y=95 K=30
- RGB
- PROCESS
- 0
- 102
- 51
-
-
- C=75 M=0 Y=75 K=0
- RGB
- PROCESS
- 47
- 172
- 102
-
-
- C=80 M=10 Y=45 K=0
- RGB
- PROCESS
- 0
- 161
- 154
-
-
- C=70 M=15 Y=0 K=0
- RGB
- PROCESS
- 54
- 169
- 225
-
-
- C=85 M=50 Y=0 K=0
- RGB
- PROCESS
- 29
- 113
- 184
-
-
- C=100 M=95 Y=5 K=0
- RGB
- PROCESS
- 45
- 46
- 131
-
-
- C=100 M=100 Y=25 K=25
- RGB
- PROCESS
- 41
- 35
- 92
-
-
- C=75 M=100 Y=0 K=0
- RGB
- PROCESS
- 102
- 36
- 131
-
-
- C=50 M=100 Y=0 K=0
- RGB
- PROCESS
- 149
- 27
- 129
-
-
- C=35 M=100 Y=35 K=10
- RGB
- PROCESS
- 163
- 25
- 91
-
-
- C=10 M=100 Y=50 K=0
- RGB
- PROCESS
- 214
- 11
- 82
-
-
- C=0 M=95 Y=20 K=0
- RGB
- PROCESS
- 231
- 29
- 115
-
-
- C=25 M=25 Y=40 K=0
- RGB
- PROCESS
- 203
- 187
- 160
-
-
- C=40 M=45 Y=50 K=5
- RGB
- PROCESS
- 164
- 138
- 123
-
-
- C=50 M=50 Y=60 K=25
- RGB
- PROCESS
- 123
- 106
- 88
-
-
- C=55 M=60 Y=65 K=40
- RGB
- PROCESS
- 99
- 78
- 66
-
-
- C=25 M=40 Y=65 K=0
- RGB
- PROCESS
- 202
- 158
- 103
-
-
- C=30 M=50 Y=75 K=10
- RGB
- PROCESS
- 177
- 127
- 74
-
-
- C=35 M=60 Y=80 K=25
- RGB
- PROCESS
- 147
- 96
- 55
-
-
- C=40 M=65 Y=90 K=35
- RGB
- PROCESS
- 125
- 78
- 36
-
-
- C=40 M=70 Y=100 K=50
- RGB
- PROCESS
- 104
- 60
- 17
-
-
- C=50 M=70 Y=80 K=70
- RGB
- PROCESS
- 67
- 41
- 24
-
-
-
-
-
- Grays
- 1
-
-
-
- C=0 M=0 Y=0 K=100
- RGB
- PROCESS
- 29
- 29
- 27
-
-
- C=0 M=0 Y=0 K=90
- RGB
- PROCESS
- 60
- 60
- 59
-
-
- C=0 M=0 Y=0 K=80
- RGB
- PROCESS
- 87
- 87
- 86
-
-
- C=0 M=0 Y=0 K=70
- RGB
- PROCESS
- 112
- 111
- 111
-
-
- C=0 M=0 Y=0 K=60
- RGB
- PROCESS
- 135
- 135
- 135
-
-
- C=0 M=0 Y=0 K=50
- RGB
- PROCESS
- 157
- 157
- 156
-
-
- C=0 M=0 Y=0 K=40
- RGB
- PROCESS
- 178
- 178
- 178
-
-
- C=0 M=0 Y=0 K=30
- RGB
- PROCESS
- 198
- 198
- 198
-
-
- C=0 M=0 Y=0 K=20
- RGB
- PROCESS
- 218
- 218
- 218
-
-
- C=0 M=0 Y=0 K=10
- RGB
- PROCESS
- 237
- 237
- 237
-
-
- C=0 M=0 Y=0 K=5
- RGB
- PROCESS
- 246
- 246
- 246
-
-
-
-
-
- Brights
- 1
-
-
-
- C=0 M=100 Y=100 K=0
- RGB
- PROCESS
- 227
- 6
- 19
-
-
- C=0 M=75 Y=100 K=0
- RGB
- PROCESS
- 234
- 91
- 12
-
-
- C=0 M=10 Y=95 K=0
- RGB
- PROCESS
- 255
- 222
- 0
-
-
- C=85 M=10 Y=100 K=0
- RGB
- PROCESS
- 0
- 152
- 58
-
-
- C=100 M=90 Y=0 K=0
- RGB
- PROCESS
- 39
- 52
- 139
-
-
- C=60 M=90 Y=0 K=0
- RGB
- PROCESS
- 130
- 54
- 140
-
-
-
-
-
-
-
-
- Adobe PDF library 10.01
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-endstream
endobj
3 0 obj
<>
endobj
7 0 obj
<>/Resources<>/ExtGState<>/Properties<>>>/Thumb 12 0 R/TrimBox[0.0 0.0 60.0 40.0]/Type/Page>>
endobj
8 0 obj
<>stream
-H‰ŒOË
-ƒ@¼ç+æÜÍ•æÚµô$¥ô´‡Z(ûÿ`VôXB^30“øG„Ÿ"ã:F/FÊ{ §/ù»AK¦ŸlaDï˜Y êTu@Z©+µŒÆòckÓ•ÞõÇPÚ›fz;纊îåý vI¨‹He+zÞ&{m` Æ@.„
-endstream
endobj
12 0 obj
<>stream
-8;RZ]$bHlM@465bFr3OS+M[u+-l&f~>
-endstream
endobj
13 0 obj
[/Indexed/DeviceRGB 255 14 0 R]
endobj
14 0 obj
<>stream
-8;X]O>EqN@%''O_@%e@?J;%+8(9e>X=MR6S?i^YgA3=].HDXF.R$lIL@"pJ+EP(%0
-b]6ajmNZn*!='OQZeQ^Y*,=]?C.B+\Ulg9dhD*"iC[;*=3`oP1[!S^)?1)IZ4dup`
-E1r!/,*0[*9.aFIR2&b-C#soRZ7Dl%MLY\.?d>Mn
-6%Q2oYfNRF$$+ON<+]RUJmC0InDZ4OTs0S!saG>GGKUlQ*Q?45:CI&4J'_2j $XKrcYp0n+Xl_nU*O(
-l[$6Nn+Z_Nq0]s7hs]`XX1nZ8&94a\~>
-endstream
endobj
5 0 obj
<>
endobj
15 0 obj
[/View/Design]
endobj
16 0 obj
<>>>
endobj
11 0 obj
<>
endobj
10 0 obj
[/ICCBased 17 0 R]
endobj
17 0 obj
<>stream
-H‰œ–yTSwÇoÉž•°Ãc
[€°5la‘QIBHØADED„ª•2ÖmtFOE.®cÖ}êÒõ0êè8´×Ž8GNg¦Óïï÷9÷wïïÝß½÷ó '¥ªµÕ0 Ö ÏJŒÅb¤
- 2y.-;!à’ÆK°ZÜ ü‹ž^i½"LÊÀ0ðÿ‰-×é
@8(”µrœ;q®ª7èLöœy¥•&†Qëñq¶4±jž½ç|æ9ÚÄ
-V³)gB£0ñiœWו8#©8wÕ©•õ8_Å٥ʨQãüÜ«QÊj@é&»A)/ÇÙgº>'K‚ó ÈtÕ;\ú”
Ó¥$ÕºF½ZUnÀÜå˜(4TŒ%)ë«”ƒ0C&¯”阤Z£“i˜¿óœ8¦Úbx‘ƒE¡ÁÁBÑ;…ú¯›¿P¦ÞÎӓ̹žAüom?çW=
-€x¯Íú·¶Ò- Œ¯Àòæ[›Ëû 0ñ¾¾øÎ}ø¦y)7ta¾¾õõõ>j¥ÜÇTÐ7úŸ¿@ï¼ÏÇtÜ›ò`qÊ2™±Ê€™ê&¯®ª6ê±ZL®Ä„?â_øóyxg)Ë”z¥ÈçLUáíÖ*ÔuµSkÿSeØO4?׸¸c¯¯Ø°.ò ò· åÒ R´
ßÞô-•’2ð5ßáÞüÜÏ ú÷Sá>Ó£Vš‹“då`r£¾n~ÏôY &à+`œ;ÂA4ˆÉ 䀰ÈA9Ð =¨- t°lÃ`;»Á~pŒƒÁ ðGp| ®[`Lƒ‡`<¯ "AˆYA+äùCb(ЇR¡,¨ *T2B-Ð
-¨ê‡†¡Ðnè÷ÐQètº}MA ï —0Óal»Á¾°ŽSàx ¬‚kà&¸^Á£ð>ø0|>_ƒ'á‡ð,ÂG!"F$H:Rˆ”!z¤éF‘Qd?r9‹\A&‘GÈ”ˆrQ¢áhš‹ÊÑ´íE‡Ñ]èaô4zBgÐ×Á–àE#H ‹*B=¡‹0HØIøˆp†p0MxJ$ùD1„˜D, V›‰½ÄÄÄãÄKÄ»ÄY‰dEò"EÒI2’ÔEÚBÚGúŒt™4MzN¦‘Èþär!YKî ’÷?%_&ß#¿¢°(®”0J:EAi¤ôQÆ(Ç()Ó”WT6U@ æP+¨íÔ!ê~êêmêæD¥eÒÔ´å´!ÚïhŸÓ¦h/èº']B/¢éëèÒÓ¿¢?a0nŒhF!ÃÀXÇØÍ8ÅøšñÜŒkæc&5S˜µ™˜6»lö˜Iaº2c˜K™MÌAæ!æEæ#…寒°d¬VÖë(ëk–Íe‹Øél
»—½‡}Ž}ŸCâ¸qâ9
-N'çÎ)Î].ÂuæJ¸rî
-î÷wšGä xR^¯‡÷[ÞoÆœchžgÞ`>bþ‰ù$á»ñ¥ü*~ÿ ÿ:ÿ¥…EŒ…ÒbÅ~‹ËÏ,m,£-•–Ý–,¯Y¾´Â¬â*6X[ݱF=3ë·YŸ±~dó ·‘ÛtÛ´¹iÛzÚfÙ6Û~`{ÁvÖÎÞ.ÑNg·Åî”Ý#{¾}´}…ý€ý§ö¸‘j‡‡ÏþŠ™c1X6„Æfm“Ž;'_9 œr:œ8Ýq¦:‹ËœœO:ϸ8¸¤¹´¸ìu¹éJq»–»nv=ëúÌMà–ï¶ÊmÜí¾ÀR 4 ö
-n»3Ü£ÜkÜGݯz=Ä•[=¾ô„=ƒ<Ë=GTB(É/ÙSòƒ,]6*›-•–¾W:#—È7Ë*¢ŠÊe¿ò^YDYÙ}U„j£êAyTù`ù#µD=¬þ¶"©b{ųÊôÊ+¬Ê¯: !kJ4Gµm¥ötµ}uCõ%—®K7YV³©fFŸ¢ßYÕ.©=bàá?SŒîƕƩºÈº‘ºçõyõ‡Ø
Ú†žkï5%4ý¦m–7Ÿlqlio™Z³lG+ÔZÚz²Í¹³mzyâò]íÔöÊö?uøuôw|¿"űN»ÎåwW&®ÜÛe֥ﺱ*|ÕöÕèjõê‰5k¶¬yÝèþ¢Ç¯g°ç‡^yïkEk‡Öþ¸®lÝD_pß¶õÄõÚõ×7DmØÕÏîoê¿»1mãál {àûMÅ›Î
nßLÝlÜ<9”úO ¤[þ˜¸™$™™üšhšÕ›B›¯œœ‰œ÷dÒž@ž®ŸŸ‹Ÿú i Ø¡G¡¶¢&¢–££v£æ¤V¤Ç¥8¥©¦¦‹¦ý§n§à¨R¨Ä©7©©ªª««u«é¬\¬ÐD¸®-®¡¯¯‹° °u°ê±`±Ö²K²Â³8³®´%´œµµŠ¶¶y¶ð·h·à¸Y¸Ñ¹J¹Âº;ºµ».»§¼!¼›½½¾
-¾„¾ÿ¿z¿õÀpÀìÁgÁãÂ_ÂÛÃXÃÔÄQÄÎÅKÅÈÆFÆÃÇAÇ¿È=ȼÉ:ɹÊ8Ê·Ë6˶Ì5̵Í5͵Î6ζÏ7ϸÐ9кÑ<ѾÒ?ÒÁÓDÓÆÔIÔËÕNÕÑÖUÖØ×\×àØdØèÙlÙñÚvÚûÛ€ÜÜŠÝÝ–ÞÞ¢ß)߯à6à½áDáÌâSâÛãcãëäsäü儿
æ–çç©è2è¼éFéÐê[êåëpëûì†ííœî(î´ï@ïÌðXðåñrñÿòŒóó§ô4ôÂõPõÞömöû÷Šøø¨ù8ùÇúWúçûwüü˜ý)ýºþKþÜÿmÿÿ ÷„óû
-endstream
endobj
9 0 obj
<>
endobj
18 0 obj
<>
endobj
19 0 obj
<>stream
-%!PS-Adobe-3.0
-%%Creator: Adobe Illustrator(R) 15.0
-%%AI8_CreatorVersion: 16.0.3
-%%For: (Tomas Bures) ()
-%%Title: (Untitled-1)
-%%CreationDate: 8/16/2017 5:46 PM
-%%Canvassize: 16383
-%%BoundingBox: 2 -40 60 0
-%%HiResBoundingBox: 2 -40 60 0
-%%DocumentProcessColors: Cyan Magenta Yellow Black
-%AI5_FileFormat 11.0
-%AI12_BuildNumber: 691
-%AI3_ColorUsage: Color
-%AI7_ImageSettings: 0
-%%RGBProcessColor: 0 0 0 ([Registration])
-%AI3_Cropmarks: 0 -40 60 0
-%AI3_TemplateBox: 20.5 -20.5 20.5 -20.5
-%AI3_TileBox: -376.7998 -305.6602 436.6797 265.54
-%AI3_DocumentPreview: None
-%AI5_ArtSize: 14400 14400
-%AI5_RulerUnits: 6
-%AI9_ColorModel: 1
-%AI5_ArtFlags: 0 0 0 1 0 0 1 0 0
-%AI5_TargetResolution: 800
-%AI5_NumLayers: 1
-%AI9_OpenToView: -32 18 16 1793 1016 90 1 0 -2036 276 0 0 0 1 1 0 1 1 0
-%AI5_OpenViewLayers: 7
-%%PageOrigin:-286 -416
-%AI7_GridSettings: 72 8 72 8 1 0 0.8 0.8 0.8 0.9 0.9 0.9
-%AI9_Flatten: 1
-%AI12_CMSettings: 00.MS
-%%EndComments
-
-endstream
endobj
20 0 obj
<>stream
-%%BoundingBox: 2 -40 60 0
-%%HiResBoundingBox: 2 -40 60 0
-%AI7_Thumbnail: 128 88 8
-%%BeginData: 5407 Hex Bytes
-%0000330000660000990000CC0033000033330033660033990033CC0033FF
-%0066000066330066660066990066CC0066FF009900009933009966009999
-%0099CC0099FF00CC0000CC3300CC6600CC9900CCCC00CCFF00FF3300FF66
-%00FF9900FFCC3300003300333300663300993300CC3300FF333300333333
-%3333663333993333CC3333FF3366003366333366663366993366CC3366FF
-%3399003399333399663399993399CC3399FF33CC0033CC3333CC6633CC99
-%33CCCC33CCFF33FF0033FF3333FF6633FF9933FFCC33FFFF660000660033
-%6600666600996600CC6600FF6633006633336633666633996633CC6633FF
-%6666006666336666666666996666CC6666FF669900669933669966669999
-%6699CC6699FF66CC0066CC3366CC6666CC9966CCCC66CCFF66FF0066FF33
-%66FF6666FF9966FFCC66FFFF9900009900339900669900999900CC9900FF
-%9933009933339933669933999933CC9933FF996600996633996666996699
-%9966CC9966FF9999009999339999669999999999CC9999FF99CC0099CC33
-%99CC6699CC9999CCCC99CCFF99FF0099FF3399FF6699FF9999FFCC99FFFF
-%CC0000CC0033CC0066CC0099CC00CCCC00FFCC3300CC3333CC3366CC3399
-%CC33CCCC33FFCC6600CC6633CC6666CC6699CC66CCCC66FFCC9900CC9933
-%CC9966CC9999CC99CCCC99FFCCCC00CCCC33CCCC66CCCC99CCCCCCCCCCFF
-%CCFF00CCFF33CCFF66CCFF99CCFFCCCCFFFFFF0033FF0066FF0099FF00CC
-%FF3300FF3333FF3366FF3399FF33CCFF33FFFF6600FF6633FF6666FF6699
-%FF66CCFF66FFFF9900FF9933FF9966FF9999FF99CCFF99FFFFCC00FFCC33
-%FFCC66FFCC99FFCCCCFFCCFFFFFF33FFFF66FFFF99FFFFCC110000001100
-%000011111111220000002200000022222222440000004400000044444444
-%550000005500000055555555770000007700000077777777880000008800
-%000088888888AA000000AA000000AAAAAAAABB000000BB000000BBBBBBBB
-%DD000000DD000000DDDDDDDDEE000000EE000000EEEEEEEE0000000000FF
-%00FF0000FFFFFF0000FF00FFFFFF00FFFFFF
-%524C45A8FD1C52A8FD63FFA8275227525252275252522752525227525252
-%275252522752525227527DFD63FFA827FD19522752A8FD63FFA827522752
-%2752275227522752275227522752275227522752275227527DFD63FFA827
-%FD1B527DFD63FFA827525252275252522752525227525252275252522752
-%5252275227527DFD63FFA827FD1B527DFD63FFA827522752275227522752
-%2752275227522752275227522752275227527DFD63FFA8FD1C527DFD63FF
-%A8275227525252275252522752525227525252275252522752525227527D
-%FD63FFA8FD1C527DFD63FFA8275227522752275227522752275227522752
-%275227522752275227527DFD63FFA8FD1C527DFD63FFA827525252275252
-%5227525252275252522752525227525252275252527DFD63FFA8FD1C527D
-%FD63FFA82752275227522752275227522752275227522752275227522752
-%275252FD64FFFD1C527DFD64FF5252275252522752525227525252275252
-%52275252522752525227527DFD64FFFD1C527DFD64FF5252275227522752
-%275227522752275227522752275227522752275252FD64FFFD1C527DFD64
-%FFFD0452275252522752525227525252275252522752525227FD0452FD64
-%FFFD1D52FD64FF5252275227522752275227522752275227522752275227
-%522752275252FD64FFFD1D52FD64FF525227525252275252522752525227
-%5252522752525227525252275252FD64FFFD1D52FD64FF52522752275227
-%52275227522752275227522752275227522752275252FD64FFFD1D52FD64
-%FFFD0452275252522752525227525252275252522752525227FD0452FD64
-%FFFD1D52FD64FF5252275227522752275227522752275227522752275227
-%522752275252FD64FF7DFD1C52FD64FF5252275252522752525227525252
-%275252522752525227525252275252FD64FF7DFD1C52FD64FF5252275227
-%522752275227522752275227522752275227522752275252A8FD63FF7DFD
-%1C52FD64FF7D525252275252522752525227525252275252522752525227
-%52525227A8FD63FF7DFD1C52FD64FF7D5227522752275227522752275227
-%5227522752275227522752275227A8FD63FF7DFD1C52A8FD63FF7D522752
-%52522752525227525252275252522752525227525252275227A8FD63FF7D
-%FD1C52A8FD62FFA827522752275227522752275227522752275227522752
-%2752275227527DFD61FFA8FD1C527DFD61FFA82752525227525252275252
-%52275252522752525227525252275227527DFD61FFA8FD1C527DFD61FFA8
-%275227522752275227522752275227522752275227522752275227527DFD
-%61FFA827FD1B527DFD61FFA8275227525252275252522752525227525252
-%275252522752525227527DFD61FFA827FD1B527DFD61FFA8275227522752
-%275227522752275227522752275227522752275227527DFD61FFA8FD1C52
-%A8FD61FF7D27525252275252522752525227525252275252522752525227
-%5227527DFD61FFA827FD19522752A8FD61FF7D2752275227522752275227
-%5227522752275227522752275227522752A8FD61FF7D27FD1B52A8FD61FF
-%7D27522752525227525252275252522752525227525252275252522752A8
-%FD61FF7D27FD1B52A8FD61FF7D2752275227522752275227522752275227
-%5227522752275227522752A8FD61FF7D27FD1B52A8FD61FF7D2752525227
-%5252522752525227525252275252522752525227522752A8FD61FF7D27FD
-%19522752A8FD61FF7D275227522752275227522752275227522752275227
-%52275227522752A8FD61FF7D27FD1B52A8FD61FF7D275227525252275252
-%52275252522752525227525252275252522752A8FD61FF7D27FD1B52A8FD
-%60FFA87D2752275227522752275227522752275227522752275227522752
-%2752A8FD61FF7DFD1A52277DFD62FF522752525227525252275252522752
-%5252275252522752525227522752A8FD61FF7D27FD1952277DFD61FFA852
-%27522752275227522752275227522752275227522752275227522752A8FD
-%61FF7D27FD1A527DFD61FFA8522752275252522752525227525252275252
-%522752525227525252277DFD62FF5227FD1952277DFD61FFA85227522752
-%27522752275227522752275227522752275227522752277DFD61FFA87DFD
-%1A52277DFD61FFA852275252522752525227525252275252522752525227
-%5252522752277DFD61FFA8FD1B52277DFD61FFA852275227522752275227
-%5227522752275227522752275227522752277DFD61FFA85227FD1952277D
-%FD61FFA85227522752525227525252275252522752525227525252275252
-%52277DFD61FFA85227FD1952277DFD61FFA8522752275227522752275227
-%522752275227522752275227522752277DFD61FFA8FD1B5227A8FD61FF7D
-%522752275227522752275227522752275227522752275227522752277DFD
-%62FFA87DA87DA8A8A87DA8A8A87DA8A8A87DA8A8A87DA8A8A87DA8A8A87D
-%A8FDE2FFFF
-%%EndData
-
-endstream
endobj
21 0 obj
<>stream
-%AI12_CompressedDataxœì½éŽ%Ç•&øŸ