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 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', {
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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 });
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user