diff --git a/packages/frontend/editor-ui/src/components/ContactPromptModal.vue b/packages/frontend/editor-ui/src/components/ContactPromptModal.vue index 0c211ec9005..d7bbc853424 100644 --- a/packages/frontend/editor-ui/src/components/ContactPromptModal.vue +++ b/packages/frontend/editor-ui/src/components/ContactPromptModal.vue @@ -4,8 +4,8 @@ import type { N8nPromptResponse } from '@n8n/rest-api-client/api/prompts'; import type { ModalKey } from '@/Interface'; import { VALID_EMAIL_REGEX } from '@/constants'; import Modal from '@/components/Modal.vue'; -import { useSettingsStore } from '@/stores/settings.store'; import { useRootStore } from '@n8n/stores/useRootStore'; +import { useUsersStore } from '@/stores/users.store'; import { createEventBus } from '@n8n/utils/event-bus'; import { useToast } from '@/composables/useToast'; import { useNpsSurveyStore } from '@/stores/npsSurvey.store'; @@ -20,7 +20,7 @@ const modalBus = createEventBus(); const npsSurveyStore = useNpsSurveyStore(); const rootStore = useRootStore(); -const settingsStore = useSettingsStore(); +const usersStore = useUsersStore(); const toast = useToast(); const telemetry = useTelemetry(); @@ -56,7 +56,7 @@ const closeDialog = () => { const send = async () => { if (isEmailValid.value) { - const response = (await settingsStore.submitContactInfo(email.value)) as N8nPromptResponse; + const response = (await usersStore.submitContactInfo(email.value)) as N8nPromptResponse; if (response.updated) { telemetry.track('User closed email modal', { diff --git a/packages/frontend/editor-ui/src/init.ts b/packages/frontend/editor-ui/src/init.ts index 65561756dc5..9d5ef447d9d 100644 --- a/packages/frontend/editor-ui/src/init.ts +++ b/packages/frontend/editor-ui/src/init.ts @@ -69,7 +69,9 @@ export async function initializeCore() { void useExternalHooks().run('app.mount'); if (!settingsStore.isPreviewMode) { - await usersStore.initialize(); + await usersStore.initialize({ + quota: settingsStore.userManagement.quota, + }); void versionsStore.checkForNewVersions(); } diff --git a/packages/frontend/editor-ui/src/stores/settings.store.ts b/packages/frontend/editor-ui/src/stores/settings.store.ts index 8edae8fbdab..bfad85dd1aa 100644 --- a/packages/frontend/editor-ui/src/stores/settings.store.ts +++ b/packages/frontend/editor-ui/src/stores/settings.store.ts @@ -9,7 +9,6 @@ import type { import * as eventsApi from '@n8n/rest-api-client/api/events'; import * as settingsApi from '@n8n/rest-api-client/api/settings'; import * as moduleSettingsApi from '@n8n/rest-api-client/api/module-settings'; -import * as promptsApi from '@n8n/rest-api-client/api/prompts'; import { testHealthEndpoint } from '@/api/templates'; import { INSECURE_CONNECTION_WARNING, @@ -21,7 +20,6 @@ import { UserManagementAuthenticationMethod } from '@/Interface'; import type { IDataObject, WorkflowSettings } from 'n8n-workflow'; import { defineStore } from 'pinia'; import { useRootStore } from '@n8n/stores/useRootStore'; -import { useUsersStore } from './users.store'; import { useVersionsStore } from './versions.store'; import { makeRestApiRequest } from '@n8n/rest-api-client'; import { useToast } from '@/composables/useToast'; @@ -175,12 +173,6 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, () => { const permanentlyDismissedBanners = computed(() => settings.value.banners?.dismissed ?? []); - const isBelowUserQuota = computed( - (): boolean => - userManagement.value.quota === -1 || - userManagement.value.quota > useUsersStore().allUsers.length, - ); - const isCommunityPlan = computed(() => planName.value.toLowerCase() === 'community'); const isDevRelease = computed(() => settings.value.releaseChannel === 'dev'); @@ -306,19 +298,6 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, () => { }; }; - const submitContactInfo = async (email: string) => { - try { - const usersStore = useUsersStore(); - return await promptsApi.submitContactInfo( - settings.value.instanceId, - usersStore.currentUserId || '', - email, - ); - } catch (error) { - return; - } - }; - const testTemplatesEndpoint = async () => { const timeout = new Promise((_, reject) => setTimeout(() => reject(), 2000)); await Promise.race([testHealthEndpoint(templatesHost.value), timeout]); @@ -405,7 +384,6 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, () => { isWorkerViewAvailable, workflowCallerPolicyDefaultOption, permanentlyDismissedBanners, - isBelowUserQuota, saveDataErrorExecution, saveDataSuccessExecution, saveManualExecutions, @@ -420,7 +398,6 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, () => { reset, getTimezones, testTemplatesEndpoint, - submitContactInfo, disableTemplates, stopShowingSetupPage, getSettings, diff --git a/packages/frontend/editor-ui/src/stores/users.store.ts b/packages/frontend/editor-ui/src/stores/users.store.ts index 67db319d234..7b78d38ae2b 100644 --- a/packages/frontend/editor-ui/src/stores/users.store.ts +++ b/packages/frontend/editor-ui/src/stores/users.store.ts @@ -35,6 +35,7 @@ import { computed, ref } from 'vue'; import { useTelemetry } from '@/composables/useTelemetry'; import { useSettingsStore } from '@/stores/settings.store'; import * as onboardingApi from '@/api/workflow-webhooks'; +import * as promptsApi from '@n8n/rest-api-client/api/prompts'; const _isPendingUser = (user: IUserResponse | null) => !!user?.isPending; const _isInstanceOwner = (user: IUserResponse | null) => user?.role === ROLE.Owner; @@ -46,6 +47,7 @@ export const useUsersStore = defineStore(STORES.USERS, () => { const currentUserId = ref(null); const usersById = ref>({}); const currentUserCloudInfo = ref(null); + const userQuota = ref(-1); // Stores @@ -118,6 +120,10 @@ export const useUsersStore = defineStore(STORES.USERS, () => { return getPersonalizedNodeTypes(answers); }); + const usersLimitNotReached = computed( + (): boolean => userQuota.value === -1 || userQuota.value > allUsers.value.length, + ); + // Methods const addUsers = (newUsers: User[]) => { @@ -163,11 +169,15 @@ export const useUsersStore = defineStore(STORES.USERS, () => { setCurrentUser(user); }; - const initialize = async () => { + const initialize = async (options: { quota?: number } = {}) => { if (initialized.value) { return; } + if (typeof options.quota !== 'undefined') { + userQuota.value = options.quota; + } + try { await loginWithCookie(); initialized.value = true; @@ -415,6 +425,18 @@ export const useUsersStore = defineStore(STORES.USERS, () => { return null; }; + const submitContactInfo = async (email: string) => { + try { + return await promptsApi.submitContactInfo( + rootStore.instanceId, + currentUserId.value ?? '', + email, + ); + } catch (error) { + return; + } + }; + return { initialized, currentUserId, @@ -430,6 +452,7 @@ export const useUsersStore = defineStore(STORES.USERS, () => { personalizedNodeTypes, userClaimedAiCredits, isEasyAIWorkflowOnboardingDone, + usersLimitNotReached, addUsers, loginWithCookie, initialize, @@ -467,5 +490,6 @@ export const useUsersStore = defineStore(STORES.USERS, () => { isCalloutDismissed, setCalloutDismissed, submitContactEmail, + submitContactInfo, }; }); diff --git a/packages/frontend/editor-ui/src/views/SettingsUsersView.test.ts b/packages/frontend/editor-ui/src/views/SettingsUsersView.test.ts index 49d276286cd..8e8d59af3ec 100644 --- a/packages/frontend/editor-ui/src/views/SettingsUsersView.test.ts +++ b/packages/frontend/editor-ui/src/views/SettingsUsersView.test.ts @@ -102,8 +102,8 @@ describe('SettingsUsersView', () => { describe('Below quota', () => { const pinia = createTestingPinia({ initialState: getInitialState() }); - const settingsStore = mockedStore(useSettingsStore); - settingsStore.isBelowUserQuota = false; + const usersStore = mockedStore(useUsersStore); + usersStore.usersLimitNotReached = false; it('disables the invite button', async () => { const { getByTestId } = renderView({ pinia }); diff --git a/packages/frontend/editor-ui/src/views/SettingsUsersView.vue b/packages/frontend/editor-ui/src/views/SettingsUsersView.vue index 587d5cf32bb..7f54f512bc4 100644 --- a/packages/frontend/editor-ui/src/views/SettingsUsersView.vue +++ b/packages/frontend/editor-ui/src/views/SettingsUsersView.vue @@ -44,13 +44,13 @@ const usersListActions = computed((): IUserListAction[] => { { label: i18n.baseText('settings.users.actions.copyInviteLink'), value: 'copyInviteLink', - guard: (user) => settingsStore.isBelowUserQuota && !user.firstName && !!user.inviteAcceptUrl, + guard: (user) => usersStore.usersLimitNotReached && !user.firstName && !!user.inviteAcceptUrl, }, { label: i18n.baseText('settings.users.actions.reinvite'), value: 'reinvite', guard: (user) => - settingsStore.isBelowUserQuota && !user.firstName && settingsStore.isSmtpSetup, + usersStore.usersLimitNotReached && !user.firstName && settingsStore.isSmtpSetup, }, { label: i18n.baseText('settings.users.actions.delete'), @@ -64,7 +64,7 @@ const usersListActions = computed((): IUserListAction[] => { value: 'copyPasswordResetLink', guard: (user) => hasPermission(['rbac'], { rbac: { scope: 'user:resetPassword' } }) && - settingsStore.isBelowUserQuota && + usersStore.usersLimitNotReached && !user.isPendingUser && user.id !== usersStore.currentUserId, }, @@ -248,7 +248,7 @@ async function onRoleChange(user: IUser, newRoleName: UpdateGlobalRolePayload['n
-
+
1" + v-if="usersStore.usersLimitNotReached || usersStore.allUsers.length > 1" :class="$style.usersContainer" >