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 { 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', {

View File

@@ -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();
}

View File

@@ -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,

View File

@@ -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<string | null>(null);
const usersById = ref<Record<string, IUser>>({});
const currentUserCloudInfo = ref<Cloud.UserAccount | null>(null);
const userQuota = ref<number>(-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,
};
});

View File

@@ -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 });

View File

@@ -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
</template>
<div>
<n8n-button
:disabled="ssoStore.isSamlLoginEnabled || !settingsStore.isBelowUserQuota"
:disabled="ssoStore.isSamlLoginEnabled || !usersStore.usersLimitNotReached"
:label="i18n.baseText('settings.users.invite')"
size="large"
data-test-id="settings-users-invite-button"
@@ -258,7 +258,7 @@ async function onRoleChange(user: IUser, newRoleName: UpdateGlobalRolePayload['n
</n8n-tooltip>
</div>
</div>
<div v-if="!settingsStore.isBelowUserQuota" :class="$style.setupInfoContainer">
<div v-if="!usersStore.usersLimitNotReached" :class="$style.setupInfoContainer">
<n8n-action-box
:heading="
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.
-->
<div
v-if="settingsStore.isBelowUserQuota || usersStore.allUsers.length > 1"
v-if="usersStore.usersLimitNotReached || usersStore.allUsers.length > 1"
:class="$style.usersContainer"
>
<n8n-users-list