From 75a0cdfd2016b3ca0ab11460e1052dcc4719532e Mon Sep 17 00:00:00 2001 From: Rob Hough Date: Fri, 7 Aug 2026 15:07:30 +0100 Subject: [PATCH] feat(editor): Improve agent file upload visibility (#35513) --- .../frontend/@n8n/i18n/src/locales/en.json | 5 +- .../agents/__tests__/AgentFilesPanel.test.ts | 26 +++++-- .../agents/components/AgentFilesPanel.vue | 72 +++++++++++-------- .../components/AgentVectorStoresPanel.vue | 22 +++--- 4 files changed, 77 insertions(+), 48 deletions(-) diff --git a/packages/frontend/@n8n/i18n/src/locales/en.json b/packages/frontend/@n8n/i18n/src/locales/en.json index 8a85c6b7b3d..c6eb56693ad 100644 --- a/packages/frontend/@n8n/i18n/src/locales/en.json +++ b/packages/frontend/@n8n/i18n/src/locales/en.json @@ -7557,8 +7557,9 @@ "agents.builder.files.uploadTotalTooLarge.message": "These files would make the knowledge base larger than {size} GB. Delete files or upload a smaller selection.", "agents.builder.files.uploadTooManyFiles.title": "Too many files", "agents.builder.files.uploadTooManyFiles.message": "You can upload up to {max} files at a time. Select fewer files and try again.", - "agents.builder.files.empty": "No files uploaded yet.", - "agents.builder.files.addFile": "Add file", + "agents.builder.files.empty": "No files uploaded", + "agents.builder.files.emptyDescription": "Upload CSV, PDF, Markdown, or TXT files this agent can search and read", + "agents.builder.files.addFile": "Upload file", "agents.builder.files.loading": "Loading files...", "agents.builder.files.origin.user": "User", "agents.builder.files.uploaded": "File uploaded", diff --git a/packages/frontend/editor-ui/src/features/agents/__tests__/AgentFilesPanel.test.ts b/packages/frontend/editor-ui/src/features/agents/__tests__/AgentFilesPanel.test.ts index c7aed5cbaed..1c57edffec4 100644 --- a/packages/frontend/editor-ui/src/features/agents/__tests__/AgentFilesPanel.test.ts +++ b/packages/frontend/editor-ui/src/features/agents/__tests__/AgentFilesPanel.test.ts @@ -56,12 +56,13 @@ const file: AgentFileDto = { }; describe('AgentFilesPanel', () => { - it('enables the upload button with the normal upload tooltip', () => { - const wrapper = mountPanel(); + it('enables the upload button with the normal upload tooltip when files exist', () => { + const wrapper = mountPanel({ files: [file] }); const uploadButton = wrapper.find('[data-testid="agent-files-upload"]'); expect(uploadButton.attributes('disabled')).toBeUndefined(); expect(uploadButton.attributes('aria-label')).toBe('agents.builder.files.addFile'); + expect(wrapper.findComponent({ name: 'N8nEmptyState' }).exists()).toBe(false); }); it('shows the normal empty-state message', () => { @@ -70,7 +71,24 @@ describe('AgentFilesPanel', () => { expect(wrapper.text()).toContain('agents.builder.files.empty'); }); - it('shows the knowledge base title with a tooltip and icon-only add action', () => { + it('opens the file picker from the empty-state button', async () => { + const clickSpy = vi.spyOn(HTMLInputElement.prototype, 'click'); + const wrapper = mountPanel({ files: [] }); + + wrapper.findComponent({ name: 'N8nEmptyState' }).vm.$emit('click:button'); + await wrapper.vm.$nextTick(); + + expect(clickSpy).toHaveBeenCalledOnce(); + clickSpy.mockRestore(); + }); + + it('disables the empty-state button when uploads are disabled', () => { + const wrapper = mountPanel({ files: [], disabled: true }); + + expect(wrapper.findComponent({ name: 'N8nEmptyState' }).props('buttonDisabled')).toBe(true); + }); + + it('shows the knowledge base title with a tooltip and add action', () => { const wrapper = mountPanel({ files: [file] }); expect(wrapper.find('[data-testid="agent-files-title"]').text()).toContain( @@ -79,9 +97,7 @@ describe('AgentFilesPanel', () => { const uploadButton = wrapper.findComponent({ name: 'N8nButton' }); expect(uploadButton.props('variant')).toBe('ghost'); - expect(uploadButton.props('iconOnly')).toBe(true); expect(uploadButton.props('icon')).toBe('plus'); - expect(wrapper.find('[data-testid="agent-files-upload"]').text()).toBe(''); }); it('renders uploaded files as table rows with owner, type, size, and date metadata', () => { diff --git a/packages/frontend/editor-ui/src/features/agents/components/AgentFilesPanel.vue b/packages/frontend/editor-ui/src/features/agents/components/AgentFilesPanel.vue index 4cc6f4ef706..20740565d81 100644 --- a/packages/frontend/editor-ui/src/features/agents/components/AgentFilesPanel.vue +++ b/packages/frontend/editor-ui/src/features/agents/components/AgentFilesPanel.vue @@ -6,6 +6,8 @@ import { N8nIcon, N8nTableBase, N8nTooltip, + N8nText, + N8nEmptyState, } from '@n8n/design-system'; import type { ActionDropdownItem } from '@n8n/design-system'; import { useI18n, type BaseTextKey } from '@n8n/i18n'; @@ -41,10 +43,17 @@ const i18n = useI18n(); const fileInput = useTemplateRef('fileInput'); const isMutating = computed(() => props.uploading || props.deletingFileId !== null); const isUploadDisabled = computed(() => props.disabled || props.loading || isMutating.value); -const uploadTooltip = computed(() => i18n.baseText('agents.builder.files.addFile' as BaseTextKey)); +const uploadButtonLabel = computed(() => + i18n.baseText('agents.builder.files.addFile' as BaseTextKey), +); const acceptAttr = ALLOWED_AGENT_FILE_EXTENSIONS.join(','); +const emptyStateHeader = computed(() => i18n.baseText('agents.builder.files.empty' as BaseTextKey)); +const emptyStateDescription = computed(() => + i18n.baseText('agents.builder.files.emptyDescription' as BaseTextKey), +); + function getFileIcon(file: AgentFileDto) { const extension = file.fileName.split('.').pop()?.toLowerCase(); if (extension === 'csv' || file.mimeType === 'text/csv') return 'file-code'; @@ -128,15 +137,15 @@ function onFilesSelected(event: Event) { - - - - - {{ i18n.baseText('agents.builder.files.empty') }} - - - @@ -230,7 +241,7 @@ function onFilesSelected(event: Event) { .panel { display: flex; flex-direction: column; - gap: var(--spacing--sm); + gap: var(--spacing--xs); width: 100%; } @@ -245,15 +256,14 @@ function onFilesSelected(event: Event) { .title { display: inline-flex; align-items: center; - gap: var(--spacing--3xs); min-width: 0; - color: var(--text-color--subtler); - font-size: var(--font-size--sm); - font-weight: var(--font-weight--medium); - line-height: var(--line-height--sm); } .titleIcon { + width: var(--height--xs); + height: var(--height--xs); + display: grid; + place-items: center; color: var(--text-color--subtler); } diff --git a/packages/frontend/editor-ui/src/features/agents/components/AgentVectorStoresPanel.vue b/packages/frontend/editor-ui/src/features/agents/components/AgentVectorStoresPanel.vue index 093a08f3086..a5df4cd6252 100644 --- a/packages/frontend/editor-ui/src/features/agents/components/AgentVectorStoresPanel.vue +++ b/packages/frontend/editor-ui/src/features/agents/components/AgentVectorStoresPanel.vue @@ -7,6 +7,7 @@ import { N8nIconButton, N8nTableBase, N8nTooltip, + N8nText, } from '@n8n/design-system'; import type { ActionDropdownItem } from '@n8n/design-system'; import { useI18n } from '@n8n/i18n'; @@ -74,17 +75,18 @@ function onAction(actionId: VectorStoreAction, vectorStore: AgentJsonVectorStore