From 8d58c3d282b3c8edaec680241399599058aa74c9 Mon Sep 17 00:00:00 2001 From: root Date: Wed, 14 Aug 2019 13:02:40 +0200 Subject: [PATCH] Namespace Filter --- client/src/campaigns/CUD.js | 29 +++- client/src/campaigns/List.js | 8 +- client/src/lib/namespace.js | 31 +++- client/src/lists/CUD.js | 12 +- client/src/lists/List.js | 9 +- client/src/lists/forms/CUD.js | 17 ++- client/src/lists/forms/List.js | 9 +- client/src/namespaces/Filter.js | 174 ++++++++++++++++++++++ client/src/namespaces/List.js | 9 +- client/src/namespaces/root.js | 130 +++++++++++----- client/src/reports/CUD.js | 2 + client/src/reports/List.js | 9 +- client/src/reports/templates/List.js | 10 +- client/src/root.js | 18 ++- client/src/send-configurations/List.js | 10 +- client/src/templates/CUD.js | 14 +- client/src/templates/List.js | 10 +- client/src/templates/helpers.js | 15 +- client/src/templates/mosaico/List.js | 10 +- client/src/users/List.js | 10 +- locales/en-US-last-run/common.json | 6 +- locales/en-US/common.json | 6 +- locales/es-ES/common.json | 6 +- server/config/default.yaml | 3 + server/lib/client-helpers.js | 4 +- server/models/campaigns.js | 42 ++++-- server/models/forms.js | 23 ++- server/models/lists.js | 23 +-- server/models/mosaico-templates.js | 41 ++++- server/models/namespaces.js | 59 +++++++- server/models/report-templates.js | 19 ++- server/models/reports.js | 22 ++- server/models/send-configurations.js | 20 ++- server/models/templates.js | 22 +-- server/models/users.js | 33 +++- server/routes/rest/campaigns.js | 13 +- server/routes/rest/forms.js | 6 +- server/routes/rest/lists.js | 6 +- server/routes/rest/mosaico-templates.js | 12 +- server/routes/rest/namespaces.js | 9 +- server/routes/rest/report-templates.js | 6 +- server/routes/rest/reports.js | 6 +- server/routes/rest/send-configurations.js | 6 +- server/routes/rest/templates.js | 6 +- server/routes/rest/users.js | 6 +- 45 files changed, 783 insertions(+), 158 deletions(-) create mode 100644 client/src/namespaces/Filter.js diff --git a/client/src/campaigns/CUD.js b/client/src/campaigns/CUD.js index 945965a9..04bf43b6 100644 --- a/client/src/campaigns/CUD.js +++ b/client/src/campaigns/CUD.js @@ -22,7 +22,7 @@ import { withFormErrorHandlers } from '../lib/form'; import {withAsyncErrorHandler, withErrorHandling} from '../lib/error-handling'; -import {getDefaultNamespace, NamespaceSelect, validateNamespace} from '../lib/namespace'; +import {getDefaultNamespace, NamespaceSelect, validateNamespace, getNamespaceIdFilterCookie} from '../lib/namespace'; import {DeleteModalDialog} from "../lib/modals"; import mailtrainConfig from 'mailtrainConfig'; import {getTagLanguages, getTemplateTypes, getTypeForm, ResourceType} from '../templates/helpers'; @@ -529,7 +529,7 @@ export default class CUD extends Component { const canDelete = isEdit && this.props.entity.permissions.includes('delete'); let extraSettings = null; - + const sourceTypeKey = Number.parseInt(this.getFormValue('source')); const campaignTypeKey = this.getFormValue('type'); @@ -557,6 +557,10 @@ export default class CUD extends Component { const lstOrderIdxClosure = lstOrderIdx; const selectedList = this.getFormValue(prefix + 'list'); + var listsTable = ; + if(mailtrainConfig.namespaceFilterEnabled && getNamespaceIdFilterCookie()){ + listsTable = ; + } lstsEditEntries.push(
@@ -593,7 +597,8 @@ export default class CUD extends Component { }
- + + {listsTable} {(campaignTypeKey === CampaignType.REGULAR || campaignTypeKey === CampaignType.RSS) &&
@@ -697,6 +702,9 @@ export default class CUD extends Component { // The "key" property here and in the TableSelect below is to tell React that these tables are different and should be rendered by different instances. Otherwise, React will use // only one instance, which fails because Table does not handle updates in "columns" property templateEdit = ; + if(mailtrainConfig.namespaceFilterEnabled && getNamespaceIdFilterCookie()){ + templateEdit = ; + } } else if (!isEdit && sourceTypeKey === CampaignSource.CUSTOM_FROM_CAMPAIGN) { const campaignsColumns = [ @@ -707,9 +715,10 @@ export default class CUD extends Component { { data: 5, title: t('created'), render: data => moment(data).fromNow() }, { data: 6, title: t('namespace') } ]; - templateEdit = ; - + if(mailtrainConfig.namespaceFilterEnabled && getNamespaceIdFilterCookie()){ + templateEdit = ; + } } else if (!isEdit && sourceTypeKey === CampaignSource.CUSTOM) { const customTemplateTypeKey = this.getFormValue('data_sourceCustom_type'); @@ -730,6 +739,12 @@ export default class CUD extends Component { templateEdit = } + var sendConfigTable = ; + + if(mailtrainConfig.namespaceFilterEnabled && getNamespaceIdFilterCookie()){ + sendConfigTable = + } + return (
{canDelete && @@ -773,8 +788,8 @@ export default class CUD extends Component {
- - + + {sendConfigTable} {sendSettings} diff --git a/client/src/campaigns/List.js b/client/src/campaigns/List.js index 574a9016..d9f46aed 100644 --- a/client/src/campaigns/List.js +++ b/client/src/campaigns/List.js @@ -11,8 +11,10 @@ import {CampaignSource, CampaignStatus, CampaignType} from "../../../shared/camp import {getCampaignLabels} from "./helpers"; import {tableAddDeleteButton, tableRestActionDialogInit, tableRestActionDialogRender} from "../lib/modals"; import {withComponentMixins} from "../lib/decorator-helpers"; +import {getNamespaceIdFilterCookie} from "../lib/namespace"; import styles from "./styles.scss"; import PropTypes from 'prop-types'; +import mailtrainConfig from 'mailtrainConfig'; @withComponentMixins([ withTranslation, @@ -137,6 +139,10 @@ export default class List extends Component { } ]; + var campaingsTable = this.table = node} withHeader dataUrl={"rest/campaigns-table"} columns={columns} />; + if(mailtrainConfig.namespaceFilterEnabled && getNamespaceIdFilterCookie()){ + campaingsTable =
this.table = node} withHeader dataUrl={"rest/campaigns-table/" + getNamespaceIdFilterCookie()} columns={columns} />; + } return (
{tableRestActionDialogRender(this)} @@ -152,7 +158,7 @@ export default class List extends Component { {t('campaigns')} -
this.table = node} withHeader dataUrl="rest/campaigns-table" columns={columns} /> + {campaingsTable} ); } diff --git a/client/src/lib/namespace.js b/client/src/lib/namespace.js index 55b4a569..d9cb5534 100644 --- a/client/src/lib/namespace.js +++ b/client/src/lib/namespace.js @@ -4,6 +4,7 @@ import React, {Component} from 'react'; import {withTranslation} from './i18n'; import {TreeTableSelect} from './form'; import {withComponentMixins} from "./decorator-helpers"; +import mailtrainConfig from 'mailtrainConfig'; @withComponentMixins([ @@ -12,7 +13,11 @@ import {withComponentMixins} from "./decorator-helpers"; export class NamespaceSelect extends Component { render() { const t = this.props.t; - + if(mailtrainConfig.namespaceFilterEnabled && getNamespaceIdFilterCookie()){ + return ( + + ); + } return ( ); @@ -27,10 +32,34 @@ export function validateNamespace(t, state) { } } +function getCookie(cname) { + var name = cname + "="; + var decodedCookie = decodeURIComponent(document.cookie); + var ca = decodedCookie.split(';'); + for(var i = 0; i + + if(mailtrainConfig.namespaceFilterEnabled && getNamespaceIdFilterCookie()){ + sendConfigTable = + } + return (
{canDelete && @@ -268,8 +275,9 @@ export default class CUD extends Component { {toNameFields} - + {sendConfigTable} + diff --git a/client/src/lists/List.js b/client/src/lists/List.js index f7751ebd..ecaa7b39 100644 --- a/client/src/lists/List.js +++ b/client/src/lists/List.js @@ -9,7 +9,9 @@ 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 {getNamespaceIdFilterCookie} from "../lib/namespace"; import PropTypes from 'prop-types'; +import mailtrainConfig from 'mailtrainConfig'; @withComponentMixins([ withTranslation, @@ -116,6 +118,11 @@ export default class List extends Component { } ]; + var listsTable =
this.table = node} withHeader dataUrl="rest/lists-table" columns={columns} />; + if(mailtrainConfig.namespaceFilterEnabled && getNamespaceIdFilterCookie()){ + listsTable =
this.table = node} withHeader dataUrl={"rest/lists-table/" + getNamespaceIdFilterCookie()} columns={columns} />; + } + return (
{tableRestActionDialogRender(this)} @@ -130,7 +137,7 @@ export default class List extends Component { {t('lists')} -
this.table = node} withHeader dataUrl="rest/lists-table" columns={columns} /> + {listsTable} ); } diff --git a/client/src/lists/forms/CUD.js b/client/src/lists/forms/CUD.js index 6604ecba..4c4e99ab 100644 --- a/client/src/lists/forms/CUD.js +++ b/client/src/lists/forms/CUD.js @@ -23,7 +23,7 @@ import { withFormErrorHandlers } from '../../lib/form'; import {withErrorHandling} from '../../lib/error-handling'; -import {getDefaultNamespace, NamespaceSelect, validateNamespace} from '../../lib/namespace'; +import {getDefaultNamespace, NamespaceSelect, validateNamespace, getNamespaceIdFilterCookie} from '../../lib/namespace'; import {DeleteModalDialog} from "../../lib/modals"; import mailtrainConfig from 'mailtrainConfig'; import {getTrustedUrl, getUrl} from "../../lib/urls"; @@ -486,6 +486,13 @@ export default class CUD extends Component { const previewListId = this.getFormValue('previewList'); const selectedTemplate = this.getFormValue('selectedTemplate'); + var customFormsTable = ; + var listsTable = ; + if(mailtrainConfig.namespaceFilterEnabled && getNamespaceIdFilterCookie()){ + customFormsTable = ; + listsTable = ; + } + return (
{canDelete && @@ -512,12 +519,12 @@ export default class CUD extends Component { } - {this.getFormValue('fromExistingEntity') ? - - : + {this.getFormValue('fromExistingEntity') && customFormsTable} + + {!this.getFormValue('fromExistingEntity') && <>
- + {listsTable} { previewListId &&
diff --git a/client/src/lists/forms/List.js b/client/src/lists/forms/List.js index 5b128ae8..fe1ceee9 100644 --- a/client/src/lists/forms/List.js +++ b/client/src/lists/forms/List.js @@ -9,6 +9,8 @@ import {Icon} from "../../lib/bootstrap-components"; import {tableAddDeleteButton, tableRestActionDialogInit, tableRestActionDialogRender} from "../../lib/modals"; import {withComponentMixins} from "../../lib/decorator-helpers"; import PropTypes from 'prop-types'; +import {getNamespaceIdFilterCookie} from '../../lib/namespace'; +import mailtrainConfig from 'mailtrainConfig'; @withComponentMixins([ withTranslation, @@ -62,7 +64,10 @@ export default class List extends Component { } } ]; - + var customFormsTable =
this.table = node} withHeader dataUrl="rest/forms-table" columns={columns} />; + if(mailtrainConfig.namespaceFilterEnabled && getNamespaceIdFilterCookie()){ + customFormsTable =
this.table = node} withHeader dataUrl={"rest/forms-table/" + getNamespaceIdFilterCookie()} columns={columns} />; + } return (
{tableRestActionDialogRender(this)} @@ -74,7 +79,7 @@ export default class List extends Component { {t('forms')} -
this.table = node} withHeader dataUrl="rest/forms-table" columns={columns} /> + {customFormsTable} ); } diff --git a/client/src/namespaces/Filter.js b/client/src/namespaces/Filter.js new file mode 100644 index 00000000..91dd3b4b --- /dev/null +++ b/client/src/namespaces/Filter.js @@ -0,0 +1,174 @@ +'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 { + Button, + ButtonRow, + filterData, + Form, + FormSendMethod, + TreeTableSelect, + withForm, + withFormErrorHandlers +} from '../lib/form'; +import axios from '../lib/axios'; +import {withAsyncErrorHandler, withErrorHandling} from '../lib/error-handling'; +import {getGlobalNamespaceId} from "../../../shared/namespaces"; +import {getUrl} from "../lib/urls"; +import {withComponentMixins} from "../lib/decorator-helpers"; +import {getDefaultNamespace} from "../lib/namespace"; +import mailtrainConfig from 'mailtrainConfig'; + +@withComponentMixins([ + withTranslation, + withForm, + withErrorHandling, + withPageHelpers, + requiresAuthenticatedUser +]) +export default class Filter extends Component { + constructor(props) { + super(props); + + this.state = {}; + + this.initForm(); + } + + static propTypes = { + action: PropTypes.string.isRequired, + entity: PropTypes.object, + permissions: PropTypes.object + } + + submitFormValuesMutator(data) { + return filterData(data, ['name', 'description', 'namespace']); + } + + isEditGlobal() { + return this.props.entity && this.props.entity.id === getGlobalNamespaceId(); + } + + removeNsIdSubtree(data) { + for (let idx = 0; idx < data.length; idx++) { + const entry = data[idx]; + + if (entry.key === this.props.entity.id) { + data.splice(idx, 1); + return true; + } + + if (this.removeNsIdSubtree(entry.children)) { + return true; + } + } + } + + @withAsyncErrorHandler + async loadTreeData() { + if (!this.isEditGlobal()) { + const response = await axios.get(getUrl('rest/namespaces-tree')); + const data = response.data; + for (const root of data) { + root.expanded = true; + } + + if (this.props.entity && !this.isEditGlobal()) { + this.removeNsIdSubtree(data); + } + + if (this.isComponentMounted()) { + this.setState({ + treeData: data + }); + } + } + } + + componentDidMount() { + if (this.props.entity) { + this.getFormValuesFromEntity(this.props.entity); + } else { + this.populateFormValues({ + name: '', + description: '', + namespace: getDefaultNamespace(this.props.permissions) + }); + } + + // noinspection JSIgnoredPromiseFromCall + this.loadTreeData(); + } + + localValidateFormValues(state) { + const t = this.props.t; + + if (!state.getIn(['namespace', 'value'])) { + state.setIn(['namespace', 'error'], t('namespaceMustBeSelected')); + } else { + state.setIn(['namespace', 'error'], null); + } + } + + @withFormErrorHandlers + async submitHandler(leave, disallow) { + + if(disallow){ + document.cookie = 'namespaceFilterId' + '=; expires=Thu, 01 Jan 1970 00:00:01 GMT;';//Delete namespaceFilter cookies + document.cookie = 'namespaceFilterName' + '=; expires=Thu, 01 Jan 1970 00:00:01 GMT;'; + location.reload(); + }else{ + const t = this.props.t; + + let sendMethod, url; + sendMethod = FormSendMethod.GET; + + if(this.getFormValue('namespace')){ + url = 'rest/namespaces/' + this.getFormValue('namespace'); + this.disableForm(); + this.setFormStatusMessage('info', t('selecting')); + const submitResult = await this.validateAndSendFormValuesToURL(sendMethod, url); + document.cookie = "namespaceFilterId=" + this.getFormValue('namespace') + ";"; + document.cookie = "namespaceFilterName=" + submitResult.name + ";"; + + if (submitResult) { + if(leave) { + history.back(); + }else{ + location.reload(); + } + }else { + this.enableForm(); + this.setFormStatusMessage('warning', t('thereAreErrorsInTheFormPleaseFixThemAnd')); + } + }else{ + this.setFormStatusMessage('warning', t('namespaceMustBeSelected')); + } + } + } + + render() { + const t = this.props.t; + + return ( +
+ + {t('namespaceFilter')} + +
+ + + + +
+ ); + } +} diff --git a/client/src/namespaces/List.js b/client/src/namespaces/List.js index dcb684c8..af25941b 100644 --- a/client/src/namespaces/List.js +++ b/client/src/namespaces/List.js @@ -11,6 +11,7 @@ import {getGlobalNamespaceId} from "../../../shared/namespaces"; import {withComponentMixins} from "../lib/decorator-helpers"; import mailtrainConfig from 'mailtrainConfig'; import PropTypes from 'prop-types'; +import {getNamespaceIdFilterCookie} from '../lib/namespace'; @withComponentMixins([ withTranslation, @@ -61,6 +62,12 @@ export default class List extends Component { return actions; }; + var namespacesTree = this.table = node} withHeader withDescription dataUrl="rest/namespaces-tree" actions={actions} /> + + if(mailtrainConfig.namespaceFilterEnabled && getNamespaceIdFilterCookie()){ + namespacesTree = this.table = node} withHeader withDescription dataUrl={"rest/namespaces-tree/" + getNamespaceIdFilterCookie()} actions={actions} /> + } + return (
{tableRestActionDialogRender(this)} @@ -72,7 +79,7 @@ export default class List extends Component { {t('namespaces')} - this.table = node} withHeader withDescription dataUrl="rest/namespaces-tree" actions={actions} /> + {namespacesTree}
); } diff --git a/client/src/namespaces/root.js b/client/src/namespaces/root.js index d2c334bf..29456769 100644 --- a/client/src/namespaces/root.js +++ b/client/src/namespaces/root.js @@ -1,54 +1,106 @@ 'use strict'; import React from 'react'; -import CUD from './CUD'; -import List from './List'; +import CUD from "./CUD"; +import List from './List' +import Filter from "./Filter"; import Share from '../shares/Share'; import {ellipsizeBreadcrumbLabel} from "../lib/helpers"; import {namespaceCheckPermissions} from "../lib/namespace"; +import mailtrainConfig from "mailtrainConfig"; function getMenus(t) { - return { - namespaces: { - title: t('namespaces'), - link: '/namespaces', - checkPermissions: { - createNamespace: { - entityTypeId: 'namespace', - requiredOperations: ['createNamespace'] - }, - ...namespaceCheckPermissions('createNamespace') - }, - panelRender: props => , - children: { - ':namespaceId([0-9]+)': { - title: resolved => t('namespaceName', {name: ellipsizeBreadcrumbLabel(resolved.namespace.name)}), - resolve: { - namespace: params => `rest/namespaces/${params.namespaceId}` + if(mailtrainConfig.namespaceFilterEnabled){ + return { + namespaces: { + title: t('namespaces'), + link: '/namespaces', + checkPermissions: { + createNamespace: { + entityTypeId: 'namespace', + requiredOperations: ['createNamespace'] }, - link: params => `/namespaces/${params.namespaceId}/edit`, - navs: { - ':action(edit|delete)': { - title: t('edit'), - link: params => `/namespaces/${params.namespaceId}/edit`, - visible: resolved => resolved.namespace.permissions.includes('edit'), - panelRender: props => + ...namespaceCheckPermissions('createNamespace') + }, + panelRender: props => , + children: { + ':namespaceId([0-9]+)': { + title: resolved => t('namespaceName', {name: ellipsizeBreadcrumbLabel(resolved.namespace.name)}), + resolve: { + namespace: params => `rest/namespaces/${params.namespaceId}` }, - share: { - title: t('share'), - link: params => `/namespaces/${params.namespaceId}/share`, - visible: resolved => resolved.namespace.permissions.includes('share'), - panelRender: props => + link: params => `/namespaces/${params.namespaceId}/edit`, + navs: { + ':action(edit|delete)': { + title: t('edit'), + link: params => `/namespaces/${params.namespaceId}/edit`, + visible: resolved => resolved.namespace.permissions.includes('edit'), + panelRender: props => + }, + share: { + title: t('share'), + link: params => `/namespaces/${params.namespaceId}/share`, + visible: resolved => resolved.namespace.permissions.includes('share'), + panelRender: props => + } } - } - }, - create: { - title: t('create'), - panelRender: props => - }, + }, + filter: { + title: t('filter'), + panelRender: props => + }, + create: { + title: t('create'), + panelRender: props => + }, + } } - } - }; + }; + } + else{ + return { + namespaces: { + title: t('namespaces'), + link: '/namespaces', + checkPermissions: { + createNamespace: { + entityTypeId: 'namespace', + requiredOperations: ['createNamespace'] + }, + ...namespaceCheckPermissions('createNamespace') + }, + panelRender: props => , + children: { + ':namespaceId([0-9]+)': { + title: resolved => t('namespaceName', {name: ellipsizeBreadcrumbLabel(resolved.namespace.name)}), + resolve: { + namespace: params => `rest/namespaces/${params.namespaceId}` + }, + link: params => `/namespaces/${params.namespaceId}/edit`, + navs: { + ':action(edit|delete)': { + title: t('edit'), + link: params => `/namespaces/${params.namespaceId}/edit`, + visible: resolved => resolved.namespace.permissions.includes('edit'), + panelRender: props => + }, + share: { + title: t('share'), + link: params => `/namespaces/${params.namespaceId}/share`, + visible: resolved => resolved.namespace.permissions.includes('share'), + panelRender: props => + } + } + }, + create: { + title: t('create'), + panelRender: props => + }, + } + } + }; + } + } diff --git a/client/src/reports/CUD.js b/client/src/reports/CUD.js index 4fe89ab9..4bb1f310 100644 --- a/client/src/reports/CUD.js +++ b/client/src/reports/CUD.js @@ -25,6 +25,8 @@ import {getDefaultNamespace, NamespaceSelect, validateNamespace} from '../lib/na import {DeleteModalDialog} from "../lib/modals"; import {getUrl} from "../lib/urls"; import {withComponentMixins} from "../lib/decorator-helpers"; +import {getNamespaceIdFilterCookie} from "../lib/namespace"; +import mailtrainConfig from 'mailtrainConfig'; @withComponentMixins([ withTranslation, diff --git a/client/src/reports/List.js b/client/src/reports/List.js index 629fa636..a5053ea2 100644 --- a/client/src/reports/List.js +++ b/client/src/reports/List.js @@ -13,6 +13,8 @@ import {getUrl} from "../lib/urls"; import {tableAddDeleteButton, tableRestActionDialogInit, tableRestActionDialogRender} from "../lib/modals"; import {withComponentMixins} from "../lib/decorator-helpers"; import PropTypes from 'prop-types'; +import {getNamespaceIdFilterCookie} from '../lib/namespace'; +import mailtrainConfig from 'mailtrainConfig'; @withComponentMixins([ withTranslation, @@ -147,6 +149,11 @@ export default class List extends Component { } ]; + var reportsTable =
this.table = node} withHeader dataUrl="rest/reports-table" columns={columns} />; + + if(mailtrainConfig.namespaceFilterEnabled && getNamespaceIdFilterCookie()){ + reportsTable =
this.table = node} withHeader dataUrl={"rest/reports-table/" + getNamespaceIdFilterCookie()} columns={columns} />; + } return (
@@ -162,7 +169,7 @@ export default class List extends Component { {t('reports')} -
this.table = node} withHeader dataUrl="rest/reports-table" columns={columns} /> + {reportsTable} ); } diff --git a/client/src/reports/templates/List.js b/client/src/reports/templates/List.js index 8437f341..70b92b74 100644 --- a/client/src/reports/templates/List.js +++ b/client/src/reports/templates/List.js @@ -11,6 +11,7 @@ import mailtrainConfig from 'mailtrainConfig'; import {tableAddDeleteButton, tableRestActionDialogInit, tableRestActionDialogRender} from "../../lib/modals"; import {withComponentMixins} from "../../lib/decorator-helpers"; import PropTypes from 'prop-types'; +import {getNamespaceIdFilterCookie} from '../../lib/namespace'; @withComponentMixins([ withTranslation, @@ -67,6 +68,12 @@ export default class List extends Component { } ]; + var reportTemplatesTable =
this.table = node} withHeader dataUrl="rest/report-templates-table" columns={columns} /> + + if(mailtrainConfig.namespaceFilterEnabled && getNamespaceIdFilterCookie()){ + reportTemplatesTable =
this.table = node} withHeader dataUrl={"rest/report-templates-table/" + getNamespaceIdFilterCookie()} columns={columns} /> + } + return (
{tableRestActionDialogRender(this)} @@ -82,8 +89,7 @@ export default class List extends Component { } {t('reportTemplates')} - -
this.table = node} withHeader dataUrl="rest/report-templates-table" columns={columns} /> + {reportTemplatesTable} ); } diff --git a/client/src/root.js b/client/src/root.js index 92bfba68..a1bc82c8 100644 --- a/client/src/root.js +++ b/client/src/root.js @@ -17,7 +17,8 @@ import users from './users/root'; import sendConfigurations from './send-configurations/root'; import settings from './settings/root'; -import {DropdownLink, getLanguageChooser, NavDropdown, NavLink, Section} from "./lib/page"; +import {DropdownLink, getLanguageChooser, getNamespaceChooser, NavDropdown, NavLink, Section} from "./lib/page"; +import {getNamespaceNameFilterCookie} from "./lib/namespace"; import mailtrainConfig from 'mailtrainConfig'; import Home from "./Home"; @@ -56,6 +57,10 @@ class Root extends Component { async logout() { await axios.post(getUrl('rest/logout')); window.location = getUrl(); + if(mailtrainConfig.namespaceFilterEnabled){ + document.cookie = 'namespaceFilterId' + '=; expires=Thu, 01 Jan 1970 00:00:01 GMT;';//Delete namespaceFilter cookies + document.cookie = 'namespaceFilterName' + '=; expires=Thu, 01 Jan 1970 00:00:01 GMT;'; + } } render() { @@ -64,7 +69,17 @@ class Root extends Component { const topLevelItems = structure.children; const topLevelMenu = []; + + var namespaceFilter = null; + if(mailtrainConfig.namespaceFilterEnabled){ + if(getNamespaceNameFilterCookie()){ + namespaceFilter = {getNamespaceNameFilterCookie()}; + }else{ + namespaceFilter = {'Namespace filter'}; + } + } + for (const entryKey of topLevelMenuKeys) { const entry = topLevelItems[entryKey]; const link = entry.link || entry.externalLink; @@ -91,6 +106,7 @@ class Root extends Component {
    + {namespaceFilter} {getLanguageChooser(t)} {t('account')} diff --git a/client/src/send-configurations/List.js b/client/src/send-configurations/List.js index 0238265d..a5a97648 100644 --- a/client/src/send-configurations/List.js +++ b/client/src/send-configurations/List.js @@ -11,6 +11,8 @@ import {getMailerTypes} from './helpers'; import {tableAddDeleteButton, tableRestActionDialogInit, tableRestActionDialogRender} from "../lib/modals"; import {withComponentMixins} from "../lib/decorator-helpers"; import PropTypes from 'prop-types'; +import {getNamespaceIdFilterCookie} from '../lib/namespace'; +import mailtrainConfig from 'mailtrainConfig'; @withComponentMixins([ @@ -72,6 +74,12 @@ export default class List extends Component { } ]; + var sendConfigTable =
this.table = node} withHeader dataUrl="rest/send-configurations-table" columns={columns} /> + + if(mailtrainConfig.namespaceFilterEnabled && getNamespaceIdFilterCookie()){ + sendConfigTable =
this.table = node} withHeader dataUrl={'rest/send-configurations-table/' + getNamespaceIdFilterCookie()} columns={columns} /> + } + return (
{tableRestActionDialogRender(this)} @@ -83,7 +91,7 @@ export default class List extends Component { {t('sendConfigurations-1')} -
this.table = node} withHeader dataUrl="rest/send-configurations-table" columns={columns} /> + {sendConfigTable} ); } diff --git a/client/src/templates/CUD.js b/client/src/templates/CUD.js index c89166de..b764fd49 100644 --- a/client/src/templates/CUD.js +++ b/client/src/templates/CUD.js @@ -20,7 +20,7 @@ import { withFormErrorHandlers } from '../lib/form'; import {withErrorHandling} from '../lib/error-handling'; -import {getDefaultNamespace, NamespaceSelect, validateNamespace} from '../lib/namespace'; +import {getDefaultNamespace, NamespaceSelect, validateNamespace, getNamespaceIdFilterCookie} from '../lib/namespace'; import {ContentModalDialog, DeleteModalDialog} from "../lib/modals"; import mailtrainConfig from 'mailtrainConfig'; import {getEditForm, getTagLanguages, getTemplateTypes, getTypeForm} from './helpers'; @@ -325,6 +325,12 @@ export default class CUD extends Component { { data: 6, title: t('namespace') }, ]; + var sourceTemplateTable = ; + + if(mailtrainConfig.namespaceFilterEnabled && getNamespaceIdFilterCookie()){ + sourceTemplateTable = ; + } + return (
{isEdit && @@ -362,9 +368,9 @@ export default class CUD extends Component { } - {this.getFormValue('fromExistingEntity') ? - - : + {this.getFormValue('fromExistingEntity') && sourceTemplateTable } + + {!this.getFormValue('fromExistingEntity') && <> {isEdit ? diff --git a/client/src/templates/List.js b/client/src/templates/List.js index 0aec8123..ec072891 100644 --- a/client/src/templates/List.js +++ b/client/src/templates/List.js @@ -11,7 +11,8 @@ import {getTagLanguages, getTemplateTypes} from './helpers'; import {tableAddDeleteButton, tableRestActionDialogInit, tableRestActionDialogRender} from "../lib/modals"; import {withComponentMixins} from "../lib/decorator-helpers"; import PropTypes from 'prop-types'; - +import {getNamespaceIdFilterCookie} from "../lib/namespace"; +import mailtrainConfig from 'mailtrainConfig'; @withComponentMixins([ withTranslation, @@ -81,6 +82,11 @@ export default class List extends Component { } ]; + var templatesTable =
this.table = node} withHeader dataUrl="rest/templates-table" columns={columns} />; + if(mailtrainConfig.namespaceFilterEnabled && getNamespaceIdFilterCookie()){ + templatesTable =
this.table = node} withHeader dataUrl={"rest/templates-table/" + getNamespaceIdFilterCookie()} columns={columns} />; + } + return (
{tableRestActionDialogRender(this)} @@ -95,7 +101,7 @@ export default class List extends Component { {t('templates')} -
this.table = node} withHeader dataUrl="rest/templates-table" columns={columns} /> + {templatesTable} ); } diff --git a/client/src/templates/helpers.js b/client/src/templates/helpers.js index 52d7a319..7617f4d0 100644 --- a/client/src/templates/helpers.js +++ b/client/src/templates/helpers.js @@ -22,6 +22,7 @@ import {Trans} from "react-i18next"; import {TagLanguages, renderTag} from "../../../shared/templates"; import styles from "../lib/styles.scss"; +import {getNamespaceIdFilterCookie } from "../lib/namespace"; export const ResourceType = { TEMPLATE: 'template', @@ -89,7 +90,18 @@ export function getTemplateTypes(t, prefix = '', entityTypeId = ResourceType.TEM getTypeForm: (owner, isEdit) => { const tagLanguageKey = owner.getFormValue(prefix + 'tag_language'); if (tagLanguageKey) { - return + }else{ + return + } } else { return null; } diff --git a/client/src/templates/mosaico/List.js b/client/src/templates/mosaico/List.js index b3faae6a..2f03bc65 100644 --- a/client/src/templates/mosaico/List.js +++ b/client/src/templates/mosaico/List.js @@ -12,6 +12,9 @@ import {getTagLanguages} from '../helpers'; import {tableAddDeleteButton, tableRestActionDialogInit, tableRestActionDialogRender} from "../../lib/modals"; import {withComponentMixins} from "../../lib/decorator-helpers"; import PropTypes from 'prop-types'; +import {getNamespaceIdFilterCookie} from '../../lib/namespace'; +import mailtrainConfig from 'mailtrainConfig'; +import { id } from 'brace/worker/css'; @withComponentMixins([ @@ -88,6 +91,11 @@ export default class List extends Component { } ]; + var mosaicoTemplatesTable =
this.table = node} withHeader dataUrl="rest/mosaico-templates-table" columns={columns} />; + if(mailtrainConfig.namespaceFilterEnabled && getNamespaceIdFilterCookie()){ + mosaicoTemplatesTable =
this.table = node} withHeader dataUrl={"rest/mosaico-templates-table/" + getNamespaceIdFilterCookie()} columns={columns}/> + } + return (
{tableRestActionDialogRender(this)} @@ -103,7 +111,7 @@ export default class List extends Component { {t('mosaicoTemplates')} -
this.table = node} withHeader dataUrl="rest/mosaico-templates-table" columns={columns} /> + {mosaicoTemplatesTable} ); } diff --git a/client/src/users/List.js b/client/src/users/List.js index 1ac5f0eb..2cd5068e 100644 --- a/client/src/users/List.js +++ b/client/src/users/List.js @@ -8,6 +8,7 @@ import mailtrainConfig from "mailtrainConfig"; import {Icon} from "../lib/bootstrap-components"; import {tableAddDeleteButton, tableRestActionDialogInit, tableRestActionDialogRender} from "../lib/modals"; import {withComponentMixins} from "../lib/decorator-helpers"; +import {getNamespaceIdFilterCookie} from '../lib/namespace'; @withComponentMixins([ withTranslation, @@ -59,6 +60,12 @@ export default class List extends Component { } }); + var usersTable =
this.table = node} withHeader dataUrl="rest/users-table" columns={columns} /> + + if(mailtrainConfig.namespaceFilterEnabled && getNamespaceIdFilterCookie()){ + usersTable =
this.table = node} withHeader dataUrl={'rest/users-table/' + getNamespaceIdFilterCookie()} columns={columns} /> + } + return (
{tableRestActionDialogRender(this)} @@ -67,8 +74,7 @@ export default class List extends Component { {t('users')} - -
this.table = node} withHeader dataUrl="rest/users-table" columns={columns} /> + {usersTable} ); } diff --git a/locales/en-US-last-run/common.json b/locales/en-US-last-run/common.json index dce14596..133d97d6 100644 --- a/locales/en-US-last-run/common.json +++ b/locales/en-US-last-run/common.json @@ -1024,5 +1024,9 @@ "thePasswordMustContainAtLeastOne": "The password must contain at least one lowercase letter", "thePasswordMustContainAtLeastOne-1": "The password must contain at least one uppercase letter", "thePasswordMustContainAtLeastOneNumber": "The password must contain at least one number", - "thePasswordMustContainAtLeastOneSpecial": "The password must contain at least one special character" + "thePasswordMustContainAtLeastOneSpecial": "The password must contain at least one special character", + "namespaceFilter": "Namespace filter", + "filter": "Filter", + "disallow": "Disallow", + "selectAndReturn": "Select and return" } \ No newline at end of file diff --git a/locales/en-US/common.json b/locales/en-US/common.json index 6428b301..b8bf1545 100644 --- a/locales/en-US/common.json +++ b/locales/en-US/common.json @@ -1024,5 +1024,9 @@ "thePasswordMustContainAtLeastOne": "The password must contain at least one lowercase letter", "thePasswordMustContainAtLeastOne-1": "The password must contain at least one uppercase letter", "thePasswordMustContainAtLeastOneNumber": "The password must contain at least one number", - "thePasswordMustContainAtLeastOneSpecial": "The password must contain at least one special character" + "thePasswordMustContainAtLeastOneSpecial": "The password must contain at least one special character", + "namespaceFilter": "Namespace filter", + "filter": "Filter", + "disallow": "Disallow", + "selectAndReturn": "Select and return" } \ No newline at end of file diff --git a/locales/es-ES/common.json b/locales/es-ES/common.json index 905d9d41..318f6c5c 100644 --- a/locales/es-ES/common.json +++ b/locales/es-ES/common.json @@ -1103,5 +1103,9 @@ "thePasswordMustContainAtLeastOne": "The password must contain at least one lowercase letter", "thePasswordMustContainAtLeastOne-1": "The password must contain at least one uppercase letter", "thePasswordMustContainAtLeastOneNumber": "The password must contain at least one number", - "thePasswordMustContainAtLeastOneSpecial": "The password must contain at least one special character" + "thePasswordMustContainAtLeastOneSpecial": "The password must contain at least one special character", + "namespaceFilter": "Filtrado por espacio de nombres", + "filter": "Filtro", + "disallow": "Desactivar", + "selectAndReturn": "Seleccionar y volver" } \ No newline at end of file diff --git a/server/config/default.yaml b/server/config/default.yaml index 39ffb95e..50ef513b 100644 --- a/server/config/default.yaml +++ b/server/config/default.yaml @@ -54,6 +54,9 @@ enabledLanguages: # Inject custom scripts in subscription/layout.mjml.hbs # customSubscriptionScripts: [/custom/hello-world.js] +#Enable to use Namespace Filter +namespaceFilter: + enabled: false # Enable to use Redis session cache or disable if Redis is not installed redis: diff --git a/server/lib/client-helpers.js b/server/lib/client-helpers.js index c00a9389..97c1b63c 100644 --- a/server/lib/client-helpers.js +++ b/server/lib/client-helpers.js @@ -23,6 +23,7 @@ async function getAnonymousConfig(context, appType) { sandboxUrlBaseDir: urls.getSandboxUrlBaseDir(), publicUrlBase: urls.getPublicUrlBase(), publicUrlBaseDir: urls.getPublicUrlBaseDir(), + namespaceFilterEnabled: config.namespaceFilter.enabled, appType } } @@ -49,7 +50,8 @@ async function getAuthenticatedConfig(context) { verpEnabled: config.verp.enabled, reportsEnabled: config.reports.enabled, mapsApiKey: setts.mapsApiKey, - builtinZoneMTAEnabled: config.builtinZoneMTA.enabled + builtinZoneMTAEnabled: config.builtinZoneMTA.enabled, + namespaceFilterEnabled: config.namespaceFilter.enabled } } diff --git a/server/models/campaigns.js b/server/models/campaigns.js index 7c8c5bfa..8ad68509 100644 --- a/server/models/campaigns.js +++ b/server/models/campaigns.js @@ -24,6 +24,7 @@ const contextHelpers = require('../lib/context-helpers'); const {convertFileURLs} = require('../lib/campaign-content'); const messageSender = require('../lib/message-sender'); const lists = require('./lists'); +const namespaces = require('./namespaces'); const {EntityActivityType, CampaignActivityType} = require('../../shared/activity-log'); const activityLog = require('../lib/activity-log'); @@ -67,26 +68,32 @@ function hash(entity, content) { return hasher.hash(filteredEntity); } -async function _listDTAjax(context, namespaceId, params) { +async function _listDTAjax(context, namespaceFilter, params) { + var allowedNamespaces = []; + + if(namespaceFilter){ + allowedNamespaces = await namespaces.getAllowedNamespaces(context, namespaceFilter); + } + return await dtHelpers.ajaxListWithPermissions( context, [{ entityTypeId: 'campaign', requiredOperations: ['view'] }], params, builder => { builder = builder.from('campaigns') - .innerJoin('namespaces', 'namespaces.id', 'campaigns.namespace') - .whereNull('campaigns.parent'); - if (namespaceId) { - builder = builder.where('namespaces.id', namespaceId); - } + .innerJoin('namespaces', 'namespaces.id', 'campaigns.namespace'); + for(const key in allowedNamespaces){ + builder = builder.orWhere('campaigns.namespace', allowedNamespaces[key]); + } + builder = builder.whereNull('campaigns.parent'); return builder; }, ['campaigns.id', 'campaigns.name', 'campaigns.cid', 'campaigns.description', 'campaigns.type', 'campaigns.status', 'campaigns.scheduled', 'campaigns.source', 'campaigns.created', 'namespaces.name'] ); } -async function listDTAjax(context, params) { - return await _listDTAjax(context, undefined, params); +async function listDTAjax(context, params, namespaceId) { + return await _listDTAjax(context, namespaceId, params); } async function listByNamespaceDTAjax(context, namespaceId, params) { @@ -106,14 +113,25 @@ async function listChildrenDTAjax(context, campaignId, params) { } -async function listWithContentDTAjax(context, params) { +async function listWithContentDTAjax(context, namespaceFilter, params) { + var allowedNamespaces = []; + + if(namespaceFilter){ + allowedNamespaces = await namespaces.getAllowedNamespaces(context, namespaceFilter); + } return await dtHelpers.ajaxListWithPermissions( context, [{ entityTypeId: 'campaign', requiredOperations: ['view'] }], params, - builder => builder.from('campaigns') - .innerJoin('namespaces', 'namespaces.id', 'campaigns.namespace') - .whereIn('campaigns.source', [CampaignSource.CUSTOM, CampaignSource.CUSTOM_FROM_TEMPLATE, CampaignSource.CUSTOM_FROM_CAMPAIGN]), + builder => { + builder = builder.from('campaigns') + .innerJoin('namespaces', 'namespaces.id', 'campaigns.namespace'); + for(const key in allowedNamespaces){ + builder = builder.orWhere('campaigns.namespace', allowedNamespaces[key]); + } + builder = builder.whereIn('campaigns.source', [CampaignSource.CUSTOM, CampaignSource.CUSTOM_FROM_TEMPLATE, CampaignSource.CUSTOM_FROM_CAMPAIGN]); + return builder; + }, ['campaigns.id', 'campaigns.name', 'campaigns.cid', 'campaigns.description', 'campaigns.type', 'campaigns.created', 'namespaces.name'] ); } diff --git a/server/models/forms.js b/server/models/forms.js index 8eac9cae..1aaca78d 100644 --- a/server/models/forms.js +++ b/server/models/forms.js @@ -14,6 +14,7 @@ const mjml2html = require('mjml'); const lists = require('./lists'); const dependencyHelpers = require('../lib/dependency-helpers'); +const namespaces = require("./namespaces"); const formAllowedKeys = new Set([ 'name', @@ -57,14 +58,28 @@ function hash(entity) { return hasher.hash(filterObject(entity, hashKeys)); } -async function listDTAjax(context, params) { +async function listDTAjax(context, namespaceFilter, params) { + var allowedNamespaces = []; + + if(namespaceFilter){ + allowedNamespaces = await namespaces.getAllowedNamespaces(context, namespaceFilter); + } + return await dtHelpers.ajaxListWithPermissions( context, [{ entityTypeId: 'customForm', requiredOperations: ['view'] }], params, - builder => builder - .from('custom_forms') - .innerJoin('namespaces', 'namespaces.id', 'custom_forms.namespace'), + builder => { + builder = builder + .from('custom_forms') + .innerJoin('namespaces', 'namespaces.id', 'custom_forms.namespace'); + if (namespaceFilter) { + for(const key in allowedNamespaces){ + builder = builder.orWhere('namespaces.id', allowedNamespaces[key]); + } + } + return builder; + }, ['custom_forms.id', 'custom_forms.name', 'custom_forms.description', 'namespaces.name'] ); } diff --git a/server/models/lists.js b/server/models/lists.js index da6f42f2..6ccbca2d 100644 --- a/server/models/lists.js +++ b/server/models/lists.js @@ -13,6 +13,7 @@ const segments = require('./segments'); const imports = require('./imports'); const entitySettings = require('../lib/entity-settings'); const dependencyHelpers = require('../lib/dependency-helpers'); +const namespaces = require('./namespaces'); const {EntityActivityType} = require('../../shared/activity-log'); const activityLog = require('../lib/activity-log'); @@ -26,8 +27,13 @@ function hash(entity) { } -async function _listDTAjax(context, namespaceId, params) { +async function _listDTAjax(context, namespaceFilter, params) { const campaignEntityType = entitySettings.getEntityType('campaign'); + var allowedNamespaces = []; + + if(namespaceFilter){ + allowedNamespaces = await namespaces.getAllowedNamespaces(context, namespaceFilter); + } return await dtHelpers.ajaxListWithPermissions( context, @@ -37,9 +43,11 @@ async function _listDTAjax(context, namespaceId, params) { builder = builder .from('lists') .innerJoin('namespaces', 'namespaces.id', 'lists.namespace'); - if (namespaceId) { - builder = builder.where('lists.namespace', namespaceId); - } + if (namespaceFilter) { + for(const key in allowedNamespaces){ + builder = builder.orWhere('lists.namespace', allowedNamespaces[key]); + } + } return builder; }, ['lists.id', 'lists.name', 'lists.cid', 'lists.subscribers', 'lists.description', 'namespaces.name', @@ -59,11 +67,7 @@ async function _listDTAjax(context, namespaceId, params) { ); } -async function listDTAjax(context, params) { - return await _listDTAjax(context, undefined, params); -} - -async function listByNamespaceDTAjax(context, namespaceId, params) { +async function listDTAjax(context, namespaceId, params) { return await _listDTAjax(context, namespaceId, params); } @@ -274,7 +278,6 @@ async function remove(context, id) { module.exports.UnsubscriptionMode = UnsubscriptionMode; module.exports.hash = hash; module.exports.listDTAjax = listDTAjax; -module.exports.listByNamespaceDTAjax = listByNamespaceDTAjax; module.exports.listWithSegmentByCampaignDTAjax = listWithSegmentByCampaignDTAjax; module.exports.getByIdTx = getByIdTx; module.exports.getById = getById; diff --git a/server/models/mosaico-templates.js b/server/models/mosaico-templates.js index fb05ed64..b7c3616e 100644 --- a/server/models/mosaico-templates.js +++ b/server/models/mosaico-templates.js @@ -10,6 +10,7 @@ const shares = require('./shares'); const files = require('./files'); const dependencyHelpers = require('../lib/dependency-helpers'); const { allTagLanguages } = require('../../shared/templates'); +const namespaces = require('./namespaces'); const allowedKeys = new Set(['name', 'description', 'type', 'tag_language', 'data', 'namespace']); @@ -27,24 +28,52 @@ async function getById(context, id) { }); } -async function listDTAjax(context, params) { +async function listDTAjax(context, namespaceFilter, params) { + var allowedNamespaces = []; + + if(namespaceFilter){ + allowedNamespaces = await namespaces.getAllowedNamespaces(context, namespaceFilter); + } return await dtHelpers.ajaxListWithPermissions( context, [{ entityTypeId: 'mosaicoTemplate', requiredOperations: ['view'] }], params, - builder => builder.from('mosaico_templates').innerJoin('namespaces', 'namespaces.id', 'mosaico_templates.namespace'), + builder => { + builder = builder + .from('mosaico_templates') + .innerJoin('namespaces', 'namespaces.id', 'mosaico_templates.namespace'); + if (namespaceFilter) { + for(const key in allowedNamespaces){ + builder = builder.orWhere('namespaces.id', allowedNamespaces[key]); + } + } + return builder; + }, [ 'mosaico_templates.id', 'mosaico_templates.name', 'mosaico_templates.description', 'mosaico_templates.type', 'mosaico_templates.tag_language', 'mosaico_templates.created', 'namespaces.name' ] ); } -async function listByTagLanguageDTAjax(context, tagLanguage, params) { +async function listByTagLanguageDTAjax(context, tagLanguage, namespaceFilter, params) { + var allowedNamespaces = []; + + if(namespaceFilter){ + allowedNamespaces = await namespaces.getAllowedNamespaces(context, namespaceFilter); + } return await dtHelpers.ajaxListWithPermissions( context, [{ entityTypeId: 'mosaicoTemplate', requiredOperations: ['view'] }], params, - builder => builder.from('mosaico_templates') - .innerJoin('namespaces', 'namespaces.id', 'mosaico_templates.namespace') - .where('mosaico_templates.tag_language', tagLanguage), + builder => { + builder = builder.from('mosaico_templates') + .innerJoin('namespaces', 'namespaces.id', 'mosaico_templates.namespace'); + if (namespaceFilter) { + for(const key in allowedNamespaces){ + builder = builder.orWhere('namespaces.id', allowedNamespaces[key]); + } + } + builder = builder.where('mosaico_templates.tag_language', tagLanguage); + return builder; + }, [ 'mosaico_templates.id', 'mosaico_templates.name', 'mosaico_templates.description', 'mosaico_templates.type', 'mosaico_templates.tag_language', 'mosaico_templates.created', 'namespaces.name' ] ); } diff --git a/server/models/namespaces.js b/server/models/namespaces.js index b1343874..b7f676a0 100644 --- a/server/models/namespaces.js +++ b/server/models/namespaces.js @@ -12,7 +12,8 @@ const dependencyHelpers = require('../lib/dependency-helpers'); const allowedKeys = new Set(['name', 'description', 'namespace']); -async function listTree(context) { + +async function listTree(context, nsId, search) { enforce(!context.user.admin, 'listTree is not supposed to be called by assumed admin'); const entityType = entitySettings.getEntityType('namespace'); @@ -33,7 +34,7 @@ async function listTree(context) { 'namespaces.id', 'namespaces.name', 'namespaces.description', 'namespaces.namespace', knex.raw(`GROUP_CONCAT(${entityType.permissionsTable + '.operation'} SEPARATOR \';\') as permissions`) ]); - + const entries = {}; for (let row of rows) { @@ -102,6 +103,30 @@ async function listTree(context) { delete entry.parent; } + if(nsId && !search){ + var root = []; + + function process_node(node,topNamespace){ + var branch = false; + if(node){ + if(node.key == topNamespace){ + branch = true; + return node; + } + if(node.children && !branch){ + for(const key in node.children){ + const n = process_node(node.children[key], topNamespace); + if(n){ + return n; + } + } + } + } + return null; + } + root.push(process_node(roots[0], nsId)); + return root; + } return roots; } @@ -241,6 +266,35 @@ async function remove(context, id) { }); } +async function getAllowedNamespaces(context, topNamespace){ + + const tree = await listTree(context, null, true); + var allowedNamespaces = []; + + function process_node(node, namespaces, branch, topNamespace){ + if(node){ + if(branch){ + namespaces.push(node.key); + } + if(node.key == topNamespace){ + branch = true; + namespaces.push(node.key); + } + if(node.children){ + for(const key in node.children){ + process_node(node.children[key], namespaces, branch, topNamespace); + } + } + } + } + if(tree && topNamespace){ + process_node(tree[0], allowedNamespaces, false, topNamespace); + allowedNamespaces.sort(); + } + + return allowedNamespaces; +} + module.exports.hash = hash; module.exports.listTree = listTree; module.exports.getById = getById; @@ -249,3 +303,4 @@ module.exports.create = create; module.exports.createTx = createTx; module.exports.updateWithConsistencyCheck = updateWithConsistencyCheck; module.exports.remove = remove; +module.exports.getAllowedNamespaces = getAllowedNamespaces; \ No newline at end of file diff --git a/server/models/report-templates.js b/server/models/report-templates.js index 06433e52..41a142d3 100644 --- a/server/models/report-templates.js +++ b/server/models/report-templates.js @@ -9,6 +9,7 @@ const namespaceHelpers = require('../lib/namespace-helpers'); const shares = require('./shares'); const reports = require('./reports'); const dependencyHelpers = require('../lib/dependency-helpers'); +const namespaces = require('./namespaces'); const allowedKeys = new Set(['name', 'description', 'mime_type', 'user_fields', 'js', 'hbs', 'namespace']); @@ -25,12 +26,26 @@ async function getById(context, id) { }); } -async function listDTAjax(context, params) { +async function listDTAjax(context, namespaceFilter, params) { + var allowedNamespaces = []; + + if(namespaceFilter){ + allowedNamespaces = await namespaces.getAllowedNamespaces(context, namespaceFilter); + } return await dtHelpers.ajaxListWithPermissions( context, [{ entityTypeId: 'reportTemplate', requiredOperations: ['view'] }], params, - builder => builder.from('report_templates').innerJoin('namespaces', 'namespaces.id', 'report_templates.namespace'), + builder => { + builder = builder.from('report_templates') + .innerJoin('namespaces', 'namespaces.id', 'report_templates.namespace'); + if (namespaceFilter) { + for(const key in allowedNamespaces){ + builder = builder.orWhere('namespaces.id', allowedNamespaces[key]); + } + } + return builder; + }, [ 'report_templates.id', 'report_templates.name', 'report_templates.description', 'report_templates.created', 'namespaces.name' ] ); } diff --git a/server/models/reports.js b/server/models/reports.js index 0f2bde6c..acc4bc65 100644 --- a/server/models/reports.js +++ b/server/models/reports.js @@ -14,6 +14,7 @@ const contextHelpers = require('../lib/context-helpers'); const {LinkId} = require('./links'); const subscriptions = require('./subscriptions'); const {Readable} = require('stream'); +const namespaces = require('./namespaces'); const ReportState = require('../../shared/reports').ReportState; @@ -45,7 +46,12 @@ async function getByIdWithTemplate(context, id, withPermissions = true) { }); } -async function listDTAjax(context, params) { +async function listDTAjax(context, namespaceFilter, params) { + var allowedNamespaces = []; + + if(namespaceFilter){ + allowedNamespaces = await namespaces.getAllowedNamespaces(context, namespaceFilter); + } return await dtHelpers.ajaxListWithPermissions( context, [ @@ -53,9 +59,17 @@ async function listDTAjax(context, params) { { entityTypeId: 'reportTemplate', requiredOperations: ['view'] } ], params, - builder => builder.from('reports') - .innerJoin('report_templates', 'reports.report_template', 'report_templates.id') - .innerJoin('namespaces', 'namespaces.id', 'reports.namespace'), + builder => { + builder = builder.from('reports') + .innerJoin('report_templates', 'reports.report_template', 'report_templates.id') + .innerJoin('namespaces', 'namespaces.id', 'reports.namespace'); + if (namespaceFilter) { + for(const key in allowedNamespaces){ + builder = builder.orWhere('namespaces.id', allowedNamespaces[key]); + } + } + return builder; + }, [ 'reports.id', 'reports.name', 'report_templates.name', 'reports.description', 'reports.last_run', 'namespaces.name', 'reports.state', 'report_templates.mime_type' diff --git a/server/models/send-configurations.js b/server/models/send-configurations.js index 7fa4c909..9b2b4204 100644 --- a/server/models/send-configurations.js +++ b/server/models/send-configurations.js @@ -13,6 +13,7 @@ const contextHelpers = require('../lib/context-helpers'); const mailers = require('../lib/mailers'); const senders = require('../lib/senders'); const dependencyHelpers = require('../lib/dependency-helpers'); +const namespaces = require('./namespaces'); const allowedKeys = new Set(['name', 'description', 'from_email', 'from_email_overridable', 'from_name', 'from_name_overridable', 'reply_to', 'reply_to_overridable', 'x_mailer', 'verp_hostname', 'verp_disable_sender_header', 'mailer_type', 'mailer_settings', 'namespace']); @@ -22,7 +23,14 @@ function hash(entity) { return hasher.hash(filterObject(entity, allowedKeys)); } -async function _listDTAjax(context, namespaceId, params) { +async function _listDTAjax(context, namespaceFilter, params) { + + var allowedNamespaces = []; + + if(namespaceFilter){ + allowedNamespaces = await namespaces.getAllowedNamespaces(context, namespaceFilter); + } + return await dtHelpers.ajaxListWithPermissions( context, [{ entityTypeId: 'sendConfiguration', requiredOperations: ['viewPublic'] }], @@ -31,8 +39,10 @@ async function _listDTAjax(context, namespaceId, params) { builder = builder .from('send_configurations') .innerJoin('namespaces', 'namespaces.id', 'send_configurations.namespace'); - if (namespaceId) { - builder = builder.where('send_configurations.namespace', namespaceId); + if (namespaceFilter) { + for(const key in allowedNamespaces){ + builder = builder.orWhere('send_configurations.namespace', allowedNamespaces[key]); + } } return builder; }, @@ -40,8 +50,8 @@ async function _listDTAjax(context, namespaceId, params) { ); } -async function listDTAjax(context, params) { - return await _listDTAjax(context, undefined, params); +async function listDTAjax(context, namespaceId, params) { + return await _listDTAjax(context, namespaceId, params); } async function listByNamespaceDTAjax(context, namespaceId, params) { diff --git a/server/models/templates.js b/server/models/templates.js index 209e1635..ed63aa0d 100644 --- a/server/models/templates.js +++ b/server/models/templates.js @@ -12,6 +12,7 @@ const dependencyHelpers = require('../lib/dependency-helpers'); const {convertFileURLs} = require('../lib/campaign-content'); const { allTagLanguages } = require('../../shared/templates'); const messageSender = require('../lib/message-sender'); +const namespaces = require('./namespaces'); const allowedKeys = new Set(['name', 'description', 'type', 'tag_language', 'data', 'html', 'text', 'namespace']); @@ -37,15 +38,23 @@ async function getById(context, id, withPermissions = true) { }); } -async function _listDTAjax(context, namespaceId, params) { +async function _listDTAjax(context, namespaceFilter, params) { + var allowedNamespaces = []; + + if(namespaceFilter){ + allowedNamespaces = await namespaces.getAllowedNamespaces(context, namespaceFilter); + } + return await dtHelpers.ajaxListWithPermissions( context, [{ entityTypeId: 'template', requiredOperations: ['view'] }], params, builder => { builder = builder.from('templates').innerJoin('namespaces', 'namespaces.id', 'templates.namespace'); - if (namespaceId) { - builder = builder.where('namespaces.id', namespaceId); + if (namespaceFilter) { + for(const key in allowedNamespaces){ + builder = builder.orWhere('namespaces.id', allowedNamespaces[key]); + } } return builder; }, @@ -53,11 +62,7 @@ async function _listDTAjax(context, namespaceId, params) { ); } -async function listDTAjax(context, params) { - return await _listDTAjax(context, undefined, params); -} - -async function listByNamespaceDTAjax(context, namespaceId, params) { +async function listDTAjax(context, namespaceId, params) { return await _listDTAjax(context, namespaceId, params); } @@ -177,7 +182,6 @@ module.exports.hash = hash; module.exports.getByIdTx = getByIdTx; module.exports.getById = getById; module.exports.listDTAjax = listDTAjax; -module.exports.listByNamespaceDTAjax = listByNamespaceDTAjax; module.exports.create = create; module.exports.updateWithConsistencyCheck = updateWithConsistencyCheck; module.exports.remove = remove; diff --git a/server/models/users.js b/server/models/users.js index 6ff8626e..a67f244c 100644 --- a/server/models/users.js +++ b/server/models/users.js @@ -14,6 +14,7 @@ const {getTrustedUrl} = require('../lib/urls'); const { tUI } = require('../lib/translate'); const messageSender = require('../lib/message-sender'); const {getSystemSendConfigurationId} = require('../../shared/send-configurations'); +const namespaces = require('./namespaces'); const bluebird = require('bluebird'); @@ -108,17 +109,35 @@ async function serverValidate(context, data, isOwnAccount) { return result; } -async function listDTAjax(context, params) { +async function listDTAjax(context, namespaceFilter, params) { + + var allowedNamespaces = []; + + if(namespaceFilter){ + allowedNamespaces = await namespaces.getAllowedNamespaces(context, namespaceFilter); + } + return await dtHelpers.ajaxListWithPermissions( context, [{ entityTypeId: 'namespace', requiredOperations: ['manageUsers'] }], params, - builder => builder - .from('users') - .innerJoin('namespaces', 'namespaces.id', 'users.namespace') - .innerJoin('generated_role_names', 'generated_role_names.role', 'users.role') - .where('generated_role_names.entity_type', 'global'), - [ 'users.id', 'users.username', 'users.name', 'namespaces.name', 'generated_role_names.name' ] + builder => { + builder = builder + .from('users') + .innerJoin('namespaces', 'namespaces.id', 'users.namespace') + .innerJoin('generated_role_names', 'generated_role_names.role', 'users.role') + + if (namespaceFilter) { + for(const key in allowedNamespaces){ + builder = builder.orWhere('generated_role_names.entity_type', 'global').andWhere('users.namespace', allowedNamespaces[key]); + } + }else{ + builder = builder.where('generated_role_names.entity_type', 'global'); + } + + return builder; + }, + [ 'users.id', 'users.username', 'users.name', 'namespaces.name', 'generated_role_names.name' ] ); } diff --git a/server/routes/rest/campaigns.js b/server/routes/rest/campaigns.js index 0d1cf657..b246a129 100644 --- a/server/routes/rest/campaigns.js +++ b/server/routes/rest/campaigns.js @@ -6,13 +6,20 @@ const campaigns = require('../../models/campaigns'); const router = require('../../lib/router-async').create(); const {castToInteger} = require('../../lib/helpers'); - router.postAsync('/campaigns-table', passport.loggedIn, async (req, res) => { - return res.json(await campaigns.listDTAjax(req.context, req.body)); + return res.json(await campaigns.listDTAjax(req.context, req.body, null)); +}); + +router.postAsync('/campaigns-table/:namespaceId', passport.loggedIn, async (req, res) => { + return res.json(await campaigns.listDTAjax(req.context, req.body, castToInteger(req.params.namespaceId))); }); router.postAsync('/campaigns-with-content-table', passport.loggedIn, async (req, res) => { - return res.json(await campaigns.listWithContentDTAjax(req.context, req.body)); + return res.json(await campaigns.listWithContentDTAjax(req.context, null, req.body)); +}); + +router.postAsync('/campaigns-with-content-table/:namespaceId', passport.loggedIn, async (req, res) => { + return res.json(await campaigns.listWithContentDTAjax(req.context, req.params.namespaceId, req.body)); }); router.postAsync('/campaigns-others-by-list-table/:campaignId/:listIds', passport.loggedIn, async (req, res) => { diff --git a/server/routes/rest/forms.js b/server/routes/rest/forms.js index ad8cdb6e..5b82e81b 100644 --- a/server/routes/rest/forms.js +++ b/server/routes/rest/forms.js @@ -13,7 +13,11 @@ const {castToInteger} = require('../../lib/helpers'); router.postAsync('/forms-table', passport.loggedIn, async (req, res) => { - return res.json(await forms.listDTAjax(req.context, req.body)); + return res.json(await forms.listDTAjax(req.context, null, req.body)); +}); + +router.postAsync('/forms-table/:namespaceId', passport.loggedIn, async (req, res) => { + return res.json(await forms.listDTAjax(req.context, req.params.namespaceId, req.body)); }); router.getAsync('/forms/:formId', passport.loggedIn, async (req, res) => { diff --git a/server/routes/rest/lists.js b/server/routes/rest/lists.js index b524f099..93c78703 100644 --- a/server/routes/rest/lists.js +++ b/server/routes/rest/lists.js @@ -8,11 +8,11 @@ const {castToInteger} = require('../../lib/helpers'); router.postAsync('/lists-table', passport.loggedIn, async (req, res) => { - return res.json(await lists.listDTAjax(req.context, req.body)); + return res.json(await lists.listDTAjax(req.context, null, req.body)); }); -router.postAsync('/lists-by-namespace-table/:namespaceId', passport.loggedIn, async (req, res) => { - return res.json(await lists.listByNamespaceDTAjax(req.context, castToInteger(req.params.namespaceId), req.body)); +router.postAsync('/lists-table/:namespaceId', passport.loggedIn, async (req, res) => { + return res.json(await lists.listDTAjax(req.context, castToInteger(req.params.namespaceId), req.body)); }); router.postAsync('/lists-with-segment-by-campaign-table/:campaignId', passport.loggedIn, async (req, res) => { diff --git a/server/routes/rest/mosaico-templates.js b/server/routes/rest/mosaico-templates.js index 52bd2357..3ee0035b 100644 --- a/server/routes/rest/mosaico-templates.js +++ b/server/routes/rest/mosaico-templates.js @@ -30,11 +30,19 @@ router.deleteAsync('/mosaico-templates/:mosaicoTemplateId', passport.loggedIn, p }); router.postAsync('/mosaico-templates-table', passport.loggedIn, async (req, res) => { - return res.json(await mosaicoTemplates.listDTAjax(req.context, req.body)); + return res.json(await mosaicoTemplates.listDTAjax(req.context, null, req.body)); +}); + +router.postAsync('/mosaico-templates-table/:namespaceId', passport.loggedIn, async (req, res) => { + return res.json(await mosaicoTemplates.listDTAjax(req.context, req.params.namespaceId, req.body)); }); router.postAsync('/mosaico-templates-by-tag-language-table/:tagLanguage', passport.loggedIn, async (req, res) => { - return res.json(await mosaicoTemplates.listByTagLanguageDTAjax(req.context, req.params.tagLanguage, req.body)); + return res.json(await mosaicoTemplates.listByTagLanguageDTAjax(req.context, req.params.tagLanguage, null, req.body)); +}); + +router.postAsync('/mosaico-templates-by-tag-language-table/:tagLanguage/:namespaceId', passport.loggedIn, async (req, res) => { + return res.json(await mosaicoTemplates.listByTagLanguageDTAjax(req.context, req.params.tagLanguage, req.params.namespaceId, req.body)); }); diff --git a/server/routes/rest/namespaces.js b/server/routes/rest/namespaces.js index cd093b16..0d52f5c6 100644 --- a/server/routes/rest/namespaces.js +++ b/server/routes/rest/namespaces.js @@ -32,9 +32,16 @@ router.deleteAsync('/namespaces/:nsId', passport.loggedIn, passport.csrfProtecti return res.json(); }); +router.getAsync('/namespaces-tree/:nsId', passport.loggedIn, async (req, res) => { + + const tree = await namespaces.listTree(req.context, req.params.nsId, false); + + return res.json(tree); +}); + router.getAsync('/namespaces-tree', passport.loggedIn, async (req, res) => { - const tree = await namespaces.listTree(req.context); + const tree = await namespaces.listTree(req.context, null, false); return res.json(tree); }); diff --git a/server/routes/rest/report-templates.js b/server/routes/rest/report-templates.js index 4fb62313..5dd71c18 100644 --- a/server/routes/rest/report-templates.js +++ b/server/routes/rest/report-templates.js @@ -31,7 +31,11 @@ router.deleteAsync('/report-templates/:reportTemplateId', passport.loggedIn, pas }); router.postAsync('/report-templates-table', passport.loggedIn, async (req, res) => { - return res.json(await reportTemplates.listDTAjax(req.context, req.body)); + return res.json(await reportTemplates.listDTAjax(req.context, null, req.body)); +}); + +router.postAsync('/report-templates-table/:namespaceId', passport.loggedIn, async (req, res) => { + return res.json(await reportTemplates.listDTAjax(req.context, req.params.namespaceId, req.body)); }); router.getAsync('/report-template-user-fields/:reportTemplateId', passport.loggedIn, async (req, res) => { diff --git a/server/routes/rest/reports.js b/server/routes/rest/reports.js index f9c594e5..f3e9d5fd 100644 --- a/server/routes/rest/reports.js +++ b/server/routes/rest/reports.js @@ -35,7 +35,11 @@ router.deleteAsync('/reports/:reportId', passport.loggedIn, passport.csrfProtect }); router.postAsync('/reports-table', passport.loggedIn, async (req, res) => { - return res.json(await reports.listDTAjax(req.context, req.body)); + return res.json(await reports.listDTAjax(req.context, null, req.body)); +}); + +router.postAsync('/reports-table/:namespaceId', passport.loggedIn, async (req, res) => { + return res.json(await reports.listDTAjax(req.context, req.params.namespaceId, req.body)); }); router.postAsync('/report-start/:id', passport.loggedIn, passport.csrfProtection, async (req, res) => { diff --git a/server/routes/rest/send-configurations.js b/server/routes/rest/send-configurations.js index 3c148295..4dbb38c9 100644 --- a/server/routes/rest/send-configurations.js +++ b/server/routes/rest/send-configurations.js @@ -37,11 +37,11 @@ router.deleteAsync('/send-configurations/:sendConfigurationId', passport.loggedI }); router.postAsync('/send-configurations-table', passport.loggedIn, async (req, res) => { - return res.json(await sendConfigurations.listDTAjax(req.context, req.body)); + return res.json(await sendConfigurations.listDTAjax(req.context, null, req.body)); }); -router.postAsync('/send-configurations-by-namespace-table/:namespaceId', passport.loggedIn, async (req, res) => { - return res.json(await sendConfigurations.listByNamespaceDTAjax(req.context, castToInteger(req.params.namespaceId), req.body)); +router.postAsync('/send-configurations-table/:namespaceId', passport.loggedIn, async (req, res) => { + return res.json(await sendConfigurations.listDTAjax(req.context, castToInteger(req.params.namespaceId), req.body)); }); router.postAsync('/send-configurations-with-send-permission-table', passport.loggedIn, async (req, res) => { diff --git a/server/routes/rest/templates.js b/server/routes/rest/templates.js index ee7ec252..9863fe8a 100644 --- a/server/routes/rest/templates.js +++ b/server/routes/rest/templates.js @@ -31,11 +31,11 @@ router.deleteAsync('/templates/:templateId', passport.loggedIn, passport.csrfPro }); router.postAsync('/templates-table', passport.loggedIn, async (req, res) => { - return res.json(await templates.listDTAjax(req.context, req.body)); + return res.json(await templates.listDTAjax(req.context, null, req.body)); }); -router.postAsync('/templates-by-namespace-table/:namespaceId', passport.loggedIn, async (req, res) => { - return res.json(await templates.listByNamespaceDTAjax(req.context, castToInteger(req.params.namespaceId), req.body)); +router.postAsync('/templates-table/:namespaceId', passport.loggedIn, async (req, res) => { + return res.json(await templates.listDTAjax(req.context, castToInteger(req.params.namespaceId), req.body)); }); module.exports = router; \ No newline at end of file diff --git a/server/routes/rest/users.js b/server/routes/rest/users.js index 955668ba..bb763b14 100644 --- a/server/routes/rest/users.js +++ b/server/routes/rest/users.js @@ -35,7 +35,11 @@ router.postAsync('/users-validate', passport.loggedIn, async (req, res) => { }); router.postAsync('/users-table', passport.loggedIn, async (req, res) => { - return res.json(await users.listDTAjax(req.context, req.body)); + return res.json(await users.listDTAjax(req.context, null, req.body)); +}); + +router.postAsync('/users-table/:namespaceId', passport.loggedIn, async (req, res) => { + return res.json(await users.listDTAjax(req.context, castToInteger(req.params.namespaceId), req.body)); });