feat: Add user management invite links without SMTP set up (#5084)

* feat: update n8n-users-list to no longer use preset list of actions

* feat: prepared users settings for invite links feature

* refactor: Return invite link URLs when inviting users (#5079)

* refactor: Return invite link URLs when inviting users

* test: Refactor and add tests to mailer

* feat: Add FE inviteAcceptUrl integration (#5085)

* feat: update n8n-users-list to no longer use preset list of actions

* feat: prepared users settings for invite links feature

* feat: add integration with new inviteAcceptUrl changes

* feat: Add inviteAcceptUrl to user list for pending users

Co-authored-by: Alex Grozav <alex@grozav.com>

* fix conflicts

* fix lint issue

* test: Make sure inviteAcceptUrl is defined

* feat: update smtp setup suggestion

* feat: add invite link summary when inviting multiple users

* refactor: Add telemetry flag for when email is sent

* fix: add email_sent correctly to telemetry event

* feat: move SMTP info-tip to invite modal

Co-authored-by: Omar Ajoue <krynble@gmail.com>
This commit is contained in:
Alex Grozav
2023-01-05 17:10:08 +02:00
committed by GitHub
parent 11a46a4cbc
commit 2327563c44
22 changed files with 419 additions and 247 deletions

View File

@@ -2,21 +2,54 @@
<Modal
:name="INVITE_USER_MODAL_KEY"
@enter="onSubmit"
:title="$locale.baseText('settings.users.inviteNewUsers')"
:title="
$locale.baseText(
showInviteUrls ? 'settings.users.copyInviteUrls' : 'settings.users.inviteNewUsers',
)
"
:center="true"
width="460px"
:eventBus="modalBus"
>
<template #content>
<div v-if="showInviteUrls">
<n8n-users-list :users="invitedUsers">
<template #actions="{ user }">
<n8n-tooltip>
<template #content>
{{ $locale.baseText('settings.users.inviteLink.copy') }}
</template>
<n8n-icon-button
icon="link"
type="tertiary"
@click="onCopyInviteLink(user)"
></n8n-icon-button>
</n8n-tooltip>
</template>
</n8n-users-list>
</div>
<n8n-form-inputs
v-else
:inputs="config"
:eventBus="formBus"
:columnView="true"
@input="onInput"
@submit="onSubmit"
/>
<n8n-info-tip v-if="!settingsStore.isSmtpSetup" class="mt-s">
<i18n path="settings.users.setupSMTPInfo">
<template #link>
<a
href="https://docs.n8n.io/reference/user-management.html#step-one-smtp"
target="_blank"
>
{{ $locale.baseText('settings.users.setupSMTPInfo.link') }}
</a>
</template>
</i18n>
</n8n-info-tip>
</template>
<template #footer>
<template v-if="!showInviteUrls" #footer>
<n8n-button
:loading="loading"
:disabled="!enabledButton"
@@ -32,13 +65,15 @@
import mixins from 'vue-typed-mixins';
import { showMessage } from '@/mixins/showMessage';
import { copyPaste } from '@/mixins/copyPaste';
import Modal from './Modal.vue';
import Vue from 'vue';
import { IFormInputs, IInviteResponse } from '@/Interface';
import { IFormInputs, IInviteResponse, IUser } from '@/Interface';
import { VALID_EMAIL_REGEX, INVITE_USER_MODAL_KEY } from '@/constants';
import { ROLE } from '@/utils';
import { mapStores } from 'pinia';
import { useUsersStore } from '@/stores/users';
import { useSettingsStore } from '@/stores/settings';
const NAME_EMAIL_FORMAT_REGEX = /^.* <(.*)>$/;
@@ -53,7 +88,7 @@ function getEmail(email: string): string {
return parsed;
}
export default mixins(showMessage).extend({
export default mixins(showMessage, copyPaste).extend({
components: { Modal },
name: 'InviteUsersModal',
props: {
@@ -67,6 +102,7 @@ export default mixins(showMessage).extend({
formBus: new Vue(),
modalBus: new Vue(),
emails: '',
showInviteUrls: null as IInviteResponse[] | null,
loading: false,
INVITE_USER_MODAL_KEY,
};
@@ -108,22 +144,35 @@ export default mixins(showMessage).extend({
];
},
computed: {
...mapStores(useUsersStore),
...mapStores(useUsersStore, useSettingsStore),
emailsCount(): number {
return this.emails.split(',').filter((email: string) => !!email.trim()).length;
},
buttonLabel(): string {
if (this.emailsCount > 1) {
return this.$locale.baseText('settings.users.inviteXUser', {
interpolate: { count: this.emailsCount.toString() },
});
return this.$locale.baseText(
`settings.users.inviteXUser${this.settingsStore.isSmtpSetup ? '' : '.inviteUrl'}`,
{
interpolate: { count: this.emailsCount.toString() },
},
);
}
return this.$locale.baseText('settings.users.inviteUser');
return this.$locale.baseText(
`settings.users.inviteUser${this.settingsStore.isSmtpSetup ? '' : '.inviteUrl'}`,
);
},
enabledButton(): boolean {
return this.emailsCount >= 1;
},
invitedUsers(): IUser[] {
console.log(this.usersStore.allUsers, this.showInviteUrls);
return this.showInviteUrls
? this.usersStore.allUsers.filter((user) =>
this.showInviteUrls!.find((invite) => invite.user.id === user.id),
)
: [];
},
},
methods: {
validateEmails(value: string | number | boolean | null | undefined) {
@@ -165,56 +214,106 @@ export default mixins(showMessage).extend({
}
const invited: IInviteResponse[] = await this.usersStore.inviteUsers(emails);
const invitedEmails = invited.reduce(
(accu, { user, error }) => {
if (error) {
accu.error.push(user.email);
} else {
accu.success.push(user.email);
}
return accu;
},
{
success: [] as string[],
error: [] as string[],
},
const erroredInvites = invited.filter((invite) => invite.error);
const successfulEmailInvites = invited.filter(
(invite) => !invite.error && invite.user.emailSent,
);
const successfulUrlInvites = invited.filter(
(invite) => !invite.error && !invite.user.emailSent,
);
if (invitedEmails.success.length) {
if (successfulEmailInvites.length) {
this.$showMessage({
type: 'success',
title: this.$locale.baseText(
invitedEmails.success.length > 1
successfulEmailInvites.length > 1
? 'settings.users.usersInvited'
: 'settings.users.userInvited',
),
message: this.$locale.baseText('settings.users.emailInvitesSent', {
interpolate: { emails: invitedEmails.success.join(', ') },
interpolate: {
emails: successfulEmailInvites.map(({ user }) => user.email).join(', '),
},
}),
});
}
if (invitedEmails.error.length) {
if (successfulUrlInvites.length) {
if (successfulUrlInvites.length === 1) {
this.copyToClipboard(successfulUrlInvites[0].user.inviteAcceptUrl);
}
this.$showMessage({
type: 'success',
title: this.$locale.baseText(
successfulUrlInvites.length > 1
? 'settings.users.multipleInviteUrlsCreated'
: 'settings.users.inviteUrlCreated',
),
message: this.$locale.baseText(
successfulUrlInvites.length > 1
? 'settings.users.multipleInviteUrlsCreated.message'
: 'settings.users.inviteUrlCreated.message',
{
interpolate: {
emails: successfulUrlInvites.map(({ user }) => user.email).join(', '),
},
},
),
});
}
if (erroredInvites.length) {
setTimeout(() => {
this.$showMessage({
type: 'error',
title: this.$locale.baseText('settings.users.usersEmailedError'),
message: this.$locale.baseText('settings.users.emailInvitesSentError', {
interpolate: { emails: invitedEmails.error.join(', ') },
interpolate: { emails: erroredInvites.map(({ error }) => error).join(', ') },
}),
});
}, 0); // notifications stack on top of each other otherwise
}
this.modalBus.$emit('close');
if (successfulUrlInvites.length > 1) {
this.showInviteUrls = successfulUrlInvites;
} else {
this.modalBus.$emit('close');
}
} catch (error) {
this.$showError(error, this.$locale.baseText('settings.users.usersInvitedError'));
}
this.loading = false;
},
showCopyInviteLinkToast(successfulUrlInvites: IInviteResponse[]) {
this.$showMessage({
type: 'success',
title: this.$locale.baseText(
successfulUrlInvites.length > 1
? 'settings.users.multipleInviteUrlsCreated'
: 'settings.users.inviteUrlCreated',
),
message: this.$locale.baseText(
successfulUrlInvites.length > 1
? 'settings.users.multipleInviteUrlsCreated.message'
: 'settings.users.inviteUrlCreated.message',
{
interpolate: {
emails: successfulUrlInvites.map(({ user }) => user.email).join(', '),
},
},
),
});
},
onSubmitClick() {
this.formBus.$emit('submit');
},
onCopyInviteLink(user: IUser) {
if (user.inviteAcceptUrl && this.showInviteUrls) {
this.copyToClipboard(user.inviteAcceptUrl);
this.showCopyInviteLinkToast([]);
}
},
},
});
</script>