Clone from existing custom forms Added

This commit is contained in:
root 2019-07-23 12:35:41 +02:00
parent e9bf4a890c
commit 1bf37e65a3
7 changed files with 108 additions and 97 deletions

View file

@ -10,6 +10,7 @@ import {
AlignedRow, AlignedRow,
Button, Button,
ButtonRow, ButtonRow,
CheckBox,
Dropdown, Dropdown,
Fieldset, Fieldset,
filterData, filterData,
@ -45,7 +46,9 @@ export default class CUD extends Component {
this.state = { this.state = {
previewContents: null, previewContents: null,
previewFullscreen: false previewFullscreen: false,
fromSourceCustomForms: false,
sourceCustomForms: null,
}; };
this.serverValidatedFields = [ this.serverValidatedFields = [
@ -330,8 +333,10 @@ export default class CUD extends Component {
const data = { const data = {
name: '', name: '',
description: '', description: '',
fromSourceCustomForms: false,
sourceCustomForms: null,
selectedTemplate: 'layout', selectedTemplate: 'layout',
namespace: mailtrainConfig.user.namespace namespace: mailtrainConfig.user.namespace,
}; };
this.supplyDefaults(data); this.supplyDefaults(data);
@ -350,6 +355,12 @@ export default class CUD extends Component {
validateNamespace(t, state); validateNamespace(t, state);
if (state.getIn(['fromSourceCustomForms', 'value']) && !state.getIn(['sourceCustomForms', 'value'])) {
state.setIn(['sourceCustomForms', 'error'], t('sourceCustomFormsMustNotBeEmpty'));
} else {
state.setIn(['sourceCustomForms', 'error'], null);
}
let formsServerValidationRunning = false; let formsServerValidationRunning = false;
const formsErrors = []; const formsErrors = [];
@ -380,21 +391,20 @@ export default class CUD extends Component {
} }
@withFormErrorHandlers @withFormErrorHandlers
async submitHandler(submitAndLeave, copy) { async submitHandler(submitAndLeave) {
const t = this.props.t; const t = this.props.t;
let sendMethod, url; let sendMethod, url;
if (this.props.entity) { if (this.props.entity) {
if(copy){ sendMethod = FormSendMethod.PUT;
sendMethod = FormSendMethod.POST; url = `rest/forms/${this.props.entity.id}`;
url = `rest/forms/${this.props.entity.id}`
}else{
sendMethod = FormSendMethod.PUT;
url = `rest/forms/${this.props.entity.id}`
}
} else { } else {
sendMethod = FormSendMethod.POST; sendMethod = FormSendMethod.POST;
url = 'rest/forms' if(this.getFormValue('sourceCustomForms')!=null){
url = `rest/forms/${this.getFormValue('sourceCustomForms')}`;
}else{
url = 'rest/forms';
}
} }
this.disableForm(); this.disableForm();
@ -408,22 +418,14 @@ export default class CUD extends Component {
this.navigateToWithFlashMessage('/lists/forms', 'success', t('customFormsUpdated')); this.navigateToWithFlashMessage('/lists/forms', 'success', t('customFormsUpdated'));
} else { } else {
await this.getFormValuesFromURL(`rest/forms/${this.props.entity.id}`); await this.getFormValuesFromURL(`rest/forms/${this.props.entity.id}`);
this.enableForm(); this.enableForm()
if(copy){ this.setFormStatusMessage('success', t('customFormsUpdated'));
this.navigateToWithFlashMessage(`/lists/forms`, 'success', t('customFormsCopied'));
}else{
this.setFormStatusMessage('success', t('customFormsUpdated'));
}
} }
} else { } else {
if (submitAndLeave) { if (submitAndLeave) {
this.navigateToWithFlashMessage('/lists/forms', 'success', t('customFormsCreated')); this.navigateToWithFlashMessage('/lists/forms', 'success', t('customFormsCreated'));
} else { } else {
if(copy){ this.navigateToWithFlashMessage(`/lists/forms/${submitResult}/edit`, 'success', t('customFormsCreated'));
this.navigateToWithFlashMessage(`/lists/forms`, 'success', t('customFormsCopied'));
}else{
this.navigateToWithFlashMessage(`/lists/forms/${submitResult}/edit`, 'success', t('customFormsCreated'));
}
} }
} }
} else { } else {
@ -469,6 +471,12 @@ export default class CUD extends Component {
}); });
} }
const customFormsColumns = [
{ data: 1, title: t('name') },
{ data: 2, title: t('description') },
{ data: 3, title: t('namespace') }
];
const listsColumns = [ const listsColumns = [
{ data: 0, title: "#" }, { data: 0, title: "#" },
{ data: 1, title: t('name') }, { data: 1, title: t('name') },
@ -501,62 +509,72 @@ export default class CUD extends Component {
<NamespaceSelect/> <NamespaceSelect/>
<Fieldset label={t('formsPreview')}> {!isEdit &&
<TableSelect id="previewList" label={t('listToPreviewOn')} withHeader dropdown dataUrl='rest/lists-table' columns={listsColumns} selectionLabelIndex={1} help={t('selectListWhoseFieldsWillBeUsedToPreview')}/> <CheckBox id="fromSourceCustomForms" label={t('customForms')} text={t('cloneFromAnExistingCustomForms')}/>
}
{ previewListId && {this.getFormValue('fromSourceCustomForms') &&
<div> <TableSelect key="sourceCustomFormsKey" id="sourceCustomForms" withHeader dropdown dataUrl='rest/forms-table' columns={customFormsColumns} selectionLabelIndex={1} />
<AlignedRow> }
<div className="help-block">
<small> {!this.getFormValue('fromSourceCustomForms') &&
{t('noteTheseLinksAreSolelyForAQuickPreview')} <Fieldset label={t('formsPreview')}>
</small> <TableSelect id="previewList" label={t('listToPreviewOn')} withHeader dropdown dataUrl='rest/lists-table' columns={listsColumns} selectionLabelIndex={1} help={t('selectListWhoseFieldsWillBeUsedToPreview')}/>
</div>
<p> { previewListId &&
<ActionLink onClickAsync={async () => await this.preview('web_subscribe')}>Subscribe</ActionLink> <div>
{' | '} <AlignedRow>
<ActionLink onClickAsync={async () => await this.preview('web_confirm_subscription_notice')}>Confirm Subscription Notice</ActionLink> <div className="help-block">
{' | '} <small>
<ActionLink onClickAsync={async () => await this.preview('web_confirm_unsubscription_notice')}>Confirm Unsubscription Notice</ActionLink> {t('noteTheseLinksAreSolelyForAQuickPreview')}
{' | '} </small>
<ActionLink onClickAsync={async () => await this.preview('web_subscribed_notice')}>Subscribed Notice</ActionLink>
{' | '}
<ActionLink onClickAsync={async () => await this.preview('web_updated_notice')}>Updated Notice</ActionLink>
{' | '}
<ActionLink onClickAsync={async () => await this.preview('web_unsubscribed_notice')}>Unsubscribed Notice</ActionLink>
{' | '}
<ActionLink onClickAsync={async () => await this.preview('web_manual_unsubscribe_notice')}>Manual Unsubscribe Notice</ActionLink>
{' | '}
<ActionLink onClickAsync={async () => await this.preview('web_unsubscribe')}>Unsubscribe</ActionLink>
{' | '}
<ActionLink onClickAsync={async () => await this.preview('web_manage')}>Manage</ActionLink>
{' | '}
<ActionLink onClickAsync={async () => await this.preview('web_manage_address')}>Manage Address</ActionLink>
{' | '}
<ActionLink onClickAsync={async () => await this.preview('web_privacy_policy_notice')}>Privacy Policy</ActionLink>
</p>
</AlignedRow>
{this.state.previewContents &&
<div className={this.state.previewFullscreen ? formsStyles.editorFullscreen : formsStyles.editor}>
<div className={formsStyles.navbar}>
<div className={formsStyles.navbarLeft}>
{this.state.fullscreen && <img className={formsStyles.logo} src={getTrustedUrl('static/mailtrain-notext.png')}/>}
<div className={formsStyles.title}>{t('formPreview') + ' ' + this.state.previewLabel}</div>
</div> </div>
<div className={formsStyles.navbarRight}> <p>
<a className={formsStyles.btn} onClick={() => this.preview(this.state.previewKey)} title={t('refresh')}><Icon icon="sync-alt"/></a> <ActionLink onClickAsync={async () => await this.preview('web_subscribe')}>Subscribe</ActionLink>
<a className={formsStyles.btn} onClick={() => this.setState({previewFullscreen: !this.state.previewFullscreen})} title={t('maximizeEditor')}><Icon icon="window-maximize"/></a> {' | '}
<a className={formsStyles.btn} onClick={() => this.setState({previewContents: null, previewFullscreen: false})} title={t('closePreview')}><Icon icon="window-close"/></a> <ActionLink onClickAsync={async () => await this.preview('web_confirm_subscription_notice')}>Confirm Subscription Notice</ActionLink>
{' | '}
<ActionLink onClickAsync={async () => await this.preview('web_confirm_unsubscription_notice')}>Confirm Unsubscription Notice</ActionLink>
{' | '}
<ActionLink onClickAsync={async () => await this.preview('web_subscribed_notice')}>Subscribed Notice</ActionLink>
{' | '}
<ActionLink onClickAsync={async () => await this.preview('web_updated_notice')}>Updated Notice</ActionLink>
{' | '}
<ActionLink onClickAsync={async () => await this.preview('web_unsubscribed_notice')}>Unsubscribed Notice</ActionLink>
{' | '}
<ActionLink onClickAsync={async () => await this.preview('web_manual_unsubscribe_notice')}>Manual Unsubscribe Notice</ActionLink>
{' | '}
<ActionLink onClickAsync={async () => await this.preview('web_unsubscribe')}>Unsubscribe</ActionLink>
{' | '}
<ActionLink onClickAsync={async () => await this.preview('web_manage')}>Manage</ActionLink>
{' | '}
<ActionLink onClickAsync={async () => await this.preview('web_manage_address')}>Manage Address</ActionLink>
{' | '}
<ActionLink onClickAsync={async () => await this.preview('web_privacy_policy_notice')}>Privacy Policy</ActionLink>
</p>
</AlignedRow>
{this.state.previewContents &&
<div className={this.state.previewFullscreen ? formsStyles.editorFullscreen : formsStyles.editor}>
<div className={formsStyles.navbar}>
<div className={formsStyles.navbarLeft}>
{this.state.fullscreen && <img className={formsStyles.logo} src={getTrustedUrl('static/mailtrain-notext.png')}/>}
<div className={formsStyles.title}>{t('formPreview') + ' ' + this.state.previewLabel}</div>
</div>
<div className={formsStyles.navbarRight}>
<a className={formsStyles.btn} onClick={() => this.preview(this.state.previewKey)} title={t('refresh')}><Icon icon="sync-alt"/></a>
<a className={formsStyles.btn} onClick={() => this.setState({previewFullscreen: !this.state.previewFullscreen})} title={t('maximizeEditor')}><Icon icon="window-maximize"/></a>
<a className={formsStyles.btn} onClick={() => this.setState({previewContents: null, previewFullscreen: false})} title={t('closePreview')}><Icon icon="window-close"/></a>
</div>
</div> </div>
<iframe className={formsStyles.host} src={"data:text/html;charset=utf-8," + encodeURIComponent(this.state.previewContents)}></iframe>
</div> </div>
<iframe className={formsStyles.host} src={"data:text/html;charset=utf-8," + encodeURIComponent(this.state.previewContents)}></iframe> }
</div> </div>
} }
</div> </Fieldset>
} }
</Fieldset>
{ selectedTemplate && {!this.getFormValue('fromSourceCustomForms') && selectedTemplate &&
<Fieldset label={t('templates')}> <Fieldset label={t('templates')}>
<Dropdown id="selectedTemplate" label={t('edit')} options={templateOptGroups} help={this.templateSettings[selectedTemplate].help}/> <Dropdown id="selectedTemplate" label={t('edit')} options={templateOptGroups} help={this.templateSettings[selectedTemplate].help}/>
<ACEEditor id={selectedTemplate} height="500px" mode={this.templateSettings[selectedTemplate].mode}/> <ACEEditor id={selectedTemplate} height="500px" mode={this.templateSettings[selectedTemplate].mode}/>
@ -565,8 +583,7 @@ export default class CUD extends Component {
<ButtonRow> <ButtonRow>
<Button type="submit" className="btn-primary" icon="check" label={t('save')}/> <Button type="submit" className="btn-primary" icon="check" label={t('save')}/>
<Button type="submit" className="btn-primary" icon="check" label={t('saveAndLeave')} onClickAsync={async () => await this.submitHandler(true, false)}/> <Button type="submit" className="btn-primary" icon="check" label={t('saveAndLeave')} onClickAsync={async () => await this.submitHandler(true)}/>
{isEdit && <Button type="submit" className="btn-success" icon="copy" label={t('copy')} onClickAsync={async () => await this.submitHandler(false, true)}/>}
{canDelete && <LinkButton className="btn-danger" icon="trash-alt" label={t('delete')} to={`/lists/forms/${this.props.entity.id}/delete`}/>} {canDelete && <LinkButton className="btn-danger" icon="trash-alt" label={t('delete')} to={`/lists/forms/${this.props.entity.id}/delete`}/>}
</ButtonRow> </ButtonRow>
</Form> </Form>

View file

@ -924,6 +924,7 @@
"editTemplate": "Edit Template", "editTemplate": "Edit Template",
"createTemplate": "Create Template", "createTemplate": "Create Template",
"cloneFromAnExistingTemplate": "Clone from an existing template", "cloneFromAnExistingTemplate": "Clone from an existing template",
"cloneFromAnExistingCustomForms": "Clone from an existing custom forms",
"mosaico": "Mosaico", "mosaico": "Mosaico",
"templateContentHtml": "Template content (HTML)", "templateContentHtml": "Template content (HTML)",
"mosaicoTemplateDesigner": "Mosaico Template Designer", "mosaicoTemplateDesigner": "Mosaico Template Designer",

View file

@ -924,6 +924,7 @@
"editTemplate": "Edit Template", "editTemplate": "Edit Template",
"createTemplate": "Create Template", "createTemplate": "Create Template",
"cloneFromAnExistingTemplate": "Clone from an existing template", "cloneFromAnExistingTemplate": "Clone from an existing template",
"cloneFromAnExistingCustomForms": "Clone from an existing custom forms",
"mosaico": "Mosaico", "mosaico": "Mosaico",
"templateContentHtml": "Template content (HTML)", "templateContentHtml": "Template content (HTML)",
"mosaicoTemplateDesigner": "Mosaico Template Designer", "mosaicoTemplateDesigner": "Mosaico Template Designer",

View file

@ -993,8 +993,8 @@
"templateDeleted": "Template deleted", "templateDeleted": "Template deleted",
"editTemplate": "Edit Template", "editTemplate": "Edit Template",
"createTemplate": "Create Template", "createTemplate": "Create Template",
"cloneFromAnExistingTemplate": "Clone from an existing template", "cloneFromAnExistingTemplate": "Clonar a partir de templates ya existentes",
"cloneFromAnExistingTemplate - TODO: update line above and then delete this line to mark that the translation has been fixed": "Clone from an existing template", "cloneFromAnExistingCustomForms": "Clonar a partir de formularios personalizados ya existentes",
"mosaico": "Mosaico", "mosaico": "Mosaico",
"templateContentHtml": "Template content (HTML)", "templateContentHtml": "Template content (HTML)",
"mosaicoTemplateDesigner": "Mosaico Template Designer", "mosaicoTemplateDesigner": "Mosaico Template Designer",

View file

@ -105,7 +105,8 @@
"save": "Salvar", "save": "Salvar",
"saveAndLeave": "Salvar e sair", "saveAndLeave": "Salvar e sair",
"saveAndLeave - TODO: update line above and then delete this line to mark that the translation has been fixed": "Save and leave", "saveAndLeave - TODO: update line above and then delete this line to mark that the translation has been fixed": "Save and leave",
"copy": "Copy", "copy": "Copia",
"copy - TODO: update line above and then delete this line to mark that the translation has been fixed": "Copy",
"saveAndGoToStatus": "Save and go to status", "saveAndGoToStatus": "Save and go to status",
"saveAndGoToStatus - TODO: update line above and then delete this line to mark that the translation has been fixed": "Save and go to status", "saveAndGoToStatus - TODO: update line above and then delete this line to mark that the translation has been fixed": "Save and go to status",
"testSend": "Testar envio", "testSend": "Testar envio",
@ -995,6 +996,8 @@
"createTemplate": "Criar modelo", "createTemplate": "Criar modelo",
"cloneFromAnExistingTemplate": "Clone from an existing template", "cloneFromAnExistingTemplate": "Clone from an existing template",
"cloneFromAnExistingTemplate - TODO: update line above and then delete this line to mark that the translation has been fixed": "Clone from an existing template", "cloneFromAnExistingTemplate - TODO: update line above and then delete this line to mark that the translation has been fixed": "Clone from an existing template",
"cloneFromAnExistingCustomForms": "Clone from an existing custom forms",
"cloneFromAnExistingCustomForms - TODO: update line above and then delete this line to mark that the translation has been fixed": "Clone from an existing custom forms",
"mosaico": "Mosaico", "mosaico": "Mosaico",
"templateContentHtml": "Conteúdo do modelo (HTML)", "templateContentHtml": "Conteúdo do modelo (HTML)",
"mosaicoTemplateDesigner": "Editor do Modelo Mosaico", "mosaicoTemplateDesigner": "Editor do Modelo Mosaico",

View file

@ -175,26 +175,16 @@ async function updateWithConsistencyCheck(context, entity) {
}); });
} }
async function copy(context, entity) { async function copy(context, entity, formId) {
await knex.transaction(async tx => { return await knex.transaction(async tx => {
await shares.enforceEntityPermissionTx(tx, context, 'customForm', entity.id, 'edit'); await shares.enforceEntityPermissionTx(tx, context, 'namespace', entity.namespace, 'createCustomForm');
const existing = await _getById(tx, formId);
const existing = await _getById(tx, entity.id);
const existingHash = hash(existing);
if (existingHash !== entity.originalHash) {
throw new interoperableErrors.ChangedError();
}
await namespaceHelpers.validateEntity(tx, entity); await namespaceHelpers.validateEntity(tx, entity);
await namespaceHelpers.validateMove(context, entity, existing, 'customForm', 'createCustomForm', 'delete');
const form = filterObject(entity, allowedFormKeys); const form = filterObject(existing, allowedFormKeys);
enforce(!Object.keys(checkForMjmlErrors(form)).length, 'Error(s) in form templates'); enforce(!Object.keys(checkForMjmlErrors(form)).length, 'Error(s) in form templates');
entity.name = entity.name + '_COPY';
const ids = await tx('custom_forms').insert(filterObject(entity, formAllowedKeys)); const ids = await tx('custom_forms').insert(filterObject(entity, formAllowedKeys));
const id = ids[0]; const id = ids[0];
for (const formKey in form) { for (const formKey in form) {
@ -204,8 +194,8 @@ async function copy(context, entity) {
data_value: form[formKey] data_value: form[formKey]
}) })
} }
await shares.rebuildPermissionsTx(tx, { entityTypeId: 'customForm', entityId: id });
await shares.rebuildPermissionsTx(tx, { entityTypeId: 'customForm', entityId: id });
return id; return id;
}); });
} }

View file

@ -28,9 +28,8 @@ router.postAsync('/forms', passport.loggedIn, passport.csrfProtection, async (re
router.postAsync('/forms/:formId', passport.loggedIn, passport.csrfProtection, async (req, res) => { router.postAsync('/forms/:formId', passport.loggedIn, passport.csrfProtection, async (req, res) => {
const entity = req.body; const entity = req.body;
entity.id = castToInteger(req.params.formId); const formId= castToInteger(req.params.formId);
await forms.copy(req.context, entity); return res.json(await forms.copy(req.context, entity, formId));
return res.json();
}); });
router.putAsync('/forms/:formId', passport.loggedIn, passport.csrfProtection, async (req, res) => { router.putAsync('/forms/:formId', passport.loggedIn, passport.csrfProtection, async (req, res) => {