feat: Change desktop UM experience (#5312)

* refactor: Hide prompt for desktop

* feat: add email field to personalization modal

* fix: update survey interfaces

* chore: enable personalization survey email key display condition

* feat: add users page upsell for desktop client

* feat: disable UM on desktop where possible

* refactor: Have a single function to decide whether UM is enabled

* feat: update community nodes upsell link

---------

Co-authored-by: Alex Grozav <alex@grozav.com>
Co-authored-by: krynble <omar@n8n.io>
Co-authored-by: freyamade <freya@n8n.io>
This commit is contained in:
Omar Ajoue 2023-02-08 10:42:22 +01:00 committed by GitHub
parent d8865aa917
commit 5e3e70b83b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 177 additions and 64 deletions

View file

@ -571,6 +571,7 @@ export interface IN8nUISettings {
}
export interface IPersonalizationSurveyAnswers {
email: string | null;
codingSkill: string | null;
companyIndustry: string[];
companySize: string | null;

View file

@ -303,7 +303,8 @@ class Server extends AbstractServer {
showSetupOnFirstLoad:
config.getEnv('userManagement.disabled') === false &&
config.getEnv('userManagement.isInstanceOwnerSetUp') === false &&
config.getEnv('userManagement.skipInstanceOwnerSetup') === false,
config.getEnv('userManagement.skipInstanceOwnerSetup') === false &&
config.getEnv('deployment.type').startsWith('desktop_') === false,
});
// refresh enterprise status

View file

@ -37,10 +37,20 @@ export function isEmailSetUp(): boolean {
}
export function isUserManagementEnabled(): boolean {
return (
!config.getEnv('userManagement.disabled') ||
config.getEnv('userManagement.isInstanceOwnerSetUp')
);
// This can be simplified but readability is more important here
if (config.getEnv('userManagement.isInstanceOwnerSetUp')) {
// Short circuit - if owner is set up, UM cannot be disabled.
// Users must reset their instance in order to do so.
return true;
}
// UM is disabled for desktop by default
if (config.getEnv('deployment.type').startsWith('desktop_')) {
return false;
}
return config.getEnv('userManagement.disabled') ? false : true;
}
export function isSharingEnabled(): boolean {
@ -51,13 +61,6 @@ export function isSharingEnabled(): boolean {
);
}
export function isUserManagementDisabled(): boolean {
return (
config.getEnv('userManagement.disabled') &&
!config.getEnv('userManagement.isInstanceOwnerSetUp')
);
}
export async function getRoleId(scope: Role['scope'], name: Role['name']): Promise<Role['id']> {
return Db.collections.Role.findOneOrFail({
select: ['id'],

View file

@ -13,7 +13,7 @@ import {
getInstanceBaseUrl,
hashPassword,
isEmailSetUp,
isUserManagementDisabled,
isUserManagementEnabled,
sanitizeUser,
validatePassword,
} from '@/UserManagement/UserManagementHelper';
@ -94,7 +94,7 @@ export class UsersController {
@Post('/')
async sendEmailInvites(req: UserRequest.Invite) {
// TODO: this should be checked in the middleware rather than here
if (isUserManagementDisabled()) {
if (!isUserManagementEnabled()) {
this.logger.debug(
'Request to send email invite(s) to user(s) failed because user management is disabled',
);

View file

@ -13,7 +13,7 @@ import {
isAuthenticatedRequest,
isAuthExcluded,
isPostUsersId,
isUserManagementDisabled,
isUserManagementEnabled,
} from '@/UserManagement/UserManagementHelper';
import type { Repository } from 'typeorm';
import type { User } from '@db/entities/User';
@ -101,7 +101,7 @@ export const setupAuthMiddlewares = (
}
// skip authentication if user management is disabled
if (isUserManagementDisabled()) {
if (!isUserManagementEnabled()) {
req.user = await userRepository.findOneOrFail({
relations: ['globalRole'],
where: {},

View file

@ -106,6 +106,7 @@ export default Vue.extend({
.heading {
margin-bottom: var(--spacing-l);
text-align: center;
}
.description {

View file

@ -509,6 +509,7 @@ export type IPersonalizationSurveyAnswersV3 = {
automationGoalSm?: string[] | null;
automationGoalSmOther?: string | null;
usageModes?: string[] | null;
email?: string | null;
};
export type IPersonalizationLatestVersion = IPersonalizationSurveyAnswersV3;

View file

@ -89,6 +89,7 @@ import { useUIStore } from '@/stores/ui';
import { useCredentialsStore } from '@/stores/credentials';
import { useUsageStore } from '@/stores/usage';
import { EnterpriseEditionFeature, VIEWS } from '@/constants';
import { BaseTextKey } from '@/plugins/i18n';
export default mixins(showMessage).extend({
name: 'CredentialSharing',
@ -179,9 +180,14 @@ export default mixins(showMessage).extend({
this.modalBus.$emit('close');
},
goToUpgrade() {
let linkUrl = this.$locale.baseText(this.uiStore.contextBasedTranslationKeys.upgradeLinkUrl);
if (linkUrl.includes('subscription')) {
const linkUrlTranslationKey = this.uiStore.contextBasedTranslationKeys
.upgradeLinkUrl as BaseTextKey;
let linkUrl = this.$locale.baseText(linkUrlTranslationKey);
if (linkUrlTranslationKey.endsWith('.upgradeLinkUrl')) {
linkUrl = `${this.usageStore.viewPlansUrl}&source=credential_sharing`;
} else if (linkUrlTranslationKey.endsWith('.desktop')) {
linkUrl = `${linkUrl}&utm_campaign=upgrade-credentials-sharing`;
}
window.open(linkUrl, '_blank');

View file

@ -1,5 +1,5 @@
<template>
<div v-if="this.featureInfo" :class="[$style.container]">
<div v-if="featureInfo" :class="[$style.container]">
<div v-if="showTitle" class="mb-2xl">
<n8n-heading size="2xlarge">
{{ $locale.baseText(featureInfo.featureName) }}

View file

@ -154,6 +154,7 @@ import { useTagsStore } from '@/stores/tags';
import { getWorkflowPermissions, IPermissions } from '@/permissions';
import { useUsersStore } from '@/stores/users';
import { useUsageStore } from '@/stores/usage';
import { BaseTextKey } from '@/plugins/i18n';
const hasChanged = (prev: string[], curr: string[]) => {
if (prev.length !== curr.length) {
@ -522,9 +523,14 @@ export default mixins(workflowHelpers, titleChange).extend({
}
},
goToUpgrade() {
let linkUrl = this.$locale.baseText(this.contextBasedTranslationKeys.upgradeLinkUrl);
if (linkUrl.includes('subscription')) {
const linkUrlTranslationKey = this.uiStore.contextBasedTranslationKeys
.upgradeLinkUrl as BaseTextKey;
let linkUrl = this.$locale.baseText(linkUrlTranslationKey);
if (linkUrlTranslationKey.endsWith('.upgradeLinkUrl')) {
linkUrl = `${this.usageStore.viewPlansUrl}&source=workflow_sharing`;
} else if (linkUrlTranslationKey.endsWith('.desktop')) {
linkUrl = `${linkUrl}&utm_campaign=upgrade-workflow-sharing`;
}
window.open(linkUrl, '_blank');

View file

@ -92,6 +92,7 @@ import {
NOT_SURE_YET_GOAL,
AUTOMATION_GOAL_OTHER_KEY,
COMPANY_TYPE_KEY,
EMAIL_KEY,
SAAS_COMPANY_TYPE,
ECOMMERCE_COMPANY_TYPE,
MSP_INDUSTRY,
@ -155,6 +156,16 @@ export default mixins(showMessage, workflowHelpers).extend({
...mapStores(useRootStore, useSettingsStore, useUIStore, useUsersStore),
survey() {
const survey: IFormInputs = [
{
name: EMAIL_KEY,
properties: {
label: this.$locale.baseText('personalizationModal.yourEmailAddress'),
type: 'text',
placeholder: this.$locale.baseText('personalizationModal.email'),
},
shouldDisplay: () =>
this.settingsStore.isDesktopDeployment && !this.usersStore.currentUser?.firstName,
},
{
name: COMPANY_TYPE_KEY,
properties: {

View file

@ -115,18 +115,6 @@
>
{{ $locale.baseText('workflows.shareModal.save') }}
</n8n-button>
<template #fallback>
<n8n-link :to="uiStore.contextBasedTranslationKeys.workflows.sharing.unavailable.linkUrl">
<n8n-button :loading="loading" size="medium">
{{
$locale.baseText(
uiStore.contextBasedTranslationKeys.workflows.sharing.unavailable.button,
)
}}
</n8n-button>
</n8n-link>
</template>
</enterprise-edition>
</template>
</Modal>
@ -198,6 +186,11 @@ export default mixins(showMessage).extend({
isSharingEnabled(): boolean {
return this.settingsStore.isEnterpriseFeatureEnabled(EnterpriseEditionFeature.Sharing);
},
fallbackLinkUrl(): string {
return `${this.$locale.baseText(
this.uiStore.contextBasedTranslationKeys.upgradeLinkUrl as BaseTextKey,
)}${true ? '&utm_campaign=upgrade-workflow-sharing' : ''}`;
},
modalTitle(): string {
return this.$locale.baseText(
this.isSharingEnabled
@ -444,11 +437,14 @@ export default mixins(showMessage).extend({
});
},
goToUpgrade() {
let linkUrl = this.$locale.baseText(
this.uiStore.contextBasedTranslationKeys.upgradeLinkUrl as BaseTextKey,
);
if (linkUrl.includes('subscription')) {
const linkUrlTranslationKey = this.uiStore.contextBasedTranslationKeys
.upgradeLinkUrl as BaseTextKey;
let linkUrl = this.$locale.baseText(linkUrlTranslationKey);
if (linkUrlTranslationKey.endsWith('.upgradeLinkUrl')) {
linkUrl = `${this.usageStore.viewPlansUrl}&source=workflow_sharing`;
} else if (linkUrlTranslationKey.endsWith('.desktop')) {
linkUrl = `${linkUrl}&utm_campaign=upgrade-workflow-sharing`;
}
window.open(linkUrl, '_blank');

View file

@ -176,6 +176,7 @@ export const INSTANCE_ID_HEADER = 'n8n-instance-id';
export const WAIT_TIME_UNLIMITED = '3000-01-01T00:00:00.000Z';
/** PERSONALIZATION SURVEY */
export const EMAIL_KEY = 'email';
export const WORK_AREA_KEY = 'workArea';
export const FINANCE_WORK_AREA = 'finance';
export const IT_ENGINEERING_WORK_AREA = 'IT-Engineering';

View file

@ -513,10 +513,6 @@
"fakeDoor.settings.sso.actionBox.title": "Were working on this (as a paid feature)",
"fakeDoor.settings.sso.actionBox.title.cloud": "Were working on this",
"fakeDoor.settings.sso.actionBox.description": "SSO will offer a secured and convenient way to access n8n using your existing credentials (Google, Github, Keycloak…)",
"fakeDoor.settings.users.name": "Users",
"fakeDoor.settings.users.actionBox.title": "Upgrade to add users",
"fakeDoor.settings.users.actionBox.description": "Create multiple users on our higher plans and share workflows and credentials to collaborate",
"fakeDoor.settings.users.actionBox.button": "Upgrade now",
"fakeDoor.actionBox.button.label": "Join the list",
"fixedCollectionParameter.choose": "Choose...",
"fixedCollectionParameter.currentlyNoItemsExist": "Currently no items exist",
@ -949,6 +945,8 @@
"personalizationModal.leadGeneration": "Lead generation, enrichment, routing",
"personalizationModal.customerCommunication": "Customer communication",
"personalizationModal.customerActions": "Actions when lead changes status",
"personalizationModal.yourEmailAddress": "Your email address",
"personalizationModal.email": "Enter your email..",
"personalizationModal.adCampaign": "Ad campaign management",
"personalizationModal.reporting": "Reporting",
"personalizationModal.dataSynching": "Data syncing",
@ -1552,9 +1550,24 @@
"contextual.workflows.sharing.unavailable.button": "View plans",
"contextual.workflows.sharing.unavailable.button.cloud": "Upgrade now",
"contextual.workflows.sharing.unavailable.button.desktop": "View plans",
"contextual.users.settings.unavailable.title": "Upgrade to add users",
"contextual.users.settings.unavailable.title.cloud": "Upgrade to add users",
"contextual.users.settings.unavailable.title.desktop": "Upgrade to add users",
"contextual.users.settings.unavailable.description": "Create multiple users on our higher plans and share workflows and credentials to collaborate",
"contextual.users.settings.unavailable.description.cloud": "Create multiple users on our higher plans and share workflows and credentials to collaborate",
"contextual.users.settings.unavailable.description.desktop": "Create multiple users on our higher plans and share workflows and credentials to collaborate",
"contextual.users.settings.unavailable.button": "View plans",
"contextual.users.settings.unavailable.button.cloud": "Upgrade now",
"contextual.users.settings.unavailable.button.desktop": "View plans",
"contextual.communityNodes.unavailable.description.desktop": "Community nodes feature is unavailable on desktop. Please choose one of our available self-hosting plans.",
"contextual.communityNodes.unavailable.button.desktop": "View plans",
"contextual.upgradeLinkUrl": "https://subscription.n8n.io/",
"contextual.upgradeLinkUrl.cloud": "https://app.n8n.cloud/manage?edition=cloud",
"contextual.upgradeLinkUrl.desktop": "https://n8n.io/pricing",
"contextual.upgradeLinkUrl.desktop": "https://n8n.io/pricing/?utm_source=n8n-internal&utm_medium=desktop",
"settings.ldap": "LDAP",
"settings.ldap.infoTip": "Learn more about <a href='https://docs.n8n.io/user-management/ldap/' target='_blank'>LDAP in the Docs</a>",
"settings.ldap.save": "Save connection",

View file

@ -35,6 +35,7 @@ import { EnterpriseEditionFeature, VIEWS } from './constants';
import { useSettingsStore } from './stores/settings';
import { useTemplatesStore } from './stores/templates';
import SettingsUsageAndPlanVue from './views/SettingsUsageAndPlan.vue';
import { useUsersStore } from '@/stores/users';
Vue.use(Router);
@ -507,7 +508,11 @@ const router = new Router({
deny: {
shouldDeny: () => {
const settingsStore = useSettingsStore();
return settingsStore.isUserManagementEnabled === false;
return (
settingsStore.isUserManagementEnabled === false &&
!(settingsStore.isCloudDeployment || settingsStore.isDesktopDeployment)
);
},
},
},

View file

@ -207,6 +207,15 @@ export const useUIStore = defineStore(STORES.UI, {
},
},
},
users: {
settings: {
unavailable: {
title: `contextual.users.settings.unavailable.title${contextKey}`,
description: `contextual.users.settings.unavailable.description${contextKey}`,
button: `contextual.users.settings.unavailable.button${contextKey}`,
},
},
},
};
},
getLastSelectedNode(): INodeUi | null {

View file

@ -19,11 +19,12 @@ import {
validatePasswordToken,
validateSignupToken,
} from '@/api/users';
import { EnterpriseEditionFeature, PERSONALIZATION_MODAL_KEY, STORES } from '@/constants';
import {
import { PERSONALIZATION_MODAL_KEY, STORES } from '@/constants';
import type {
ICredentialsResponse,
IInviteResponse,
IPersonalizationLatestVersion,
IRole,
IUser,
IUserResponse,
IUsersState,
@ -41,6 +42,9 @@ const isDefaultUser = (user: IUserResponse | null) =>
const isPendingUser = (user: IUserResponse | null) => Boolean(user && user.isPending);
const isInstanceOwner = (user: IUserResponse | null) =>
Boolean(user?.globalRole?.name === ROLE.Owner);
export const useUsersStore = defineStore(STORES.USERS, {
state: (): IUsersState => ({
currentUserId: null,
@ -56,6 +60,9 @@ export const useUsersStore = defineStore(STORES.USERS, {
isDefaultUser(): boolean {
return isDefaultUser(this.currentUser);
},
isInstanceOwner(): boolean {
return isInstanceOwner(this.currentUser);
},
getUserById(state) {
return (userId: string): IUser | null => state.users[userId];
},

View file

@ -35,14 +35,10 @@
<n8n-action-box
:heading="$locale.baseText('settings.communityNodes.empty.title')"
:description="getEmptyStateDescription"
:buttonText="
shouldShowInstallButton
? $locale.baseText('settings.communityNodes.empty.installPackageLabel')
: ''
"
:buttonText="getEmptyStateButtonText"
:calloutText="actionBoxConfig.calloutText"
:calloutTheme="actionBoxConfig.calloutTheme"
@click="openInstallModal"
@click="onClickEmptyStateButton"
/>
</div>
<div :class="$style.cardsContainer" v-else>
@ -70,6 +66,7 @@ import { useCommunityNodesStore } from '@/stores/communityNodes';
import { useUIStore } from '@/stores/ui';
import { mapStores } from 'pinia';
import { useSettingsStore } from '@/stores/settings';
import { BaseTextKey } from '@/plugins/i18n';
const PACKAGE_COUNT_THRESHOLD = 31;
@ -129,8 +126,13 @@ export default mixins(showMessage).extend({
},
computed: {
...mapStores(useCommunityNodesStore, useSettingsStore, useUIStore),
getEmptyStateDescription() {
getEmptyStateDescription(): string {
const packageCount = this.communityNodesStore.availablePackageCount;
if (this.settingsStore.isDesktopDeployment) {
return this.$locale.baseText('contextual.communityNodes.unavailable.description.desktop');
}
return packageCount < PACKAGE_COUNT_THRESHOLD
? this.$locale.baseText('settings.communityNodes.empty.description.no-packages', {
interpolate: {
@ -144,18 +146,23 @@ export default mixins(showMessage).extend({
},
});
},
shouldShowInstallButton() {
return !this.settingsStore.isDesktopDeployment && this.settingsStore.isNpmAvailable;
},
actionBoxConfig() {
getEmptyStateButtonText(): string {
if (this.settingsStore.isDesktopDeployment) {
return {
calloutText: this.$locale.baseText('settings.communityNodes.notAvailableOnDesktop'),
calloutTheme: 'warning',
hideButton: true,
};
return this.$locale.baseText('contextual.communityNodes.unavailable.button.desktop');
}
return this.shouldShowInstallButton
? this.$locale.baseText('settings.communityNodes.empty.installPackageLabel')
: '';
},
shouldShowInstallButton(): boolean {
return this.settingsStore.isDesktopDeployment || this.settingsStore.isNpmAvailable;
},
actionBoxConfig(): {
calloutText: string;
calloutTheme: 'warning' | string;
hideButton: boolean;
} {
if (!this.settingsStore.isNpmAvailable) {
return {
calloutText: this.$locale.baseText('settings.communityNodes.npmUnavailable.warning', {
@ -184,7 +191,21 @@ export default mixins(showMessage).extend({
},
},
methods: {
openInstallModal(event: MouseEvent) {
onClickEmptyStateButton(): void {
if (this.settingsStore.isDesktopDeployment) {
return this.goToUpgrade();
}
this.openInstallModal();
},
goToUpgrade(): void {
const linkUrl = `${this.$locale.baseText(
'contextual.upgradeLinkUrl.desktop',
)}&utm_campaign=upgrade-community-nodes&selfHosted=true`;
window.open(linkUrl, '_blank');
},
openInstallModal(): void {
const telemetryPayload = {
is_empty_state: this.communityNodesStore.getInstalledPackages.length === 0,
};

View file

@ -10,7 +10,23 @@
/>
</div>
</div>
<div v-if="usersStore.showUMSetupWarning" :class="$style.setupInfoContainer">
<div v-if="!settingsStore.isUserManagementEnabled" :class="$style.setupInfoContainer">
<n8n-action-box
:heading="
$locale.baseText(uiStore.contextBasedTranslationKeys.users.settings.unavailable.title)
"
:description="
$locale.baseText(
uiStore.contextBasedTranslationKeys.users.settings.unavailable.description,
)
"
:buttonText="
$locale.baseText(uiStore.contextBasedTranslationKeys.users.settings.unavailable.button)
"
@click="goToUpgrade"
/>
</div>
<div v-else-if="usersStore.showUMSetupWarning" :class="$style.setupInfoContainer">
<n8n-action-box
:heading="$locale.baseText('settings.users.setupToInviteUsers')"
:buttonText="$locale.baseText('settings.users.setupMyAccount')"
@ -53,6 +69,8 @@ import { mapStores } from 'pinia';
import { useUIStore } from '@/stores/ui';
import { useSettingsStore } from '@/stores/settings';
import { useUsersStore } from '@/stores/users';
import { BaseTextKey } from '@/plugins/i18n';
import { useUsageStore } from '@/stores/usage';
export default mixins(showMessage, copyPaste).extend({
name: 'SettingsUsersView',
@ -66,7 +84,7 @@ export default mixins(showMessage, copyPaste).extend({
}
},
computed: {
...mapStores(useSettingsStore, useUIStore, useUsersStore),
...mapStores(useSettingsStore, useUIStore, useUsersStore, useUsageStore),
isSharingEnabled() {
return this.settingsStore.isEnterpriseFeatureEnabled(EnterpriseEditionFeature.Sharing);
},
@ -135,6 +153,19 @@ export default mixins(showMessage, copyPaste).extend({
});
}
},
goToUpgrade() {
const linkUrlTranslationKey = this.uiStore.contextBasedTranslationKeys
.upgradeLinkUrl as BaseTextKey;
let linkUrl = this.$locale.baseText(linkUrlTranslationKey);
if (linkUrlTranslationKey.endsWith('.upgradeLinkUrl')) {
linkUrl = `${this.usageStore.viewPlansUrl}&source=users`;
} else if (linkUrlTranslationKey.endsWith('.desktop')) {
linkUrl = `${linkUrl}&utm_campaign=upgrade-users`;
}
window.open(linkUrl, '_blank');
},
},
});
</script>