From 602364caaef82bb15b841539131dfb1b333e4e0c Mon Sep 17 00:00:00 2001 From: Tomas Bures Date: Fri, 11 Aug 2017 18:16:44 +0200 Subject: [PATCH] Fluid layout Reworked routing and breadcrumb mechanism. It allows resolved parameters in paths, which allows including names of entities in the breadcrumb. Secondary navigation which is aware of permissions. --- client/src/account/root.js | 4 +- client/src/lib/form.js | 341 +---------------------- client/src/lib/page.css | 24 ++ client/src/lib/page.js | 392 ++++++++++++++++++++------- client/src/lists/CUD.js | 50 ++-- client/src/lists/List.js | 19 +- client/src/lists/fields/CUD.js | 60 ++-- client/src/lists/fields/List.js | 4 +- client/src/lists/forms/CUD.js | 40 ++- client/src/lists/forms/List.js | 5 +- client/src/lists/root.js | 113 ++++---- client/src/namespaces/CUD.js | 42 ++- client/src/namespaces/List.js | 41 ++- client/src/namespaces/root.js | 35 ++- client/src/reports/CUD.js | 49 ++-- client/src/reports/List.js | 11 +- client/src/reports/root.js | 96 ++++--- client/src/reports/templates/CUD.js | 45 ++- client/src/reports/templates/List.js | 5 +- client/src/shares/Share.js | 57 ++-- client/src/shares/UserShares.js | 22 +- client/src/users/CUD.js | 73 ++--- client/src/users/List.js | 12 +- client/src/users/root.js | 31 ++- models/fields.js | 27 +- models/forms.js | 3 +- models/lists.js | 19 +- models/namespaces.js | 21 +- models/report-templates.js | 19 +- models/reports.js | 31 ++- models/shares.js | 14 +- routes/reports.js | 2 +- views/layout.hbs | 8 +- 33 files changed, 808 insertions(+), 907 deletions(-) diff --git a/client/src/account/root.js b/client/src/account/root.js index 8dca08f8..9e163bf6 100644 --- a/client/src/account/root.js +++ b/client/src/account/root.js @@ -31,14 +31,14 @@ const getStructure = t => { if (mailtrainConfig.isAuthMethodLocal) { subPaths.forgot = { title: t('Password reset'), - params: [':username?'], + extraParams: [':username?'], link: '/account/forgot', component: Reset }; subPaths.reset = { title: t('Password reset'), - params: [':username', ':resetToken'], + extraParams: [':username', ':resetToken'], link: '/account/reset', component: ResetLink }; diff --git a/client/src/lib/form.js b/client/src/lib/form.js index 333c2174..e2494c9f 100644 --- a/client/src/lib/form.js +++ b/client/src/lib/form.js @@ -559,6 +559,7 @@ class ACEEditor extends Component { } } + function withForm(target) { const inst = target.prototype; @@ -672,20 +673,8 @@ function withForm(target) { }); }; - inst.getFormValuesFromURL = async function(url, mutator) { - 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(url); - - const data = response.data; + inst.getFormValuesFromEntity = function(entity, mutator) { + const data = Object.assign({}, entity); data.originalHash = data.hash; delete data.hash; @@ -697,330 +686,6 @@ function withForm(target) { this.populateFormValues(data); }; - inst.validateAndSendFormValuesToURL = async function(method, url, mutator) { - await this.waitForFormServerValidated(); - - if (this.isFormWithoutErrors()) { - const data = this.getFormValues(); - - if (mutator) { - mutator(data); - } - - let response; - if (method === FormSendMethod.PUT) { - response = await axios.put(url, data); - } else if (method === FormSendMethod.POST) { - response = await axios.post(url, data); - } - - return response.data || true; - - } else { - this.showFormValidation(); - return false; - } - }; - - - inst.populateFormValues = function(data) { - 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] - })); - } - })); - - validateFormState(this, mutState); - }) - })); - }; - - inst.waitForFormServerValidated = async function() { - if (!this.isFormServerValidated()) { - await new Promise(resolve => { formValidateResolve = resolve; }); - } - }; - - inst.scheduleFormRevalidate = function() { - scheduleValidateForm(this); - }; - - inst.updateFormValue = function(key, value) { - this.setState(previousState => { - const oldValue = previousState.formState.getIn(['data', key, 'value']); - - let newState = { - formState: previousState.formState.withMutations(mutState => { - mutState.setIn(['data', key, 'value'], value); - validateFormState(this, mutState); - }) - }; - - const onChangeCallbacks = this.state.formSettings.onChange || {}; - - if (onChangeCallbacks[key]) { - onChangeCallbacks[key](newState, key, oldValue, value); - } - - return newState; - }); - }; - - inst.getFormValue = function(name) { - return this.state.formState.getIn(['data', name, 'value']); - }; - - inst.getFormValues = function(name) { - return this.state.formState.get('data').map(attr => attr.get('value')).toJS(); - }; - - inst.getFormError = function(name) { - return this.state.formState.getIn(['data', name, 'error']); - }; - - inst.isFormWithLoadingNotice = function() { - return this.state.formState.get('state') === FormState.LoadingWithNotice; - }; - - inst.isFormLoading = function() { - return this.state.formState.get('state') === FormState.Loading || this.state.formState.get('state') === FormState.LoadingWithNotice; - }; - - inst.isFormReady = function() { - return this.state.formState.get('state') === FormState.Ready; - }; - - inst.isFormValidationShown = function() { - return this.state.formState.get('isValidationShown'); - }; - - inst.addFormValidationClass = function(className, name) { - if (this.isFormValidationShown()) { - const error = this.getFormError(name); - if (error) { - return className + ' has-error'; - } else { - return className + ' has-success'; - } - } else { - return className; - } - }; - - inst.getFormValidationMessage = function(name) { - if (this.isFormValidationShown()) { - return this.getFormError(name); - } else { - return ''; - } - }; - - inst.showFormValidation = function() { - this.setState(previousState => ({formState: previousState.formState.set('isValidationShown', true)})); - }; - - inst.hideFormValidation = function() { - this.setState(previousState => ({formState: previousState.formState.set('isValidationShown', false)})); - }; - - inst.isFormWithoutErrors = function() { - return !this.state.formState.get('data').find(attr => attr.get('error')); - }; - - inst.isFormServerValidated = function() { - return !this.state.formSettings.serverValidation || this.state.formSettings.serverValidation.changed.every(attr => this.state.formState.getIn(['data', attr, 'serverValidated'])); - }; - - inst.getFormStatusMessageText = function() { - return this.state.formState.get('statusMessageText'); - }; - - inst.getFormStatusMessageSeverity = function() { - return this.state.formState.get('statusMessageSeverity'); - }; - - inst.setFormStatusMessage = function(severity, text) { - this.setState(previousState => ({ - formState: previousState.formState.withMutations(map => { - map.set('statusMessageText', text); - map.set('statusMessageSeverity', severity); - }) - })); - }; - - inst.clearFormStatusMessage = function() { - this.setState(previousState => ({ - formState: previousState.formState.withMutations(map => { - map.set('statusMessageText', ''); - }) - })); - }; - - inst.enableForm = function() { - this.setState(previousState => ({formState: previousState.formState.set('isDisabled', false)})); - }; - - inst.disableForm = function() { - this.setState(previousState => ({formState: previousState.formState.set('isDisabled', true)})); - }; - - inst.isFormDisabled = function() { - return this.state.formState.get('isDisabled'); - }; - - inst.formHandleChangedError = async function(fn) { - const t = this.props.t; - try { - await fn(); - } catch (error) { - if (error instanceof interoperableErrors.ChangedError) { - this.disableForm(); - this.setFormStatusMessage('danger', - - {t('Your updates cannot be saved.')}{' '} - {t('Someone else has introduced modification in the meantime. Refresh your page to start anew with fresh data. Please note that your changes will be lost.')} - - ); - return; - } - - if (error instanceof interoperableErrors.NotFoundError) { - this.disableForm(); - this.setFormStatusMessage('danger', - - {t('Your updates cannot be saved.')}{' '} - {t('It seems that someone else has deleted the entity in the meantime.')} - - ); - return; - } - - throw error; - } - }; - - return target; -} - -function withForm(target) { - const inst = target.prototype; - - const cleanFormState = Immutable.Map({ - state: FormState.Loading, - isValidationShown: false, - isDisabled: false, - statusMessageText: '', - data: Immutable.Map(), - isServerValidationRunning: false - }); - - // 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 || []) { - payload[attr] = mutState.getIn(['data', attr, 'value']); - } - - 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(settings.serverValidation.url, payload) - .then(response => { - - 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 => { - 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); - })); - } - } - - inst.initForm = function(settings) { - const state = this.state || {}; - state.formState = cleanFormState; - state.formSettings = settings || {}; - this.state = state; - }; - - inst.resetFormState = function() { - this.setState({ - formState: cleanFormState - }); - }; - inst.getFormValuesFromURL = async function(url, mutator) { setTimeout(() => { this.setState(previousState => { diff --git a/client/src/lib/page.css b/client/src/lib/page.css index 1dbf2b8f..28bfa709 100644 --- a/client/src/lib/page.css +++ b/client/src/lib/page.css @@ -43,4 +43,28 @@ h3.legend { font-size: 21px; margin-bottom: 20px; +} + +.mt-secondary-nav { + margin-top: 5px; + margin-right: 5px; + text-align: right; +} + +@media (max-width: 767px) { + .mt-secondary-nav { + margin: 0px; + background-color: #f5f5f5; + padding: 8px 5px; + border-radius: 4px; + } +} + +.mt-secondary-nav > li { + display: inline-block; + float: none; +} + +.mt-secondary-nav > li > a { + padding: 3px 10px; } \ No newline at end of file diff --git a/client/src/lib/page.js b/client/src/lib/page.js index fa48a796..5b069c68 100644 --- a/client/src/lib/page.js +++ b/client/src/lib/page.js @@ -4,120 +4,235 @@ import React, { Component } from 'react'; import { translate } from 'react-i18next'; import PropTypes from 'prop-types'; import { withRouter } from 'react-router'; -import { BrowserRouter as Router, Route, Link, Switch } from 'react-router-dom' +import {BrowserRouter as Router, Route, Link, Switch, Redirect} from 'react-router-dom' import './page.css'; -import { withErrorHandling } from './error-handling'; +import { withErrorHandling, withAsyncErrorHandler } from './error-handling'; import interoperableErrors from '../../../shared/interoperable-errors'; import { DismissibleAlert, Button } from './bootstrap-components'; import mailtrainConfig from 'mailtrainConfig'; +import axios from '../lib/axios'; -class PageContent extends Component { - static propTypes = { - structure: PropTypes.object.isRequired - } - - getRoutes(urlPrefix, children) { - let routes = []; - for (let routeKey in children) { - const structure = children[routeKey]; - - let path = urlPrefix + routeKey; - let pathWithParams = path; - - if (structure.params) { - pathWithParams = pathWithParams + '/' + structure.params.join('/'); - } - - if (structure.component || structure.render) { - const route = { - component: structure.component, - render: structure.render, - path: (pathWithParams === '' ? '/' : pathWithParams) - }; - - routes.push(route); - } - - if (structure.children) { - routes = routes.concat(this.getRoutes(path + '/', structure.children)); - } - } - - return routes; - } - - renderRoute(route) { - if (route.component) { - return ; - } else if (route.render) { - return ; - } - } - - render() { - let routes = this.getRoutes('', this.props.structure); - return {routes.map(x => this.renderRoute(x))}; - } -} - -@withRouter class Breadcrumb extends Component { static propTypes = { - structure: PropTypes.object.isRequired + route: PropTypes.object.isRequired, + params: PropTypes.object.isRequired, + resolved: PropTypes.object.isRequired } - renderElement(breadcrumbElem) { - if (breadcrumbElem.isActive) { - return
  • {breadcrumbElem.title}
  • ; + renderElement(entry, isActive) { + const params = this.props.params; + let title; + if (typeof entry.title === 'function') { + title = entry.title(this.props.resolved); + } else { + title = entry.title; + } - } else if (breadcrumbElem.externalLink) { - return
  • {breadcrumbElem.title}
  • ; + if (isActive) { + return
  • {title}
  • ; - } else if (breadcrumbElem.link) { - let link; - if (typeof breadcrumbElem.link === 'function') { - link = breadcrumbElem.link(this.props.match); + } else if (entry.externalLink) { + let externalLink; + if (typeof entry.externalLink === 'function') { + externalLink = entry.externalLink(params); } else { - link = breadcrumbElem.link; + externalLink = entry.externalLink; } - return
  • {breadcrumbElem.title}
  • ; + + 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
  • {breadcrumbElem.title}
  • ; + return
  • {title}
  • ; } } render() { - const location = this.props.location.pathname; - const locationElems = location.split('/'); + const route = this.props.route; - let breadcrumbElems = []; - let children = this.props.structure; - - for (let idx = 0; idx < locationElems.length; idx++) { - const breadcrumbElem = children[locationElems[idx]]; - if (!breadcrumbElem) { - break; - } - - breadcrumbElem.isActive = (idx === locationElems.length - 1); - breadcrumbElem.idx = idx; - - breadcrumbElems.push(breadcrumbElem); - children = breadcrumbElem.children; - - if (!children) { - break; - } - } - - const renderedElems = breadcrumbElems.map(x => this.renderElement(x)); + const renderedElems = [...route.parents.map(x => this.renderElement(x)), this.renderElement(route, true)]; return
      {renderedElems}
    ; } } +class SecondaryNavBar 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 className = ''; + if (entry.active) { + className += ' 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 in 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)); + } + } + + let className = 'mt-secondary-nav nav nav-pills'; + if (this.props.className) { + className += ' ' + this.props.className; + } + + return
      {renderedElems}
    ; + } +} + +@translate() +@withErrorHandling +class RouteContent extends Component { + constructor(props) { + super(props); + this.state = {}; + + if (Object.keys(props.route.resolve).length === 0) { + this.state.resolved = {}; + } + } + + static propTypes = { + route: PropTypes.object.isRequired, + flashMessage: PropTypes.object + } + + @withAsyncErrorHandler + async resolve() { + const route = this.props.route; + + const keys = Object.keys(route.resolve); + + if (keys.length > 0) { + const promises = keys.map(key => axios.get(route.resolve[key](this.props.match.params))); + const resolvedArr = await Promise.all(promises); + + const resolved = {}; + for (let idx = 0; idx < keys.length; idx++) { + resolved[keys[idx]] = resolvedArr[idx].data; + } + + this.setState({ + resolved + }); + } + } + + componentDidMount() { + this.resolve(); + } + + render() { + const t = this.props.t; + const route = this.props.route; + const params = this.props.match.params; + const resolved = this.state.resolved; + + if (!route.render && !route.component && route.link) { + let link; + if (typeof route.link === 'function') { + link = route.link(params); + } else { + link = route.link; + } + + return ; + + } else { + if (resolved) { + const compProps = { + match: this.props.match, + location: this.props.location, + resolved + }; + + let component; + if (route.render) { + component = route.render(compProps); + } else if (route.component) { + component = React.createElement(route.component, compProps, null); + } + + + + return ( +
    +
    + + + +
    + {this.props.flashMessage} + {component} +
    + ); + } else { + return
    {t('Loading...')}
    ; + } + } + } +} @withRouter @@ -229,13 +344,93 @@ class SectionContent extends Component { }) } + getRoutes(urlPrefix, resolve, parents, structure, navs) { + 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 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), + component: entry.component, + render: entry.render, + title: entry.title, + link: entry.link, + resolve: entryResolve, + parents, + navs: [...navs, ...entryNavs] + }; + + 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(this.getRoutes(path + '/', entryResolve, childrenParents, { [navKey]: nav }, childNavs)); + } + } + + if (entry.children) { + routes = routes.concat(this.getRoutes(path + '/', entryResolve, childrenParents, entry.children, entryNavs)); + } + } + + return routes; + } + + renderRoute(route) { + let flashMessage; + if (this.state.flashMessageText) { + flashMessage = {this.state.flashMessageText}; + } + + const render = props => ; + + return + } + render() { + let routes = this.getRoutes('', {}, [], this.props.structure, []); + return ( -
    - - {(this.state.flashMessageText && {this.state.flashMessageText})} - -
    + {routes.map(x => this.renderRoute(x))} ); } } @@ -280,9 +475,18 @@ class Title extends Component { } class Toolbar extends Component { + static propTypes = { + className: PropTypes.string, + }; + render() { + let className = 'pull-right mt-button-row'; + if (this.props.className) { + className += ' ' + this.props.className; + } + return ( -
    +
    {this.props.children}
    ); diff --git a/client/src/lists/CUD.js b/client/src/lists/CUD.js index 6cbd0b6c..1abd00ae 100644 --- a/client/src/lists/CUD.js +++ b/client/src/lists/CUD.js @@ -8,7 +8,7 @@ import { withForm, Form, FormSendMethod, InputField, TextArea, TableSelect, ButtonRow, Button, Dropdown, StaticField, CheckBox } from '../lib/form'; -import { withErrorHandling, withAsyncErrorHandler } from '../lib/error-handling'; +import { withErrorHandling } from '../lib/error-handling'; import { DeleteModalDialog } from '../lib/delete'; import { validateNamespace, NamespaceSelect } from '../lib/namespace'; import { UnsubscriptionMode } from '../../../shared/lists'; @@ -24,27 +24,19 @@ export default class CUD extends Component { this.state = {}; - if (props.edit) { - this.state.entityId = parseInt(props.match.params.id); - } - this.initForm(); } static propTypes = { - edit: PropTypes.bool + action: PropTypes.string.isRequired, + entity: PropTypes.object } - - @withAsyncErrorHandler - async loadFormValues() { - await this.getFormValuesFromURL(`/rest/lists/${this.state.entityId}`, data => { - data.form = data.default_form ? 'custom' : 'default'; - }); - } - + componentDidMount() { - if (this.props.edit) { - this.loadFormValues(); + if (this.props.entity) { + this.getFormValuesFromEntity(this.props.entity, data => { + data.form = data.default_form ? 'custom' : 'default'; + }); } else { this.populateFormValues({ name: '', @@ -60,7 +52,6 @@ export default class CUD extends Component { localValidateFormValues(state) { const t = this.props.t; - const edit = this.props.edit; if (!state.getIn(['name', 'value'])) { state.setIn(['name', 'error'], t('Name must not be empty')); @@ -79,12 +70,11 @@ export default class CUD extends Component { async submitHandler() { const t = this.props.t; - const edit = this.props.edit; let sendMethod, url; - if (edit) { + if (this.props.entity) { sendMethod = FormSendMethod.PUT; - url = `/rest/lists/${this.state.entityId}` + url = `/rest/lists/${this.props.entity.id}` } else { sendMethod = FormSendMethod.POST; url = '/rest/lists' @@ -110,7 +100,7 @@ export default class CUD extends Component { render() { const t = this.props.t; - const edit = this.props.edit; + const isEdit = !!this.props.entity; const unsubcriptionModeOptions = [ { @@ -144,7 +134,7 @@ export default class CUD extends Component { key: 'custom', label: t('Custom Forms (select form below)') } - ] + ]; const customFormsColumns = [ {data: 0, title: "#"}, @@ -155,23 +145,23 @@ export default class CUD extends Component { return (
    - {edit && + {isEdit && } - {edit ? t('Edit List') : t('Create List')} + {isEdit ? t('Edit List') : t('Create List')}
    - {edit && + {isEdit && {this.getFormValue('cid')} @@ -184,7 +174,7 @@ export default class CUD extends Component { {this.getFormValue('form') === 'custom' && - The custom form used for this list. You can create a form here.}/> + The custom form used for this list. You can create a form here.}/> } @@ -194,7 +184,7 @@ export default class CUD extends Component {
    diff --git a/client/src/lists/List.js b/client/src/lists/List.js index 6744bdff..9d696063 100644 --- a/client/src/lists/List.js +++ b/client/src/lists/List.js @@ -48,21 +48,21 @@ export default class List extends Component { if (perms.includes('edit')) { actions.push({ label: , - link: '/lists/edit/' + data[0] - }); - } - - if (perms.includes('share')) { - actions.push({ - label: , - link: '/lists/share/' + data[0] + link: `/lists/${data[0]}/edit` }); } if (perms.includes('manageFields')) { actions.push({ label: , - link: '/lists/fields/' + data[0] + link: `/lists/${data[0]}/fields` + }); + } + + if (perms.includes('share')) { + actions.push({ + label: , + link: `/lists/${data[0]}/share` }); } @@ -70,7 +70,6 @@ export default class List extends Component { }; const columns = [ - { data: 0, title: "#" }, { data: 1, title: t('Name') }, { data: 2, title: t('ID'), render: data => `${data}` }, { data: 3, title: t('Subscribers') }, diff --git a/client/src/lists/fields/CUD.js b/client/src/lists/fields/CUD.js index 76b42d6d..19918ae4 100644 --- a/client/src/lists/fields/CUD.js +++ b/client/src/lists/fields/CUD.js @@ -26,17 +26,11 @@ export default class CUD extends Component { this.state = {}; - this.state.listId = parseInt(props.match.params.listId); - - if (props.edit) { - this.state.entityId = parseInt(props.match.params.fieldId); - } - this.fieldTypes = getFieldTypes(props.t); this.initForm({ serverValidation: { - url: `/rest/fields-validate/${this.state.listId}`, + url: `/rest/fields-validate/${this.props.list.id}`, changed: ['key'], extra: ['id'] } @@ -44,28 +38,21 @@ export default class CUD extends Component { } static propTypes = { - edit: PropTypes.bool - } - - @withAsyncErrorHandler - async loadFormValues() { - await this.getFormValuesFromURL(`/rest/fields/${this.state.listId}/${this.state.entityId}`, data => { - if (data.default_value === null) { - data.default_value = ''; - } - }); + action: PropTypes.string.isRequired, + list: PropTypes.object, + entity: PropTypes.object } @withAsyncErrorHandler async loadOrderOptions() { const t = this.props.t; - const flds = await axios.get(`/rest/fields/${this.state.listId}`); + const flds = await axios.get(`/rest/fields/${this.props.list.id}`); const getOrderOptions = fld => { return [ {key: 'none', label: t('Not visible')}, - ...flds.data.filter(x => x.id !== this.state.entityId && x[fld] !== null).sort((x, y) => x[fld] - y[fld]).map(x => ({ key: x.id, label: `${x.name} (${this.fieldTypes[x.type].label})`})), + ...flds.data.filter(x => (!this.props.entity || x.id !== this.props.entity.id) && x[fld] !== null).sort((x, y) => x[fld] - y[fld]).map(x => ({ key: x.id, label: `${x.name} (${this.fieldTypes[x.type].label})`})), {key: 'end', label: t('End of list')} ]; }; @@ -78,8 +65,13 @@ export default class CUD extends Component { } componentDidMount() { - if (this.props.edit) { - this.loadFormValues(); + if (this.props.entity) { + this.getFormValuesFromEntity(this.props.entity, data => { + if (data.default_value === null) { + data.default_value = ''; + } + }); + } else { this.populateFormValues({ name: '', @@ -101,7 +93,6 @@ export default class CUD extends Component { localValidateFormValues(state) { const t = this.props.t; - const edit = this.props.edit; if (!state.getIn(['name', 'value'])) { state.setIn(['name', 'error'], t('Name must not be empty')); @@ -123,15 +114,14 @@ export default class CUD extends Component { async submitHandler() { const t = this.props.t; - const edit = this.props.edit; let sendMethod, url; - if (edit) { + if (this.props.entity) { sendMethod = FormSendMethod.PUT; - url = `/rest/fields/${this.state.listId}/${this.state.entityId}` + url = `/rest/fields/${this.props.list.id}/${this.props.entity.id}` } else { sendMethod = FormSendMethod.POST; - url = `/rest/fields/${this.state.listId}` + url = `/rest/fields/${this.props.list.id}` } try { @@ -145,7 +135,7 @@ export default class CUD extends Component { }); if (submitSuccessful) { - this.navigateToWithFlashMessage(`/rest/fields/${this.state.listId}`, 'success', t('Field saved')); + this.navigateToWithFlashMessage(`/lists/${this.props.list.id}/fields`, 'success', t('Field saved')); } else { this.enableForm(); this.setFormStatusMessage('warning', t('There are errors in the form. Please fix them and submit again.')); @@ -167,7 +157,7 @@ export default class CUD extends Component { render() { const t = this.props.t; - const edit = this.props.edit; + const isEdit = !!this.props.entity; /* const orderColumns = [ @@ -183,18 +173,18 @@ export default class CUD extends Component { return (
    - {edit && + {isEdit && } - {edit ? t('Edit Field') : t('Create Field')} + {isEdit ? t('Edit Field') : t('Create Field')}
    @@ -215,7 +205,7 @@ export default class CUD extends Component {
    diff --git a/client/src/lists/fields/List.js b/client/src/lists/fields/List.js index da50988a..e95782c2 100644 --- a/client/src/lists/fields/List.js +++ b/client/src/lists/fields/List.js @@ -29,7 +29,7 @@ export default class List extends Component { const actions = data => [{ label: , - link: `/lists/fields/edit/${this.state.listId}/${data[0]}` + link: `/lists/${this.state.listId}/fields/${data[0]}/edit` }]; const columns = [ @@ -42,7 +42,7 @@ export default class List extends Component { return (
    - + {t('Fields')} diff --git a/client/src/lists/forms/CUD.js b/client/src/lists/forms/CUD.js index 4d6b708d..c4999d63 100644 --- a/client/src/lists/forms/CUD.js +++ b/client/src/lists/forms/CUD.js @@ -24,10 +24,6 @@ export default class CUD extends Component { this.state = {}; - if (props.edit) { - this.state.entityId = parseInt(props.match.params.id); - } - this.serverValidatedFields = [ 'layout', 'web_subscribe', @@ -241,11 +237,12 @@ export default class CUD extends Component { } static propTypes = { - edit: PropTypes.bool + action: PropTypes.string.isRequired, + entity: PropTypes.object } - @withAsyncErrorHandler - async loadOrPopulateFormValues() { + + componentDidMount() { function supplyDefaults(data) { for (const key in mailtrainConfig.defaultCustomFormValues) { if (!data[key]) { @@ -254,11 +251,12 @@ export default class CUD extends Component { } } - if (this.props.edit) { - await this.getFormValuesFromURL(`/rest/forms/${this.state.entityId}`, data => { + if (this.props.entity) { + this.getFormValuesFromEntity(this.props.entity, data => { data.selectedTemplate = 'layout'; supplyDefaults(data); }); + } else { const data = { name: '', @@ -272,13 +270,8 @@ export default class CUD extends Component { } } - componentDidMount() { - this.loadOrPopulateFormValues(); - } - localValidateFormValues(state) { const t = this.props.t; - const edit = this.props.edit; if (!state.getIn(['name', 'value'])) { state.setIn(['name', 'error'], t('Name must not be empty')); @@ -319,12 +312,11 @@ export default class CUD extends Component { async submitHandler() { const t = this.props.t; - const edit = this.props.edit; let sendMethod, url; - if (edit) { + if (this.props.entity) { sendMethod = FormSendMethod.PUT; - url = `/rest/forms/${this.state.entityId}` + url = `/rest/forms/${this.props.entity.id}` } else { sendMethod = FormSendMethod.POST; url = '/rest/forms' @@ -348,7 +340,7 @@ export default class CUD extends Component { render() { const t = this.props.t; - const edit = this.props.edit; + const isEdit = !!this.props.entity; const templateOptGroups = []; @@ -377,18 +369,18 @@ export default class CUD extends Component { return (
    - {edit && + {isEdit && } - {edit ? t('Edit Custom Forms') : t('Create Custom Forms')} + {isEdit ? t('Edit Custom Forms') : t('Create Custom Forms')}
    @@ -441,7 +433,7 @@ export default class CUD extends Component {
    diff --git a/client/src/lists/forms/List.js b/client/src/lists/forms/List.js index e57dd493..7a1e3eb9 100644 --- a/client/src/lists/forms/List.js +++ b/client/src/lists/forms/List.js @@ -48,13 +48,13 @@ export default class List extends Component { if (perms.includes('edit')) { actions.push({ label: , - link: '/lists/forms/edit/' + data[0] + link: `/lists/forms/${data[0]}/edit` }); } if (perms.includes('share')) { actions.push({ label: , - link: '/lists/forms/share/' + data[0] + link: `/lists/forms/${data[0]}/share` }); } @@ -62,7 +62,6 @@ export default class List extends Component { }; const columns = [ - { data: 0, title: "#" }, { data: 1, title: t('Name') }, { data: 2, title: t('Description') }, { data: 3, title: t('Namespace') } diff --git a/client/src/lists/root.js b/client/src/lists/root.js index c1995c3b..b93ac090 100644 --- a/client/src/lists/root.js +++ b/client/src/lists/root.js @@ -28,79 +28,86 @@ const getStructure = t => { link: '/lists', component: ListsList, children: { -/* FIXME - ':listId': { + ':listId([0-9]+)': { title: resolved => t('List "{{name}}"', {name: resolved.list.name}), resolve: { - list: match => `/rest/lists/${match.params.listId}` + list: params => `/rest/lists/${params.listId}` }, - actions: { - edit: { + link: params => `/lists/${params.listId}/edit`, + navs: { + ':action(edit|delete)': { title: t('Edit'), - params: [':action?'], - render: props => () + link: params => `/lists/${params.listId}/edit`, + visible: resolved => resolved.list.permissions.includes('edit'), + render: props => }, - create: { - title: t('Create'), - render: props => () + fields: { + title: t('Fields'), + link: params => `/lists/${params.listId}/fields/`, + visible: resolved => resolved.list.permissions.includes('manageFields'), + component: FieldsList, + children: { + ':fieldId([0-9]+)': { + title: resolved => t('Field "{{name}}"', {name: resolved.field.name}), + resolve: { + field: params => `/rest/fields/${params.listId}/${params.fieldId}` + }, + 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`, + render: props => + } + } + }, + create: { + title: t('Create Field'), + render: props => + } + } }, share: { title: t('Share'), - render: props => () + link: params => `/lists/${params.listId}/share`, + visible: resolved => resolved.list.permissions.includes('share'), + render: props => } } }, -*/ - edit: { - title: t('Edit List'), - params: [':id', ':action?'], - render: props => () - }, create: { - title: t('Create List'), - render: props => () - }, - share: { - title: t('Share List'), - params: [':id'], - render: props => ( t('Share List "{{name}}"', {name: entity.name})} getUrl={id => `/rest/lists/${id}`} entityTypeId="list" {...props} />) - }, - fields: { - title: t('Fields'), - params: [':listId'], - link: match => `/lists/fields/${match.params.listId}`, - component: FieldsList, - children: { - edit: { - title: t('Edit Field'), - params: [':listId', ':fieldId', ':action?'], - render: props => () - }, - create: { - title: t('Create Field'), - params: [':listId'], - render: props => () - }, - } + title: t('Create'), + render: props => }, forms: { title: t('Custom Forms'), link: '/lists/forms', component: FormsList, children: { - edit: { - title: t('Edit Custom Forms'), - params: [':id', ':action?'], - render: props => () + ':formsId([0-9]+)': { + title: resolved => t('Custom Forms "{{name}}"', {name: 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'), + render: props => + }, + share: { + title: t('Share'), + link: params => `/lists/forms/${params.formsId}/share`, + visible: resolved => resolved.forms.permissions.includes('share'), + render: props => + } + } }, create: { - title: t('Create Custom Forms'), - render: props => () - }, - share: { - title: t('Share Custom Forms'), - params: [':id'], - render: props => ( t('Custom Forms "{{name}}"', {name: entity.name})} getUrl={id => `/rest/forms/${id}`} entityTypeId="customForm" {...props} />) + title: t('Create'), + render: props => } } } diff --git a/client/src/namespaces/CUD.js b/client/src/namespaces/CUD.js index 710cdf38..26819b35 100644 --- a/client/src/namespaces/CUD.js +++ b/client/src/namespaces/CUD.js @@ -21,21 +21,17 @@ export default class CUD extends Component { this.state = {}; - if (props.edit) { - this.state.entityId = parseInt(props.match.params.id); - } - this.initForm(); this.hasChildren = false; - } static propTypes = { - edit: PropTypes.bool + action: PropTypes.string.isRequired, + entity: PropTypes.object } isEditGlobal() { - return this.state.entityId === 1; /* Global namespace id */ + return this.props.entity && this.props.entity.id === 1; /* Global namespace id */ } isDelete() { @@ -46,7 +42,7 @@ export default class CUD extends Component { for (let idx = 0; idx < data.length; idx++) { const entry = data[idx]; - if (entry.key === this.state.entityId) { + if (entry.key === this.props.entity.id) { if (entry.children.length > 0) { this.hasChildren = true; } @@ -71,7 +67,7 @@ export default class CUD extends Component { root.expanded = true; } - if (this.props.edit && !this.isEditGlobal()) { + if (this.props.entity && !this.isEditGlobal()) { this.removeNsIdSubtree(data); } @@ -81,14 +77,9 @@ export default class CUD extends Component { }); } - @withAsyncErrorHandler - async loadFormValues() { - await this.getFormValuesFromURL(`/rest/namespaces/${this.state.entityId}`); - } - componentDidMount() { - if (this.props.edit) { - this.loadFormValues(); + if (this.props.entity) { + this.getFormValuesFromEntity(this.props.entity); } else { this.populateFormValues({ name: '', @@ -122,12 +113,11 @@ export default class CUD extends Component { async submitHandler() { const t = this.props.t; - const edit = this.props.edit; let sendMethod, url; - if (edit) { + if (this.props.entity) { sendMethod = FormSendMethod.PUT; - url = `/rest/namespaces/${this.state.entityId}` + url = `/rest/namespaces/${this.props.entity.id}` } else { sendMethod = FormSendMethod.POST; url = '/rest/namespaces' @@ -188,23 +178,23 @@ export default class CUD extends Component { render() { const t = this.props.t; - const edit = this.props.edit; + const isEdit = !!this.props.entity; return (
    - {!this.isEditGlobal() && !this.hasChildren && edit && + {!this.isEditGlobal() && !this.hasChildren && isEdit && } - {edit ? t('Edit Namespace') : t('Create Namespace')} + {isEdit ? t('Edit Namespace') : t('Create Namespace')}
    @@ -215,7 +205,7 @@ export default class CUD extends Component {
    diff --git a/client/src/namespaces/List.js b/client/src/namespaces/List.js index e118524c..bbd89b39 100644 --- a/client/src/namespaces/List.js +++ b/client/src/namespaces/List.js @@ -4,13 +4,38 @@ import React, { Component } from 'react'; import { translate } from 'react-i18next'; import { requiresAuthenticatedUser, withPageHelpers, Title, Toolbar, NavButton } from '../lib/page'; import { TreeTable } from '../lib/tree'; +import { withErrorHandling, withAsyncErrorHandler } from '../lib/error-handling'; +import axios from '../lib/axios'; @translate() +@withErrorHandling @withPageHelpers @requiresAuthenticatedUser export default class List extends Component { constructor(props) { super(props); + + this.state = {}; + } + + @withAsyncErrorHandler + async fetchPermissions() { + const request = { + createNamespace: { + entityTypeId: 'namespace', + requiredOperations: ['createNamespace'] + } + }; + + const result = await axios.post('/rest/permissions-check', request); + + this.setState({ + createPermitted: result.data.createNamespace + }); + } + + componentDidMount() { + this.fetchPermissions(); } render() { @@ -21,15 +46,15 @@ export default class List extends Component { if (node.data.permissions.includes('edit')) { actions.push({ - label: 'Edit', - link: '/namespaces/edit/' + node.key + label: , + link: `/namespaces/${node.key}/edit` }); } if (node.data.permissions.includes('share')) { actions.push({ - label: 'Share', - link: '/namespaces/share/' + node.key + label: , + link: `/namespaces/${node.key}/share` }); } @@ -38,9 +63,11 @@ export default class List extends Component { return (
    - - - + {this.state.createPermitted && + + + + } {t('Namespaces')} diff --git a/client/src/namespaces/root.js b/client/src/namespaces/root.js index c346bb6f..70361d39 100644 --- a/client/src/namespaces/root.js +++ b/client/src/namespaces/root.js @@ -20,20 +20,31 @@ const getStructure = t => ({ link: '/namespaces', component: List, children: { - edit : { - title: t('Edit Namespace'), - params: [':id', ':action?'], - render: props => () + ':namespaceId([0-9]+)': { + title: resolved => t('Namespace "{{name}}"', {name: 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'), + render: props => + }, + share: { + title: t('Share'), + link: params => `/namespaces/${params.namespaceId}/share`, + visible: resolved => resolved.namespace.permissions.includes('share'), + render: props => + } + } }, - create : { - title: t('Create Namespace'), - render: props => () + create: { + title: t('Create'), + render: props => }, - share: { - title: t('Share Namespace'), - params: [':id'], - render: props => ( t('Share Namespace "{{name}}"', {name: entity.name})} getUrl={id => `/rest/namespaces/${id}`} entityTypeId="namespace" {...props} />) - } } } } diff --git a/client/src/reports/CUD.js b/client/src/reports/CUD.js index ea477e4e..7f0a237a 100644 --- a/client/src/reports/CUD.js +++ b/client/src/reports/CUD.js @@ -25,10 +25,6 @@ export default class CUD extends Component { this.state = {}; - if (props.edit) { - this.state.entityId = parseInt(props.match.params.id); - } - this.initForm({ onChange: { report_template: ::this.onReportTemplateChange @@ -37,11 +33,8 @@ export default class CUD extends Component { } static propTypes = { - edit: PropTypes.bool - } - - isDelete() { - return this.props.match.params.action === 'delete'; + action: PropTypes.string.isRequired, + entity: PropTypes.object } @withAsyncErrorHandler @@ -60,18 +53,13 @@ export default class CUD extends Component { } } - @withAsyncErrorHandler - async loadFormValues() { - await this.getFormValuesFromURL(`/rest/reports/${this.state.entityId}`, data => { - for (const key in data.params) { - data[`param_${key}`] = data.params[key]; - } - }); - } - componentDidMount() { - if (this.props.edit) { - this.loadFormValues(); + if (this.props.entity) { + this.getFormValuesFromEntity(this.props.entity, data => { + for (const key in data.params) { + data[`param_${key}`] = data.params[key]; + } + }); } else { this.populateFormValues({ name: '', @@ -85,7 +73,7 @@ export default class CUD extends Component { localValidateFormValues(state) { const t = this.props.t; - const edit = this.props.edit; + const edit = this.props.entity; if (!state.getIn(['name', 'value'])) { state.setIn(['name', 'error'], t('Name must not be empty')); @@ -130,7 +118,6 @@ export default class CUD extends Component { async submitHandler() { const t = this.props.t; - const edit = this.props.edit; if (!this.getFormValue('user_fields')) { this.setFormStatusMessage('warning', t('Report parameters are not selected. Wait for them to get displayed and then fill them in.')); @@ -138,9 +125,9 @@ export default class CUD extends Component { } let sendMethod, url; - if (edit) { + if (this.props.entity) { sendMethod = FormSendMethod.PUT; - url = `/rest/reports/${this.state.entityId}` + url = `/rest/reports/${this.props.entity.id}` } else { sendMethod = FormSendMethod.POST; url = '/rest/reports' @@ -172,7 +159,7 @@ export default class CUD extends Component { render() { const t = this.props.t; - const edit = this.props.edit; + const isEdit = !!this.props.entity; const reportTemplateColumns = [ { data: 0, title: "#" }, @@ -226,18 +213,18 @@ export default class CUD extends Component { return (
    - {edit && + {isEdit && } - {edit ? t('Edit Report') : t('Create Report')} + {isEdit ? t('Edit Report') : t('Create Report')}
    @@ -259,7 +246,7 @@ export default class CUD extends Component {
    diff --git a/client/src/reports/List.js b/client/src/reports/List.js index a03a59cc..e6050675 100644 --- a/client/src/reports/List.js +++ b/client/src/reports/List.js @@ -94,12 +94,12 @@ export default class List extends Component { if (mimeType === 'text/html') { viewContent = { label: , - link: `reports/view/${id}` + link: `/reports/${id}/view` }; } else if (mimeType === 'text/csv') { viewContent = { label: , - href: `reports/download/${id}` + href: `/reports/${id}/download` }; } @@ -127,7 +127,7 @@ export default class List extends Component { actions.push( { label: , - link: `reports/output/${id}` + link: `/reports/${id}/output` } ); } @@ -139,14 +139,14 @@ export default class List extends Component { if (perms.includes('edit') && permsReportTemplate.includes('execute')) { actions.push({ label: , - link: `/reports/edit/${id}` + link: `/reports/${id}/edit` }); } if (perms.includes('share')) { actions.push({ label: , - link: `/reports/share/${id}` + link: `/reports/${id}/share` }); } @@ -154,7 +154,6 @@ export default class List extends Component { }; const columns = [ - { data: 0, title: "#" }, { data: 1, title: t('Name') }, { data: 2, title: t('Template') }, { data: 3, title: t('Description') }, diff --git a/client/src/reports/root.js b/client/src/reports/root.js index 980a6484..67b00174 100644 --- a/client/src/reports/root.js +++ b/client/src/reports/root.js @@ -13,6 +13,7 @@ import ReportsOutput from './Output'; import ReportTemplatesCUD from './templates/CUD'; import ReportTemplatesList from './templates/List'; import Share from '../shares/Share'; +import { ReportState } from '../../../shared/reports'; const getStructure = t => { @@ -28,49 +29,78 @@ const getStructure = t => { link: '/reports', component: ReportsList, children: { - edit: { - title: t('Edit Report'), - params: [':id', ':action?'], - render: props => () + ':reportId([0-9]+)': { + title: resolved => t('Report "{{name}}"', {name: resolved.report.name}), + resolve: { + report: params => `/rest/reports/${params.reportId}` + }, + link: params => `/reports/${params.reportId}/edit`, + navs: { + ':action(edit|delete)': { + title: t('Edit'), + link: params => `/reports/${params.reportId}/edit`, + visible: resolved => resolved.report.permissions.includes('edit'), + render: props => + }, + view: { + title: t('View'), + link: params => `/reports/${params.reportId}/view`, + visible: resolved => resolved.report.permissions.includes('viewContent') && resolved.report.state === ReportState.FINISHED && resolved.report.mime_type === 'text/html', + render: props => (), + }, + download: { + title: t('Download'), + externalLink: params => `/reports/${params.reportId}/download`, + visible: resolved => resolved.report.permissions.includes('viewContent') && resolved.report.state === ReportState.FINISHED && resolved.report.mime_type === 'text/csv' + }, + output: { + title: t('Output'), + link: params => `/reports/${params.reportId}/output`, + visible: resolved => resolved.report.permissions.includes('viewOutput'), + render: props => () + }, + share: { + title: t('Share'), + link: params => `/reports/${params.reportId}/share`, + visible: resolved => resolved.report.permissions.includes('share'), + render: props => + } + } }, create: { - title: t('Create Report'), - render: props => () - }, - view: { - title: t('View Report'), - params: [':id' ], - render: props => () - }, - output: { - title: t('View Report Output'), - params: [':id' ], - render: props => () - }, - share: { - title: t('Share Report'), - params: [':id'], - render: props => ( t('Share Report "{{name}}"', {name: entity.name})} getUrl={id => `/rest/reports/${id}`} entityTypeId="report" {...props} />) + title: t('Create'), + render: props => }, 'templates': { title: t('Templates'), link: '/reports/templates', component: ReportTemplatesList, children: { - edit: { - title: t('Edit Report Template'), - params: [':id', ':action?'], - render: props => () + ':templateId([0-9]+)': { + title: resolved => t('Template "{{name}}"', {name: resolved.template.name}), + resolve: { + template: params => `/rest/report-templates/${params.templateId}` + }, + link: params => `/reports/templates/${params.templateId}/edit`, + navs: { + ':action(edit|delete)': { + title: t('Edit'), + link: params => `/reports/templates/${params.templateId}/edit`, + visible: resolved => resolved.template.permissions.includes('edit'), + render: props => + }, + share: { + title: t('Share'), + link: params => `/reports/templates/${params.templateId}/share`, + visible: resolved => resolved.template.permissions.includes('share'), + render: props => + } + } }, create: { - title: t('Create Report Template'), - params: [':wizard?'], - render: props => () - }, - share: { - title: t('Share Report Template'), - params: [':id'], - render: props => ( t('Share Report Template "{{name}}"', {name: entity.name})} getUrl={id => `/rest/report-templates/${id}`} entityTypeId="reportTemplate" {...props} />) + title: t('Create'), + extraParams: [':wizard?'], + render: props => } } } diff --git a/client/src/reports/templates/CUD.js b/client/src/reports/templates/CUD.js index 677a1234..a739357e 100644 --- a/client/src/reports/templates/CUD.js +++ b/client/src/reports/templates/CUD.js @@ -20,32 +20,21 @@ export default class CUD extends Component { this.state = {}; - if (props.edit) { - this.state.entityId = parseInt(props.match.params.id); - } - this.initForm(); } static propTypes = { - edit: PropTypes.bool - } - - isDelete() { - return this.props.match.params.action === 'delete'; - } - - @withAsyncErrorHandler - async loadFormValues() { - await this.getFormValuesFromURL(`/rest/report-templates/${this.state.entityId}`); + action: PropTypes.string.isRequired, + wizard: PropTypes.string, + entity: PropTypes.object } componentDidMount() { - if (this.props.edit) { - this.loadFormValues(); + if (this.props.entity) { + this.getFormValuesFromEntity(this.props.entity); } else { - const wizard = this.props.match.params.wizard; + const wizard = this.props.wizard; if (wizard === 'subscribers-all') { this.populateFormValues({ @@ -209,7 +198,6 @@ export default class CUD extends Component { localValidateFormValues(state) { const t = this.props.t; - const edit = this.props.edit; if (!state.getIn(['name', 'value'])) { state.setIn(['name', 'error'], t('Name must not be empty')); @@ -245,12 +233,11 @@ export default class CUD extends Component { async doSubmit(stay) { const t = this.props.t; - const edit = this.props.edit; let sendMethod, url; - if (edit) { + if (this.props.entity) { sendMethod = FormSendMethod.PUT; - url = `/rest/report-templates/${this.state.entityId}` + url = `/rest/report-templates/${this.props.entity.id}` } else { sendMethod = FormSendMethod.POST; url = '/rest/report-templates' @@ -277,22 +264,22 @@ export default class CUD extends Component { render() { const t = this.props.t; - const edit = this.props.edit; + const isEdit = !!this.props.entity; return (
    - {edit && + {isEdit && } - {edit ? t('Edit Report Template') : t('Create Report Template')} + {isEdit ? t('Edit Report Template') : t('Create Report Template')}
    @@ -303,11 +290,11 @@ export default class CUD extends Component { Write the body of the JavaScript function with signature function(inputs, callback) that returns an object to be rendered by the Handlebars template below.}/> Use HTML with Handlebars syntax. See documentation here.}/> - {edit ? + {isEdit ?