refactor(editor): Detangle users store from settings store (no-changelog) (#16510)

This commit is contained in:
Alex Grozav
2025-06-20 13:01:44 +03:00
committed by GitHub
parent f598b3bf00
commit 4f4fa5e4af
6 changed files with 39 additions and 36 deletions

View File

@@ -4,8 +4,8 @@ import type { N8nPromptResponse } from '@n8n/rest-api-client/api/prompts';
import type { ModalKey } from '@/Interface'; import type { ModalKey } from '@/Interface';
import { VALID_EMAIL_REGEX } from '@/constants'; import { VALID_EMAIL_REGEX } from '@/constants';
import Modal from '@/components/Modal.vue'; import Modal from '@/components/Modal.vue';
import { useSettingsStore } from '@/stores/settings.store';
import { useRootStore } from '@n8n/stores/useRootStore'; import { useRootStore } from '@n8n/stores/useRootStore';
import { useUsersStore } from '@/stores/users.store';
import { createEventBus } from '@n8n/utils/event-bus'; import { createEventBus } from '@n8n/utils/event-bus';
import { useToast } from '@/composables/useToast'; import { useToast } from '@/composables/useToast';
import { useNpsSurveyStore } from '@/stores/npsSurvey.store'; import { useNpsSurveyStore } from '@/stores/npsSurvey.store';
@@ -20,7 +20,7 @@ const modalBus = createEventBus();
const npsSurveyStore = useNpsSurveyStore(); const npsSurveyStore = useNpsSurveyStore();
const rootStore = useRootStore(); const rootStore = useRootStore();
const settingsStore = useSettingsStore(); const usersStore = useUsersStore();
const toast = useToast(); const toast = useToast();
const telemetry = useTelemetry(); const telemetry = useTelemetry();
@@ -56,7 +56,7 @@ const closeDialog = () => {
const send = async () => { const send = async () => {
if (isEmailValid.value) { if (isEmailValid.value) {
const response = (await settingsStore.submitContactInfo(email.value)) as N8nPromptResponse; const response = (await usersStore.submitContactInfo(email.value)) as N8nPromptResponse;
if (response.updated) { if (response.updated) {
telemetry.track('User closed email modal', { telemetry.track('User closed email modal', {

View File

@@ -69,7 +69,9 @@ export async function initializeCore() {
void useExternalHooks().run('app.mount'); void useExternalHooks().run('app.mount');
if (!settingsStore.isPreviewMode) { if (!settingsStore.isPreviewMode) {
await usersStore.initialize(); await usersStore.initialize({
quota: settingsStore.userManagement.quota,
});
void versionsStore.checkForNewVersions(); void versionsStore.checkForNewVersions();
} }

View File

@@ -9,7 +9,6 @@ import type {
import * as eventsApi from '@n8n/rest-api-client/api/events'; import * as eventsApi from '@n8n/rest-api-client/api/events';
import * as settingsApi from '@n8n/rest-api-client/api/settings'; import * as settingsApi from '@n8n/rest-api-client/api/settings';
import * as moduleSettingsApi from '@n8n/rest-api-client/api/module-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 { testHealthEndpoint } from '@/api/templates';
import { import {
INSECURE_CONNECTION_WARNING, INSECURE_CONNECTION_WARNING,
@@ -21,7 +20,6 @@ import { UserManagementAuthenticationMethod } from '@/Interface';
import type { IDataObject, WorkflowSettings } from 'n8n-workflow'; import type { IDataObject, WorkflowSettings } from 'n8n-workflow';
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import { useRootStore } from '@n8n/stores/useRootStore'; import { useRootStore } from '@n8n/stores/useRootStore';
import { useUsersStore } from './users.store';
import { useVersionsStore } from './versions.store'; import { useVersionsStore } from './versions.store';
import { makeRestApiRequest } from '@n8n/rest-api-client'; import { makeRestApiRequest } from '@n8n/rest-api-client';
import { useToast } from '@/composables/useToast'; import { useToast } from '@/composables/useToast';
@@ -175,12 +173,6 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, () => {
const permanentlyDismissedBanners = computed(() => settings.value.banners?.dismissed ?? []); 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 isCommunityPlan = computed(() => planName.value.toLowerCase() === 'community');
const isDevRelease = computed(() => settings.value.releaseChannel === 'dev'); 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 testTemplatesEndpoint = async () => {
const timeout = new Promise((_, reject) => setTimeout(() => reject(), 2000)); const timeout = new Promise((_, reject) => setTimeout(() => reject(), 2000));
await Promise.race([testHealthEndpoint(templatesHost.value), timeout]); await Promise.race([testHealthEndpoint(templatesHost.value), timeout]);
@@ -405,7 +384,6 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, () => {
isWorkerViewAvailable, isWorkerViewAvailable,
workflowCallerPolicyDefaultOption, workflowCallerPolicyDefaultOption,
permanentlyDismissedBanners, permanentlyDismissedBanners,
isBelowUserQuota,
saveDataErrorExecution, saveDataErrorExecution,
saveDataSuccessExecution, saveDataSuccessExecution,
saveManualExecutions, saveManualExecutions,
@@ -420,7 +398,6 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, () => {
reset, reset,
getTimezones, getTimezones,
testTemplatesEndpoint, testTemplatesEndpoint,
submitContactInfo,
disableTemplates, disableTemplates,
stopShowingSetupPage, stopShowingSetupPage,
getSettings, getSettings,

View File

@@ -35,6 +35,7 @@ import { computed, ref } from 'vue';
import { useTelemetry } from '@/composables/useTelemetry'; import { useTelemetry } from '@/composables/useTelemetry';
import { useSettingsStore } from '@/stores/settings.store'; import { useSettingsStore } from '@/stores/settings.store';
import * as onboardingApi from '@/api/workflow-webhooks'; 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 _isPendingUser = (user: IUserResponse | null) => !!user?.isPending;
const _isInstanceOwner = (user: IUserResponse | null) => user?.role === ROLE.Owner; const _isInstanceOwner = (user: IUserResponse | null) => user?.role === ROLE.Owner;
@@ -46,6 +47,7 @@ export const useUsersStore = defineStore(STORES.USERS, () => {
const currentUserId = ref<string | null>(null); const currentUserId = ref<string | null>(null);
const usersById = ref<Record<string, IUser>>({}); const usersById = ref<Record<string, IUser>>({});
const currentUserCloudInfo = ref<Cloud.UserAccount | null>(null); const currentUserCloudInfo = ref<Cloud.UserAccount | null>(null);
const userQuota = ref<number>(-1);
// Stores // Stores
@@ -118,6 +120,10 @@ export const useUsersStore = defineStore(STORES.USERS, () => {
return getPersonalizedNodeTypes(answers); return getPersonalizedNodeTypes(answers);
}); });
const usersLimitNotReached = computed(
(): boolean => userQuota.value === -1 || userQuota.value > allUsers.value.length,
);
// Methods // Methods
const addUsers = (newUsers: User[]) => { const addUsers = (newUsers: User[]) => {
@@ -163,11 +169,15 @@ export const useUsersStore = defineStore(STORES.USERS, () => {
setCurrentUser(user); setCurrentUser(user);
}; };
const initialize = async () => { const initialize = async (options: { quota?: number } = {}) => {
if (initialized.value) { if (initialized.value) {
return; return;
} }
if (typeof options.quota !== 'undefined') {
userQuota.value = options.quota;
}
try { try {
await loginWithCookie(); await loginWithCookie();
initialized.value = true; initialized.value = true;
@@ -415,6 +425,18 @@ export const useUsersStore = defineStore(STORES.USERS, () => {
return null; return null;
}; };
const submitContactInfo = async (email: string) => {
try {
return await promptsApi.submitContactInfo(
rootStore.instanceId,
currentUserId.value ?? '',
email,
);
} catch (error) {
return;
}
};
return { return {
initialized, initialized,
currentUserId, currentUserId,
@@ -430,6 +452,7 @@ export const useUsersStore = defineStore(STORES.USERS, () => {
personalizedNodeTypes, personalizedNodeTypes,
userClaimedAiCredits, userClaimedAiCredits,
isEasyAIWorkflowOnboardingDone, isEasyAIWorkflowOnboardingDone,
usersLimitNotReached,
addUsers, addUsers,
loginWithCookie, loginWithCookie,
initialize, initialize,
@@ -467,5 +490,6 @@ export const useUsersStore = defineStore(STORES.USERS, () => {
isCalloutDismissed, isCalloutDismissed,
setCalloutDismissed, setCalloutDismissed,
submitContactEmail, submitContactEmail,
submitContactInfo,
}; };
}); });

View File

@@ -102,8 +102,8 @@ describe('SettingsUsersView', () => {
describe('Below quota', () => { describe('Below quota', () => {
const pinia = createTestingPinia({ initialState: getInitialState() }); const pinia = createTestingPinia({ initialState: getInitialState() });
const settingsStore = mockedStore(useSettingsStore); const usersStore = mockedStore(useUsersStore);
settingsStore.isBelowUserQuota = false; usersStore.usersLimitNotReached = false;
it('disables the invite button', async () => { it('disables the invite button', async () => {
const { getByTestId } = renderView({ pinia }); const { getByTestId } = renderView({ pinia });

View File

@@ -44,13 +44,13 @@ const usersListActions = computed((): IUserListAction[] => {
{ {
label: i18n.baseText('settings.users.actions.copyInviteLink'), label: i18n.baseText('settings.users.actions.copyInviteLink'),
value: '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'), label: i18n.baseText('settings.users.actions.reinvite'),
value: 'reinvite', value: 'reinvite',
guard: (user) => guard: (user) =>
settingsStore.isBelowUserQuota && !user.firstName && settingsStore.isSmtpSetup, usersStore.usersLimitNotReached && !user.firstName && settingsStore.isSmtpSetup,
}, },
{ {
label: i18n.baseText('settings.users.actions.delete'), label: i18n.baseText('settings.users.actions.delete'),
@@ -64,7 +64,7 @@ const usersListActions = computed((): IUserListAction[] => {
value: 'copyPasswordResetLink', value: 'copyPasswordResetLink',
guard: (user) => guard: (user) =>
hasPermission(['rbac'], { rbac: { scope: 'user:resetPassword' } }) && hasPermission(['rbac'], { rbac: { scope: 'user:resetPassword' } }) &&
settingsStore.isBelowUserQuota && usersStore.usersLimitNotReached &&
!user.isPendingUser && !user.isPendingUser &&
user.id !== usersStore.currentUserId, user.id !== usersStore.currentUserId,
}, },
@@ -248,7 +248,7 @@ async function onRoleChange(user: IUser, newRoleName: UpdateGlobalRolePayload['n
</template> </template>
<div> <div>
<n8n-button <n8n-button
:disabled="ssoStore.isSamlLoginEnabled || !settingsStore.isBelowUserQuota" :disabled="ssoStore.isSamlLoginEnabled || !usersStore.usersLimitNotReached"
:label="i18n.baseText('settings.users.invite')" :label="i18n.baseText('settings.users.invite')"
size="large" size="large"
data-test-id="settings-users-invite-button" data-test-id="settings-users-invite-button"
@@ -258,7 +258,7 @@ async function onRoleChange(user: IUser, newRoleName: UpdateGlobalRolePayload['n
</n8n-tooltip> </n8n-tooltip>
</div> </div>
</div> </div>
<div v-if="!settingsStore.isBelowUserQuota" :class="$style.setupInfoContainer"> <div v-if="!usersStore.usersLimitNotReached" :class="$style.setupInfoContainer">
<n8n-action-box <n8n-action-box
:heading=" :heading="
i18n.baseText(uiStore.contextBasedTranslationKeys.users.settings.unavailable.title) i18n.baseText(uiStore.contextBasedTranslationKeys.users.settings.unavailable.title)
@@ -284,7 +284,7 @@ async function onRoleChange(user: IUser, newRoleName: UpdateGlobalRolePayload['n
<!-- If there's more than 1 user it means the account quota was more than 1 in the past. So we need to allow instance owner to be able to delete users and transfer workflows. <!-- If there's more than 1 user it means the account quota was more than 1 in the past. So we need to allow instance owner to be able to delete users and transfer workflows.
--> -->
<div <div
v-if="settingsStore.isBelowUserQuota || usersStore.allUsers.length > 1" v-if="usersStore.usersLimitNotReached || usersStore.allUsers.length > 1"
:class="$style.usersContainer" :class="$style.usersContainer"
> >
<n8n-users-list <n8n-users-list