From e06494883835dc7e5b9c91df47b2f82ff4e5e7a9 Mon Sep 17 00:00:00 2001 From: Tomas Bures Date: Sun, 12 May 2019 10:00:10 +0200 Subject: [PATCH] RC1 of confirmation dialogs displayed when one navigates from a page with unsaved changes. Fixes in Share and UserShare. --- client/src/lib/form.js | 15 ++--- client/src/lib/modals.js | 25 ++++++-- client/src/shares/Share.js | 56 +++++++++++------- client/src/shares/UserShares.js | 79 +++++++++++++------------ client/src/templates/helpers.js | 101 ++++++++++++++++++++------------ locales/en-US/common.json | 5 ++ 6 files changed, 172 insertions(+), 109 deletions(-) diff --git a/client/src/lib/form.js b/client/src/lib/form.js index 4c72c522..b7b79d73 100644 --- a/client/src/lib/form.js +++ b/client/src/lib/form.js @@ -1099,9 +1099,8 @@ const withForm = createComponentMixin([], [], (TargetClass, InnerClass) => { data.originalHash = data.hash; delete data.hash; - const mutator = this.getFormValuesMutator; - if (mutator) { - mutator(data); + if (this.getFormValuesMutator) { + this.getFormValuesMutator(data); } this.populateFormValues(data); @@ -1126,9 +1125,8 @@ const withForm = createComponentMixin([], [], (TargetClass, InnerClass) => { data.originalHash = data.hash; delete data.hash; - const mutator = this.getFormValuesMutator; - if (mutator) { - const newData = mutator(data); + if (this.getFormValuesMutator) { + const newData = this.getFormValuesMutator(data); if (newData !== undefined) { data = newData; @@ -1157,9 +1155,8 @@ const withForm = createComponentMixin([], [], (TargetClass, InnerClass) => { let data = this.getFormValues(); - const mutator = this.submitFormValuesMutator; - if (mutator) { - const newData = mutator(data); + if (this.submitFormValuesMutator) { + const newData = this.submitFormValuesMutator(data); if (newData !== undefined) { data = newData; } diff --git a/client/src/lib/modals.js b/client/src/lib/modals.js index 09cdddc7..be3e8bd2 100644 --- a/client/src/lib/modals.js +++ b/client/src/lib/modals.js @@ -197,10 +197,22 @@ export function tableRestActionDialogInit(owner) { function _hide(owner, dontRefresh = false) { - owner.tableRestActionDialogData = {}; + const refreshTables = owner.tableRestActionDialogData.refreshTables; + owner.setState({ tableRestActionDialogShown: false }); + if (!dontRefresh) { - owner.table.refresh(); + owner.tableRestActionDialogData = {}; + + if (refreshTables) { + refreshTables(); + } else { + owner.table.refresh(); + } + } else { + // _hide is called twice: (1) at performing action, and at (2) success. Here we keep the refreshTables + // reference till it is really needed in step #2. + owner.tableRestActionDialogData = { refreshTables }; } } @@ -268,14 +280,19 @@ export function tableAddRestActionButton(actions, owner, action, button, title, actionData: action.data, actionInProgressMsg: actionInProgressMsg, actionDoneMsg: actionDoneMsg, - onErrorAsync: onErrorAsync + onErrorAsync: onErrorAsync, + refreshTables: action.refreshTables }; owner.setState({ tableRestActionDialogShown: true }); - owner.table.refresh(); + if (action.refreshTables) { + action.refreshTables(); + } else { + owner.table.refresh(); + } } }); } diff --git a/client/src/shares/Share.js b/client/src/shares/Share.js index 4a729bb2..f7ad7857 100644 --- a/client/src/shares/Share.js +++ b/client/src/shares/Share.js @@ -4,18 +4,16 @@ import React, {Component} from 'react'; import PropTypes from 'prop-types'; import {withTranslation} from '../lib/i18n'; import {requiresAuthenticatedUser, Title, withPageHelpers} from '../lib/page'; -import {withAsyncErrorHandler, withErrorHandling} from '../lib/error-handling'; import {Button, ButtonRow, Form, FormSendMethod, TableSelect, withForm, withFormErrorHandlers} from '../lib/form'; import {Table} from '../lib/table'; -import axios from '../lib/axios'; +import {HTTPMethod} from '../lib/axios'; import mailtrainConfig from 'mailtrainConfig'; -import {getUrl} from "../lib/urls"; import {withComponentMixins} from "../lib/decorator-helpers"; +import {tableAddRestActionButton, tableRestActionDialogInit, tableRestActionDialogRender} from "../lib/modals"; @withComponentMixins([ withTranslation, withForm, - withErrorHandling, withPageHelpers, requiresAuthenticatedUser ]) @@ -23,9 +21,13 @@ export default class Share extends Component { constructor(props) { super(props); + this.state = {}; + this.initForm({ leaveConfirmation: false }); + + tableRestActionDialogInit(this); } static propTypes = { @@ -34,19 +36,6 @@ export default class Share extends Component { entityTypeId: PropTypes.string } - @withAsyncErrorHandler - async deleteShare(userId) { - const data = { - entityTypeId: this.props.entityTypeId, - entityId: this.props.entity.id, - userId - }; - - await axios.put(getUrl('rest/shares'), data); - this.sharesTable.refresh(); - this.usersTableSelect.refresh(); - } - clearShareFields() { this.populateFormValues({ entityTypeId: this.props.entityTypeId, @@ -116,15 +105,37 @@ export default class Share extends Component { const autoGenerated = data[4]; if (!autoGenerated) { - actions.push({ - label: 'Delete', - action: () => this.deleteShare(data[3]) - }); + const username = data[0]; + const userId = data[3]; + + tableAddRestActionButton( + actions, + this, + { + method: HTTPMethod.PUT, + url: 'rest/shares', + data: { + entityTypeId: this.props.entityTypeId, + entityId: this.props.entity.id, + userId + }, + refreshTables: () => { + this.sharesTable.refresh(); + this.usersTableSelect.refresh(); + } + }, + { icon: 'trash-alt', label: t('Unshare') }, + t('Confirm Unsharing'), + t('Are you sure you want to remove the share to user "{{username}}"?', {username}), + t('Removing share for user "{{username}}"', {username}), + t('Share for user "{{username}}" removed', {username}), + null + ); } return actions; } - }) + }); let usersLabelIndex = 1; const usersColumns = [ @@ -146,6 +157,7 @@ export default class Share extends Component { return (
+ {tableRestActionDialogRender(this)} {this.props.title}

{t('addUser')}

diff --git a/client/src/shares/UserShares.js b/client/src/shares/UserShares.js index f86c2d76..975c779d 100644 --- a/client/src/shares/UserShares.js +++ b/client/src/shares/UserShares.js @@ -4,16 +4,13 @@ import React, {Component} from 'react'; import PropTypes from 'prop-types'; import {withTranslation} from '../lib/i18n'; import {requiresAuthenticatedUser, Title, withPageHelpers} from '../lib/page'; -import {withAsyncErrorHandler, withErrorHandling} from '../lib/error-handling'; import {Table} from '../lib/table'; -import axios from '../lib/axios'; -import {Icon} from "../lib/bootstrap-components"; -import {getUrl} from "../lib/urls"; +import {HTTPMethod} from '../lib/axios'; import {withComponentMixins} from "../lib/decorator-helpers"; +import {tableAddRestActionButton, tableRestActionDialogInit, tableRestActionDialogRender} from "../lib/modals"; @withComponentMixins([ withTranslation, - withErrorHandling, withPageHelpers, requiresAuthenticatedUser ]) @@ -22,33 +19,19 @@ export default class UserShares extends Component { super(props); this.sharesTables = {}; + + this.state = {}; + tableRestActionDialogInit(this); } static propTypes = { user: PropTypes.object } - @withAsyncErrorHandler - async deleteShare(entityTypeId, entityId) { - const data = { - entityTypeId, - entityId, - userId: this.props.user.id - }; - - await axios.put(getUrl('rest/shares'), data); - for (const key in this.sharesTables) { - this.sharesTables[key].refresh(); - } - } - - componentDidMount() { - } - render() { const t = this.props.t; - const renderSharesTable = (entityTypeId, title) => { + const renderSharesTable = (entityTypeId, title, typeName) => { const columns = [ { data: 0, title: t('name') }, { data: 1, title: t('role') }, @@ -59,10 +42,33 @@ export default class UserShares extends Component { const perms = data[4]; if (!autoGenerated && perms.includes('share')) { - actions.push({ - label: , - action: () => this.deleteShare(entityTypeId, data[2]) - }); + const name = data[0]; + const entityId = data[2]; + + tableAddRestActionButton( + actions, + this, + { + method: HTTPMethod.PUT, + url: 'rest/shares', + data: { + entityTypeId, + entityId, + userId: this.props.user.id + }, + refreshTables: () => { + for (const key in this.sharesTables) { + this.sharesTables[key].refresh(); + } + } + }, + { icon: 'trash-alt', label: t('Unshare') }, + t('Confirm Unsharing'), + t('Are you sure you want to remove the sharing of the {{typeName}} "{{name}}"?', {typeName, name}), + t('Removing sharing of the {{typeName}} "{{name}}"', {typeName, name}), + t('Sharing of the {{typeName}} "{{name}}" removed', {typeName, name}), + null + ); } return actions; @@ -80,17 +86,18 @@ export default class UserShares extends Component { return (
+ {tableRestActionDialogRender(this)} {t('sharesForUserUsername', {username: this.props.user.username})} - {renderSharesTable('namespace', t('namespaces'))} - {renderSharesTable('list', t('lists'))} - {renderSharesTable('template', t('Templates'))} - {renderSharesTable('mosaicoTemplate', t('Mosaico Templates'))} - {renderSharesTable('campaign', t('Campaigns'))} - {renderSharesTable('customForm', t('customForms-1'))} - {renderSharesTable('report', t('reports'))} - {renderSharesTable('reportTemplate', t('reportTemplates'))} - {renderSharesTable('sendConfiguration', t('Send Configurations'))} + {renderSharesTable('namespace', t('namespaces'), t('namespace_lc'))} + {renderSharesTable('list', t('lists'), t('list_lc'))} + {renderSharesTable('template', t('Templates'), t('template_lc'))} + {renderSharesTable('mosaicoTemplate', t('Mosaico Templates'), t('Mosaico template'))} + {renderSharesTable('campaign', t('Campaigns'), t('campaign_lc'))} + {renderSharesTable('customForm', t('customForms-1', t('custom forms')))} + {renderSharesTable('report', t('reports'), t('report_lc'))} + {renderSharesTable('reportTemplate', t('reportTemplates'), t('report template'))} + {renderSharesTable('sendConfiguration', t('Send Configurations'), t('send configuration'))}
); } diff --git a/client/src/templates/helpers.js b/client/src/templates/helpers.js index 54e53c10..b99d5a33 100644 --- a/client/src/templates/helpers.js +++ b/client/src/templates/helpers.js @@ -103,14 +103,19 @@ export function getTemplateTypes(t, prefix = '', entityTypeId = ResourceType.TEM /> , exportHTMLEditorData: async owner => { - const {html, metadata, model} = await owner.editorNode.exportState(); - return { - [prefix + 'html']: html, - [prefix + 'mosaicoData']: { - metadata, - model - } - }; + const state = await owner.editorNode.exportState(); + // If the sandbox is still loading, the exportState returns null. + if (state) { + return { + [prefix + 'html']: state.html, + [prefix + 'mosaicoData']: { + metadata: state.metadata, + model: state.model + } + }; + } else { + return null; + } }, exportContent: async (owner, contentType) => { const {html, metadata, model} = await owner.editorNode.exportState(); @@ -184,14 +189,19 @@ export function getTemplateTypes(t, prefix = '', entityTypeId = ResourceType.TEM /> , exportHTMLEditorData: async owner => { - const {html, metadata, model} = await owner.editorNode.exportState(); - return { - [prefix + 'html']: html, - [prefix + 'mosaicoData']: { - metadata, - model - } - }; + const state = await owner.editorNode.exportState(); + // If the sandbox is still loading, the exportState returns null. + if (state) { + return { + [prefix + 'html']: state.html, + [prefix + 'mosaicoData']: { + metadata: state.metadata, + model: state.model + } + }; + } else { + return null; + } }, exportContent: async (owner, contentType) => { const {html, metadata, model} = await owner.editorNode.exportState(); @@ -265,14 +275,19 @@ export function getTemplateTypes(t, prefix = '', entityTypeId = ResourceType.TEM /> , exportHTMLEditorData: async owner => { - const {html, source, style} = await owner.editorNode.exportState(); - return { - [prefix + 'html']: html, - [prefix + 'grapesJSData']: { - source, - style - } - }; + const state = await owner.editorNode.exportState(); + // If the sandbox is still loading, the exportState returns null. + if (state) { + return { + [prefix + 'html']: state.html, + [prefix + 'grapesJSData']: { + source: state.source, + style: state.style + } + }; + } else { + return null; + } }, exportContent: async (owner, contentType) => { const {html, source, style} = await owner.editorNode.exportState(); @@ -326,13 +341,18 @@ export function getTemplateTypes(t, prefix = '', entityTypeId = ResourceType.TEM /> , exportHTMLEditorData: async owner => { - const {html, source} = await owner.editorNode.exportState(); - return { - [prefix + 'html']: html, - [prefix + 'ckeditor4Data']: { - source - } - }; + const state = await owner.editorNode.exportState(); + // If the sandbox is still loading, the exportState returns null. + if (state) { + return { + [prefix + 'html']: state.html, + [prefix + 'ckeditor4Data']: { + source: state.source + } + }; + } else { + return null; + } }, exportContent: async (owner, contentType) => { const {html, source} = await owner.editorNode.exportState(); @@ -401,13 +421,18 @@ export function getTemplateTypes(t, prefix = '', entityTypeId = ResourceType.TEM /> , exportHTMLEditorData: async owner => { - const {html, source} = await owner.editorNode.exportState(); - return { - [prefix + 'html']: html, - [prefix + 'codeEditorData']: { - source - } - }; + const state = await owner.editorNode.exportState(); + // If the sandbox is still loading, the exportState returns null. + if (state) { + return { + [prefix + 'html']: state.html, + [prefix + 'codeEditorData']: { + source: state.source + } + }; + } else { + return null; + } }, exportContent: async (owner, contentType) => { const {html, source} = await owner.editorNode.exportState(); diff --git a/locales/en-US/common.json b/locales/en-US/common.json index 7dcce754..56aa1b0a 100644 --- a/locales/en-US/common.json +++ b/locales/en-US/common.json @@ -125,6 +125,7 @@ "editRssCampaign": "Edit RSS Campaign", "editTriggeredCampaign": "Edit Triggered Campaign", "template": "Template", + "template_lc": "template", "template_plural": "Templates", "customContentClonedFromTemplate": "Custom content cloned from template", "customContentClonedFromAnotherCampaign": "Custom content cloned from another campaign", @@ -147,6 +148,7 @@ "subscribers": "Subscribers", "description": "Description", "namespace": "Namespace", + "namespace_lc": "namespace", "namespace_plural": "Namespaces", "namespaceFiltering": "Namespace filtering", "remove": "Remove", @@ -154,6 +156,7 @@ "moveUp": "Move up", "moveDown": "Move down", "list": "List", + "list_lc": "list", "list_plural": "Lists", "segment": "Segment", "useAParticularSegment": "Use a particular segment", @@ -170,6 +173,7 @@ "contentSource": "Content source", "selectingATemplateCreatesACampaign": "Selecting a template creates a campaign specific copy from it.", "campaign": "Campaign", + "campaign_lc": "campaign", "campaign_plural": "Campaigns", "contentOfTheSelectedCampaignWillBeCopied": "Content of the selected campaign will be copied into this campaign.", "renderUrl": "Render URL", @@ -350,6 +354,7 @@ "itSeemsThatSomeoneElseHasDeletedThe-1": "It seems that someone else has deleted the entity in the meantime.", "customForms": "Custom forms", "report": "Report", + "report_lc": "report", "report_plural": "Reports", "reportTemplate": "Report template", "reportTemplate_plural": "Report templates",