mirror of
https://github.com/n8n-io/n8n.git
synced 2026-07-28 03:24:59 +02:00
fix(editor): Restore agent channel credential setup (backport to release-candidate/2.32.x) (#34757)
Co-authored-by: Michael Drury <me@michaeldrury.co.uk>
This commit is contained in:
parent
e985cbac2a
commit
53e48ee8b0
|
|
@ -0,0 +1,100 @@
|
|||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { useAgentChannelSetup } from '../composables/useAgentChannelSetup';
|
||||
|
||||
const {
|
||||
fetchAllCredentialsForWorkflowMock,
|
||||
fetchProjectMock,
|
||||
projectsStoreMock,
|
||||
setCredentialsMock,
|
||||
} = vi.hoisted(() => ({
|
||||
fetchAllCredentialsForWorkflowMock: vi.fn(),
|
||||
fetchProjectMock: vi.fn(),
|
||||
projectsStoreMock: {
|
||||
currentProject: null as { id: string; scopes?: string[] } | null,
|
||||
personalProject: null as { id: string; scopes?: string[] } | null,
|
||||
myProjects: [] as Array<{ id: string; scopes?: string[] }>,
|
||||
fetchProject: vi.fn(),
|
||||
},
|
||||
setCredentialsMock: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('@n8n/stores/useRootStore', () => ({
|
||||
useRootStore: () => ({ restApiContext: {} }),
|
||||
}));
|
||||
|
||||
vi.mock('@/app/stores/ui.store', () => ({
|
||||
useUIStore: () => ({
|
||||
isModalActiveById: {},
|
||||
openNewCredential: vi.fn(),
|
||||
openExistingCredential: vi.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('@/features/credentials/credentials.store', () => ({
|
||||
useCredentialsStore: () => ({
|
||||
setCredentials: setCredentialsMock,
|
||||
fetchAllCredentialsForWorkflow: fetchAllCredentialsForWorkflowMock,
|
||||
getCredentialTypeByName: vi.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('@/features/collaboration/projects/projects.store', () => ({
|
||||
useProjectsStore: () => projectsStoreMock,
|
||||
}));
|
||||
|
||||
function createChannelSetup() {
|
||||
return useAgentChannelSetup({
|
||||
projectId: () => 'artifact-project',
|
||||
agentId: () => 'agent-1',
|
||||
currentIntegration: null,
|
||||
connectedCredentials: {},
|
||||
fetchStatus: vi.fn().mockResolvedValue(undefined),
|
||||
isIntegrationConnected: () => false,
|
||||
});
|
||||
}
|
||||
|
||||
describe('useAgentChannelSetup', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
projectsStoreMock.currentProject = null;
|
||||
projectsStoreMock.personalProject = null;
|
||||
projectsStoreMock.myProjects = [];
|
||||
projectsStoreMock.fetchProject = fetchProjectMock;
|
||||
fetchAllCredentialsForWorkflowMock.mockResolvedValue([]);
|
||||
fetchProjectMock.mockResolvedValue({
|
||||
id: 'artifact-project',
|
||||
name: 'Artifact project',
|
||||
icon: null,
|
||||
type: 'team',
|
||||
description: null,
|
||||
createdAt: '2026-01-01T00:00:00.000Z',
|
||||
updatedAt: '2026-01-01T00:00:00.000Z',
|
||||
relations: [],
|
||||
scopes: ['credential:create'],
|
||||
rolesManaged: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('uses project scopes already available in the store', async () => {
|
||||
projectsStoreMock.myProjects = [{ id: 'artifact-project', scopes: ['credential:create'] }];
|
||||
const { credentialPermissions, loadChannelState } = createChannelSetup();
|
||||
|
||||
await loadChannelState([]);
|
||||
|
||||
expect(credentialPermissions.value.create).toBe(true);
|
||||
expect(fetchProjectMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('loads scopes when an artifact project is missing from the store', async () => {
|
||||
// AGENT-443: artifact mode supplies its project through props rather than the route.
|
||||
const { credentialPermissions, loadChannelState } = createChannelSetup();
|
||||
|
||||
expect(credentialPermissions.value.create).toBe(false);
|
||||
|
||||
await loadChannelState([]);
|
||||
|
||||
expect(fetchProjectMock).toHaveBeenCalledWith('artifact-project');
|
||||
expect(credentialPermissions.value.create).toBe(true);
|
||||
});
|
||||
});
|
||||
|
|
@ -11,6 +11,7 @@ import {
|
|||
} from '@n8n/design-system';
|
||||
import type { IconName } from '@n8n/design-system/components/N8nIcon/icons';
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
import { FocusScope } from 'reka-ui';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { useAgentChannelSetup } from '../composables/useAgentChannelSetup';
|
||||
import { useAgentIntegrationStatus } from '../composables/useAgentIntegrationStatus';
|
||||
|
|
@ -245,6 +246,15 @@ watch(
|
|||
@interact-outside="(e) => e.preventDefault()"
|
||||
@update:open="$emit('update:open', $event)"
|
||||
>
|
||||
<FocusScope
|
||||
v-if="credentialModalOpen"
|
||||
as-child
|
||||
@mount-auto-focus.prevent
|
||||
@unmount-auto-focus.prevent
|
||||
>
|
||||
<span hidden aria-hidden="true" />
|
||||
</FocusScope>
|
||||
|
||||
<N8nDialogHeader :class="$style.customHeader">
|
||||
<Transition name="channel-header-fade" mode="out-in">
|
||||
<div v-if="currentView === 'list'" key="list" :class="$style.headerContent">
|
||||
|
|
@ -277,7 +287,7 @@ watch(
|
|||
</Transition>
|
||||
</N8nDialogHeader>
|
||||
|
||||
<div :class="$style.container">
|
||||
<div data-testid="agent-channel-modal" :class="$style.container">
|
||||
<Transition name="channel-view-fade" mode="out-in">
|
||||
<div v-if="currentView === 'list'" key="list" :class="$style.listView">
|
||||
<ul :class="$style.channelList">
|
||||
|
|
@ -467,6 +477,13 @@ watch(
|
|||
</N8nDialog>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
body:has([data-testid='agent-channel-modal'])
|
||||
.el-overlay:has([data-test-id='editCredential-modal']) {
|
||||
pointer-events: auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style module lang="scss">
|
||||
@use '@n8n/design-system/css/mixins/motion';
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { useUIStore } from '@/app/stores/ui.store';
|
|||
import { CREDENTIAL_EDIT_MODAL_KEY } from '@/features/credentials/credentials.constants';
|
||||
import { useCredentialsStore } from '@/features/credentials/credentials.store';
|
||||
import { useProjectsStore } from '@/features/collaboration/projects/projects.store';
|
||||
import type { Project } from '@/features/collaboration/projects/projects.types';
|
||||
|
||||
import type { AgentCredentialOption } from '../components/AgentCredentialSelect.vue';
|
||||
import { createSlackAgentApp } from './useAgentApi';
|
||||
|
|
@ -42,6 +43,7 @@ export function useAgentChannelSetup(options: UseAgentChannelSetupOptions) {
|
|||
const pendingNewCredentialType = ref<string | null>(null);
|
||||
const channelSetupRef = ref<ChannelSetupComponent>();
|
||||
const loadedIntegrations = ref<ChatIntegrationDescriptor[]>([]);
|
||||
const fetchedProjectForPermissions = ref<Project | null>(null);
|
||||
|
||||
const projectId = computed(() => toValue(options.projectId));
|
||||
const agentId = computed(() => toValue(options.agentId));
|
||||
|
|
@ -49,9 +51,17 @@ export function useAgentChannelSetup(options: UseAgentChannelSetupOptions) {
|
|||
const connectedCredentials = computed(() => toValue(options.connectedCredentials));
|
||||
|
||||
const projectForPermissions = computed(() => {
|
||||
if (projectsStore.currentProject?.id === projectId.value) return projectsStore.currentProject;
|
||||
if (projectsStore.personalProject?.id === projectId.value) return projectsStore.personalProject;
|
||||
return projectsStore.myProjects.find((project) => project.id === projectId.value) ?? null;
|
||||
const storedProject =
|
||||
[
|
||||
projectsStore.currentProject,
|
||||
projectsStore.personalProject,
|
||||
...projectsStore.myProjects,
|
||||
].find((project) => project?.id === projectId.value) ?? null;
|
||||
if (storedProject?.scopes !== undefined) return storedProject;
|
||||
if (fetchedProjectForPermissions.value?.id === projectId.value) {
|
||||
return fetchedProjectForPermissions.value;
|
||||
}
|
||||
return storedProject;
|
||||
});
|
||||
|
||||
const credentialPermissions = computed(() => {
|
||||
|
|
@ -81,6 +91,17 @@ export function useAgentChannelSetup(options: UseAgentChannelSetupOptions) {
|
|||
}
|
||||
}
|
||||
|
||||
async function ensureProjectPermissions() {
|
||||
if (!projectId.value || projectForPermissions.value?.scopes !== undefined) return;
|
||||
|
||||
try {
|
||||
const project = await projectsStore.fetchProject(projectId.value);
|
||||
if (project.id === projectId.value) fetchedProjectForPermissions.value = project;
|
||||
} catch {
|
||||
// Keep permissions fail-closed when the project cannot be loaded.
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchCredentials(integrations: ChatIntegrationDescriptor[]) {
|
||||
loadedIntegrations.value = integrations;
|
||||
credentialsLoading.value = true;
|
||||
|
|
@ -111,6 +132,7 @@ export function useAgentChannelSetup(options: UseAgentChannelSetupOptions) {
|
|||
|
||||
async function loadChannelState(integrations: ChatIntegrationDescriptor[]) {
|
||||
await Promise.all([
|
||||
ensureProjectPermissions(),
|
||||
options.fetchStatus(integrations.map((integration) => integration.type)),
|
||||
fetchCredentials(integrations),
|
||||
]);
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user