mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 18:12:04 +00:00
refactor(editor): Detangle users store from settings store (no-changelog) (#16510)
This commit is contained in:
@@ -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', {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user