mirror of
https://github.com/n8n-io/n8n.git
synced 2026-08-30 18:01:23 +08:00
feat(editor): Improve agent file upload visibility (#35513)
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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<HTMLInputElement>('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) {
|
||||
<template>
|
||||
<div :class="$style.panel" data-testid="agent-files-panel">
|
||||
<div :class="$style.toolbar">
|
||||
<span :class="$style.title" data-testid="agent-files-title">
|
||||
<N8nText bold :class="$style.title" data-testid="agent-files-title">
|
||||
{{ i18n.baseText('agents.builder.files.title') }}
|
||||
<N8nTooltip
|
||||
:content="i18n.baseText('agents.builder.files.titleTooltip' as BaseTextKey)"
|
||||
placement="top"
|
||||
>
|
||||
<N8nIcon icon="circle-help" size="small" :class="$style.titleIcon" />
|
||||
<div :class="$style.titleIcon"><N8nIcon icon="circle-help" size="small" /></div>
|
||||
</N8nTooltip>
|
||||
</span>
|
||||
</N8nText>
|
||||
|
||||
<input
|
||||
ref="fileInput"
|
||||
@@ -148,21 +157,31 @@ function onFilesSelected(event: Event) {
|
||||
@change="onFilesSelected"
|
||||
/>
|
||||
|
||||
<N8nTooltip :content="uploadTooltip" placement="top">
|
||||
<N8nButton
|
||||
variant="ghost"
|
||||
size="small"
|
||||
icon="plus"
|
||||
icon-only
|
||||
:disabled="isUploadDisabled"
|
||||
:aria-label="uploadTooltip"
|
||||
data-testid="agent-files-upload"
|
||||
@click="openFilePicker"
|
||||
/>
|
||||
</N8nTooltip>
|
||||
<N8nButton
|
||||
v-if="!props.loading && props.files.length > 0"
|
||||
variant="ghost"
|
||||
size="small"
|
||||
icon="plus"
|
||||
:disabled="isUploadDisabled"
|
||||
:aria-label="uploadButtonLabel"
|
||||
data-testid="agent-files-upload"
|
||||
@click="openFilePicker"
|
||||
>
|
||||
{{ uploadButtonLabel }}
|
||||
</N8nButton>
|
||||
</div>
|
||||
|
||||
<div :class="$style.tableContainer">
|
||||
<N8nEmptyState
|
||||
v-if="!props.loading && props.files.length === 0"
|
||||
:icon="{ type: 'icon', value: 'file' }"
|
||||
:class="$style.emptyState"
|
||||
:heading="emptyStateHeader"
|
||||
:description="emptyStateDescription"
|
||||
:button-text="uploadButtonLabel"
|
||||
:button-disabled="isUploadDisabled"
|
||||
@click:button="openFilePicker"
|
||||
/>
|
||||
<div v-else :class="$style.tableContainer">
|
||||
<N8nTableBase :max-displayed-rows="10">
|
||||
<tbody>
|
||||
<tr
|
||||
@@ -212,14 +231,6 @@ function onFilesSelected(event: Event) {
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
<tr v-if="!props.loading && props.files.length === 0" :class="$style.lastRow">
|
||||
<td :colspan="6">
|
||||
<span :class="$style.emptyMessage" data-testid="agent-files-empty">
|
||||
{{ i18n.baseText('agents.builder.files.empty') }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</N8nTableBase>
|
||||
</div>
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
+12
-10
@@ -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
|
||||
|
||||
<template>
|
||||
<div :class="$style.panel" data-testid="agent-vector-stores-panel">
|
||||
<div v-if="props.vectorStores.length > 0" :class="$style.toolbar">
|
||||
<span :class="$style.title" data-testid="agent-vector-stores-title">
|
||||
<div :class="$style.toolbar">
|
||||
<N8nText bold :class="$style.title" data-testid="agent-vector-stores-title">
|
||||
{{ i18n.baseText('agents.builder.vectorStores.panel.title') }}
|
||||
<N8nTooltip
|
||||
:content="i18n.baseText('agents.builder.vectorStores.panel.titleTooltip')"
|
||||
placement="top"
|
||||
>
|
||||
<N8nIcon icon="circle-help" size="small" :class="$style.titleIcon" />
|
||||
<div :class="$style.titleIcon"><N8nIcon icon="circle-help" size="small" /></div>
|
||||
</N8nTooltip>
|
||||
</span>
|
||||
</N8nText>
|
||||
<N8nTooltip
|
||||
v-if="props.vectorStores.length > 0"
|
||||
:content="i18n.baseText('agents.builder.vectorStores.panel.connectButton')"
|
||||
placement="top"
|
||||
>
|
||||
@@ -172,7 +174,7 @@ function onAction(actionId: VectorStoreAction, vectorStore: AgentJsonVectorStore
|
||||
.panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing--sm);
|
||||
gap: var(--spacing--xs);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@@ -187,14 +189,14 @@ function onAction(actionId: VectorStoreAction, vectorStore: AgentJsonVectorStore
|
||||
.title {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing--3xs);
|
||||
color: var(--text-color--subtler);
|
||||
font-size: var(--font-size--sm);
|
||||
font-weight: var(--font-weight--medium);
|
||||
line-height: var(--line-height--sm);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.titleIcon {
|
||||
width: var(--height--xs);
|
||||
height: var(--height--xs);
|
||||
display: grid;
|
||||
place-items: center;
|
||||
color: var(--text-color--subtler);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user