feat(core): workspace attachment uploading & error (#12330)

### TL;DR

feat: optimize workspace attachment uploading & error display

![截屏2025-05-16 15.29.43.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/MyktQ6Qwc7H6TiRCFoYN/2408fe5e-e54d-44a8-882c-91e1b26bb660.png)

### What Changes
####
Support for Workspace Attachment Uploading & Error Handling
* Added support for three attachment states: uploading (local), upload failed (local error), and uploaded (persisted). The frontend UI now displays real-time upload progress and error messages.
* Attachments that fail to upload can be deleted directly without confirmation.
* Merged display of uploading and uploaded attachments for a smoother user experience.

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Attachments now show real-time upload status including uploading, error, and uploaded states.
  - Users can remove failed (error) attachments instantly without confirmation.
  - Attachment list merges uploading and uploaded files, displaying up to 10 items.
- **Bug Fixes**
  - Improved error handling and messaging for failed attachment uploads.
- **Style**
  - Enhanced visual styling for error attachments with distinct colors and backgrounds.
- **Tests**
  - Added tests simulating slow network uploads, upload failures, and direct removal of error attachments.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
yoyoyohamapi
2025-05-22 02:35:03 +00:00
parent 21ea65edc5
commit 45ed9038b6
8 changed files with 346 additions and 64 deletions

View File

@@ -1,5 +1,5 @@
import { logger } from '@affine/core/modules/share-doc/entities/share-docs-list';
import type { WorkspaceService } from '@affine/core/modules/workspace'; import type { WorkspaceService } from '@affine/core/modules/workspace';
import { DebugLogger } from '@affine/debug';
import type { PaginationInput } from '@affine/graphql'; import type { PaginationInput } from '@affine/graphql';
import { import {
catchErrorInto, catchErrorInto,
@@ -11,18 +11,26 @@ import {
onStart, onStart,
smartRetry, smartRetry,
} from '@toeverything/infra'; } from '@toeverything/infra';
import { EMPTY, interval, Subject } from 'rxjs'; import { EMPTY, interval, of, Subject } from 'rxjs';
import { import {
concatMap, concatMap,
exhaustMap, exhaustMap,
mergeMap, mergeMap,
switchMap, switchMap,
takeUntil, takeUntil,
tap,
} from 'rxjs/operators'; } from 'rxjs/operators';
import { COUNT_PER_PAGE } from '../constants'; import { COUNT_PER_PAGE } from '../constants';
import type { EmbeddingStore } from '../stores/embedding'; import type { EmbeddingStore } from '../stores/embedding';
import type { AttachmentFile, IgnoredDoc } from '../types'; import type {
AttachmentFile,
IgnoredDoc,
LocalAttachmentFile,
PersistedAttachmentFile,
} from '../types';
const logger = new DebugLogger('WorkspaceEmbedding');
export interface EmbeddingConfig { export interface EmbeddingConfig {
enabled: boolean; enabled: boolean;
@@ -35,7 +43,7 @@ interface Attachments {
hasNextPage: boolean; hasNextPage: boolean;
}; };
edges: { edges: {
node: AttachmentFile; node: PersistedAttachmentFile;
}[]; }[];
} }
@@ -66,6 +74,8 @@ export class Embedding extends Entity {
private readonly EMBEDDING_PROGRESS_POLL_INTERVAL = 3000; private readonly EMBEDDING_PROGRESS_POLL_INTERVAL = 3000;
private readonly stopEmbeddingProgress$ = new Subject<void>(); private readonly stopEmbeddingProgress$ = new Subject<void>();
uploadingAttachments$ = new LiveData<LocalAttachmentFile[]>([]);
mergedAttachments$ = new LiveData<AttachmentFile[]>([]);
constructor( constructor(
private readonly workspaceService: WorkspaceService, private readonly workspaceService: WorkspaceService,
@@ -76,6 +86,15 @@ export class Embedding extends Entity {
this.getAttachments({ first: COUNT_PER_PAGE, after: null }); this.getAttachments({ first: COUNT_PER_PAGE, after: null });
this.getIgnoredDocs(); this.getIgnoredDocs();
this.getEmbeddingProgress(); this.getEmbeddingProgress();
this.uploadingAttachments$.subscribe(() => this.updateMergedAttachments());
this.attachments$.subscribe(() => this.updateMergedAttachments());
this.updateMergedAttachments();
}
private updateMergedAttachments() {
const uploading = this.uploadingAttachments$.value;
const uploaded = this.attachments$.value.edges.map(edge => edge.node);
this.mergedAttachments$.next([...uploading, ...uploaded].slice(0, 10));
} }
getEnabled = effect( getEnabled = effect(
@@ -184,7 +203,17 @@ export class Embedding extends Entity {
).pipe( ).pipe(
smartRetry(), smartRetry(),
mergeMap(value => { mergeMap(value => {
this.attachments$.next(value); const patched = {
...value,
edges: value.edges.map(edge => ({
...edge,
node: {
...edge.node,
status: 'uploaded' as const,
},
})),
};
this.attachments$.next(patched);
return EMPTY; return EMPTY;
}), }),
catchErrorInto(this.error$, error => { catchErrorInto(this.error$, error => {
@@ -200,19 +229,54 @@ export class Embedding extends Entity {
); );
addAttachments = effect( addAttachments = effect(
exhaustMap((files: File[]) => { // Support parallel upload
return fromPromise(signal => mergeMap((files: File[]) => {
this.store.addEmbeddingFiles( const generateLocalId = () =>
this.workspaceService.workspace.id, Math.random().toString(36).slice(2) + Date.now();
files, const localAttachments: LocalAttachmentFile[] = files.map(file => ({
signal localId: generateLocalId(),
) fileName: file.name,
).pipe( mimeType: file.type,
concatMap(() => { size: file.size,
createdAt: file.lastModified,
status: 'uploading',
}));
return of({ files, localAttachments }).pipe(
// Refresh uploading attachments immediately
tap(({ localAttachments }) => {
this.uploadingAttachments$.next([
...localAttachments,
...this.uploadingAttachments$.value,
]);
}),
// Uploading embedding files
switchMap(({ files }) => {
return fromPromise(signal =>
this.store.addEmbeddingFiles(
this.workspaceService.workspace.id,
files,
signal
)
);
}),
// Refresh uploading attachments
tap(() => {
this.uploadingAttachments$.next(
this.uploadingAttachments$.value.filter(
att => !localAttachments.some(l => l.localId === att.localId)
)
);
this.getAttachments({ first: COUNT_PER_PAGE, after: null }); this.getAttachments({ first: COUNT_PER_PAGE, after: null });
return EMPTY;
}), }),
catchErrorInto(this.error$, error => { catchErrorInto(this.error$, error => {
this.uploadingAttachments$.next(
this.uploadingAttachments$.value.map(att =>
localAttachments.some(l => l.localId === att.localId)
? { ...att, status: 'error', errorMessage: String(error) }
: att
)
);
logger.error( logger.error(
'Failed to add workspace doc embedding attachments', 'Failed to add workspace doc embedding attachments',
error error
@@ -224,6 +288,15 @@ export class Embedding extends Entity {
removeAttachment = effect( removeAttachment = effect(
exhaustMap((id: string) => { exhaustMap((id: string) => {
const localIndex = this.uploadingAttachments$.value.findIndex(
att => att.localId === id
);
if (localIndex !== -1) {
this.uploadingAttachments$.next(
this.uploadingAttachments$.value.filter(att => att.localId !== id)
);
return EMPTY;
}
return fromPromise(signal => return fromPromise(signal =>
this.store.removeEmbeddingFile( this.store.removeEmbeddingFile(
this.workspaceService.workspace.id, this.workspaceService.workspace.id,

View File

@@ -1,11 +1,32 @@
export interface AttachmentFile { export interface PersistedAttachmentFile {
fileId: string; fileId: string;
fileName: string; fileName: string;
mimeType: string; mimeType: string;
size: number; size: number;
createdAt: string; createdAt: string;
status: 'uploaded';
} }
export interface LocalAttachmentFile {
localId: string;
fileName: string;
mimeType: string;
size: number;
createdAt: number;
status: 'uploading' | 'error';
}
export interface UploadingAttachmentFile extends LocalAttachmentFile {
status: 'uploading';
}
export interface ErrorAttachmentFile extends LocalAttachmentFile {
status: 'error';
errorMessage: string;
}
export type AttachmentFile = PersistedAttachmentFile | LocalAttachmentFile;
export interface IgnoredDoc { export interface IgnoredDoc {
docId: string; docId: string;
createdAt: string; createdAt: string;

View File

@@ -0,0 +1,38 @@
import type {
AttachmentFile,
ErrorAttachmentFile,
LocalAttachmentFile,
PersistedAttachmentFile,
UploadingAttachmentFile,
} from './types';
export function isPersistedAttachment(
attachment: AttachmentFile
): attachment is PersistedAttachmentFile {
return 'fileId' in attachment;
}
export function isErrorAttachment(
attachment: AttachmentFile
): attachment is ErrorAttachmentFile {
return 'errorMessage' in attachment;
}
export function isUploadingAttachment(
attachment: AttachmentFile
): attachment is UploadingAttachmentFile {
return 'localId' in attachment && attachment.status === 'uploading';
}
export function isLocalAttachment(
attachment: AttachmentFile
): attachment is LocalAttachmentFile {
return 'localId' in attachment;
}
export function getAttachmentId(attachment: AttachmentFile): string {
if (isPersistedAttachment(attachment)) {
return attachment.fileId;
}
return attachment.localId;
}

View File

@@ -1,14 +1,27 @@
import { Loading, useConfirmModal } from '@affine/component'; import { Loading, Tooltip, useConfirmModal } from '@affine/component';
import { Pagination } from '@affine/component/setting-components'; import { Pagination } from '@affine/component/setting-components';
import { useI18n } from '@affine/i18n'; import { useI18n } from '@affine/i18n';
import { getAttachmentFileIconRC } from '@blocksuite/affine/components/icons'; import { getAttachmentFileIconRC } from '@blocksuite/affine/components/icons';
import { cssVarV2 } from '@blocksuite/affine/shared/theme'; import { cssVarV2 } from '@blocksuite/affine/shared/theme';
import { CloseIcon } from '@blocksuite/icons/rc'; import { CloseIcon, WarningIcon } from '@blocksuite/icons/rc';
import clsx from 'clsx';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { COUNT_PER_PAGE } from '../constants'; import { COUNT_PER_PAGE } from '../constants';
import type { AttachmentFile } from '../types'; import type {
AttachmentFile,
ErrorAttachmentFile,
PersistedAttachmentFile,
UploadingAttachmentFile,
} from '../types';
import { import {
getAttachmentId,
isErrorAttachment,
isPersistedAttachment,
isUploadingAttachment,
} from '../utils';
import {
attachmentError,
attachmentItem, attachmentItem,
attachmentOperation, attachmentOperation,
attachmentsWrapper, attachmentsWrapper,
@@ -18,7 +31,6 @@ import {
interface AttachmentsProps { interface AttachmentsProps {
attachments: AttachmentFile[]; attachments: AttachmentFile[];
totalCount: number; totalCount: number;
isLoading: boolean;
onPageChange: (offset: number) => void; onPageChange: (offset: number) => void;
onDelete: (id: string) => void; onDelete: (id: string) => void;
} }
@@ -28,6 +40,50 @@ interface AttachmentItemProps {
onDelete: (id: string) => void; onDelete: (id: string) => void;
} }
const UploadingItem: React.FC<{ attachment: UploadingAttachmentFile }> = ({
attachment,
}) => {
return (
<div
className={attachmentTitle}
data-testid="workspace-embedding-setting-attachment-uploading-item"
>
<Loading />
{attachment.fileName}
</div>
);
};
const ErrorItem: React.FC<{ attachment: ErrorAttachmentFile }> = ({
attachment,
}) => {
return (
<Tooltip content={attachment.errorMessage}>
<div
className={attachmentTitle}
data-testid="workspace-embedding-setting-attachment-error-item"
>
<WarningIcon />
{attachment.fileName}
</div>
</Tooltip>
);
};
const PersistedItem: React.FC<{ attachment: PersistedAttachmentFile }> = ({
attachment,
}) => {
const Icon = getAttachmentFileIconRC(attachment.mimeType);
return (
<div
className={attachmentTitle}
data-testid="workspace-embedding-setting-attachment-persisted-item"
>
<Icon style={{ marginRight: 4 }} /> {attachment.fileName}
</div>
);
};
const AttachmentItem: React.FC<AttachmentItemProps> = ({ const AttachmentItem: React.FC<AttachmentItemProps> = ({
attachment, attachment,
onDelete, onDelete,
@@ -36,6 +92,11 @@ const AttachmentItem: React.FC<AttachmentItemProps> = ({
const { openConfirmModal } = useConfirmModal(); const { openConfirmModal } = useConfirmModal();
const handleDelete = useCallback(() => { const handleDelete = useCallback(() => {
if (isErrorAttachment(attachment)) {
onDelete(getAttachmentId(attachment));
return;
}
openConfirmModal({ openConfirmModal({
title: title:
t[ t[
@@ -50,20 +111,25 @@ const AttachmentItem: React.FC<AttachmentItemProps> = ({
variant: 'error', variant: 'error',
}, },
onConfirm: () => { onConfirm: () => {
onDelete(attachment.fileId); onDelete(getAttachmentId(attachment));
}, },
}); });
}, [onDelete, attachment.fileId, openConfirmModal, t]); }, [onDelete, attachment, openConfirmModal, t]);
const Icon = getAttachmentFileIconRC(attachment.mimeType);
return ( return (
<div <div
className={attachmentItem} className={clsx(attachmentItem, {
[attachmentError]: isErrorAttachment(attachment),
})}
data-testid="workspace-embedding-setting-attachment-item" data-testid="workspace-embedding-setting-attachment-item"
> >
<div className={attachmentTitle}> {isUploadingAttachment(attachment) ? (
<Icon style={{ marginRight: 4 }} /> {attachment.fileName} <UploadingItem attachment={attachment} />
</div> ) : isErrorAttachment(attachment) ? (
<ErrorItem attachment={attachment} />
) : isPersistedAttachment(attachment) ? (
<PersistedItem attachment={attachment} />
) : null}
<div className={attachmentOperation}> <div className={attachmentOperation}>
<CloseIcon <CloseIcon
data-testid="workspace-embedding-setting-attachment-delete-button" data-testid="workspace-embedding-setting-attachment-delete-button"
@@ -79,7 +145,6 @@ const AttachmentItem: React.FC<AttachmentItemProps> = ({
export const Attachments: React.FC<AttachmentsProps> = ({ export const Attachments: React.FC<AttachmentsProps> = ({
attachments, attachments,
totalCount, totalCount,
isLoading,
onDelete, onDelete,
onPageChange, onPageChange,
}) => { }) => {
@@ -95,17 +160,13 @@ export const Attachments: React.FC<AttachmentsProps> = ({
className={attachmentsWrapper} className={attachmentsWrapper}
data-testid="workspace-embedding-setting-attachment-list" data-testid="workspace-embedding-setting-attachment-list"
> >
{isLoading ? ( {attachments.map(attachment => (
<Loading /> <AttachmentItem
) : ( key={getAttachmentId(attachment)}
attachments.map(attachment => ( attachment={attachment}
<AttachmentItem onDelete={onDelete}
key={attachment.fileId} />
attachment={attachment} ))}
onDelete={onDelete}
/>
))
)}
<Pagination <Pagination
totalCount={totalCount} totalCount={totalCount}
countPerPage={COUNT_PER_PAGE} countPerPage={COUNT_PER_PAGE}

View File

@@ -10,7 +10,7 @@ import { useI18n } from '@affine/i18n';
import track from '@affine/track'; import track from '@affine/track';
import { useLiveData, useService } from '@toeverything/infra'; import { useLiveData, useService } from '@toeverything/infra';
import type React from 'react'; import type React from 'react';
import { useCallback, useEffect, useMemo } from 'react'; import { useCallback, useEffect } from 'react';
import { EmbeddingService } from '../services/embedding'; import { EmbeddingService } from '../services/embedding';
import { Attachments } from './attachments'; import { Attachments } from './attachments';
@@ -23,7 +23,12 @@ export const EmbeddingSettings: React.FC<EmbeddingSettingsProps> = () => {
const t = useI18n(); const t = useI18n();
const embeddingService = useService(EmbeddingService); const embeddingService = useService(EmbeddingService);
const embeddingEnabled = useLiveData(embeddingService.embedding.enabled$); const embeddingEnabled = useLiveData(embeddingService.embedding.enabled$);
const attachments = useLiveData(embeddingService.embedding.attachments$); const { pageInfo, totalCount } = useLiveData(
embeddingService.embedding.attachments$
);
const attachments = useLiveData(
embeddingService.embedding.mergedAttachments$
);
const ignoredDocs = useLiveData(embeddingService.embedding.ignoredDocs$); const ignoredDocs = useLiveData(embeddingService.embedding.ignoredDocs$);
const embeddingProgress = useLiveData( const embeddingProgress = useLiveData(
embeddingService.embedding.embeddingProgress$ embeddingService.embedding.embeddingProgress$
@@ -32,14 +37,6 @@ export const EmbeddingSettings: React.FC<EmbeddingSettingsProps> = () => {
const isIgnoredDocsLoading = useLiveData( const isIgnoredDocsLoading = useLiveData(
embeddingService.embedding.isIgnoredDocsLoading$ embeddingService.embedding.isIgnoredDocsLoading$
); );
const isAttachmentsLoading = useLiveData(
embeddingService.embedding.isAttachmentsLoading$
);
const attachmentNodes = useMemo(
() => attachments.edges.map(edge => edge.node),
[attachments]
);
const ignoredDocNodes = ignoredDocs;
const workspaceDialogService = useService(WorkspaceDialogService); const workspaceDialogService = useService(WorkspaceDialogService);
const handleEmbeddingToggle = useCallback( const handleEmbeddingToggle = useCallback(
@@ -77,17 +74,17 @@ export const EmbeddingSettings: React.FC<EmbeddingSettingsProps> = () => {
(offset: number) => { (offset: number) => {
embeddingService.embedding.getAttachments({ embeddingService.embedding.getAttachments({
offset, offset,
after: attachments.pageInfo.endCursor, after: pageInfo.endCursor,
}); });
}, },
[embeddingService.embedding, attachments.pageInfo.endCursor] [embeddingService.embedding, pageInfo.endCursor]
); );
const handleSelectDoc = useCallback(() => { const handleSelectDoc = useCallback(() => {
if (isIgnoredDocsLoading) { if (isIgnoredDocsLoading) {
return; return;
} }
const initialIds = ignoredDocNodes.map(doc => doc.docId); const initialIds = ignoredDocs.map(doc => doc.docId);
workspaceDialogService.open( workspaceDialogService.open(
'doc-selector', 'doc-selector',
{ {
@@ -108,7 +105,7 @@ export const EmbeddingSettings: React.FC<EmbeddingSettingsProps> = () => {
} }
); );
}, [ }, [
ignoredDocNodes, ignoredDocs,
isIgnoredDocsLoading, isIgnoredDocsLoading,
workspaceDialogService, workspaceDialogService,
embeddingService.embedding, embeddingService.embedding,
@@ -174,12 +171,11 @@ export const EmbeddingSettings: React.FC<EmbeddingSettingsProps> = () => {
</Upload> </Upload>
</SettingRow> </SettingRow>
{attachmentNodes.length > 0 && ( {attachments.length > 0 && (
<Attachments <Attachments
attachments={attachmentNodes} attachments={attachments}
isLoading={isAttachmentsLoading}
onDelete={handleAttachmentsDelete} onDelete={handleAttachmentsDelete}
totalCount={attachments.totalCount} totalCount={totalCount}
onPageChange={handleAttachmentsPageChange} onPageChange={handleAttachmentsPageChange}
/> />
)} )}
@@ -203,9 +199,9 @@ export const EmbeddingSettings: React.FC<EmbeddingSettingsProps> = () => {
</Button> </Button>
</SettingRow> </SettingRow>
{ignoredDocNodes.length > 0 && ( {ignoredDocs.length > 0 && (
<IgnoredDocs <IgnoredDocs
ignoredDocs={ignoredDocNodes} ignoredDocs={ignoredDocs}
isLoading={isIgnoredDocsLoading} isLoading={isIgnoredDocsLoading}
/> />
)} )}

View File

@@ -23,6 +23,7 @@ export const attachmentItem = css({
alignItems: 'center', alignItems: 'center',
padding: '4px', padding: '4px',
gap: '4px', gap: '4px',
color: cssVar('textPrimaryColor'),
border: `0.5px solid ${cssVar('borderColor')}`, border: `0.5px solid ${cssVar('borderColor')}`,
borderRadius: '4px', borderRadius: '4px',
flex: 'none', flex: 'none',
@@ -33,12 +34,16 @@ export const attachmentItem = css({
export const attachmentTitle = css({ export const attachmentTitle = css({
fontSize: '14px', fontSize: '14px',
fontWeight: 400, fontWeight: 400,
color: cssVar('textPrimaryColor'),
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
gap: '4px', gap: '4px',
}); });
export const attachmentError = css({
color: cssVarV2('status/error'),
backgroundColor: cssVarV2('layer/background/error'),
});
export const attachmentOperation = css({ export const attachmentOperation = css({
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',

View File

@@ -82,15 +82,42 @@ test.describe('AISettings/Embedding', () => {
buffer: buffer2, buffer: buffer2,
}, },
]; ];
const client = await page.context().newCDPSession(page);
await client.send('Network.enable');
await client.send('Network.emulateNetworkConditions', {
offline: false,
latency: 1000,
downloadThroughput: (50 * 1024) / 8,
uploadThroughput: (50 * 1024) / 8,
connectionType: 'cellular3g',
});
await utils.settings.uploadWorkspaceEmbedding(page, attachments); await utils.settings.uploadWorkspaceEmbedding(page, attachments);
const attachmentList = await page.getByTestId( const attachmentList = await page.getByTestId(
'workspace-embedding-setting-attachment-list' 'workspace-embedding-setting-attachment-list'
); );
// Uploading
await expect(
attachmentList.getByTestId(
'workspace-embedding-setting-attachment-uploading-item'
)
).toHaveCount(2);
// Persisted
await expect( await expect(
attachmentList.getByTestId('workspace-embedding-setting-attachment-item') attachmentList.getByTestId('workspace-embedding-setting-attachment-item')
).toHaveCount(2); ).toHaveCount(2);
await client.send('Network.emulateNetworkConditions', {
offline: false,
latency: 0,
downloadThroughput: -1,
uploadThroughput: -1,
});
await utils.settings.closeSettingsPanel(page); await utils.settings.closeSettingsPanel(page);
await page.waitForTimeout(5000); // wait for the embedding to be ready await page.waitForTimeout(5000); // wait for the embedding to be ready
@@ -120,6 +147,36 @@ test.describe('AISettings/Embedding', () => {
}).toPass({ timeout: 20000 }); }).toPass({ timeout: 20000 });
}); });
test('should display failed info if upload attachment failed', async ({
loggedInPage: page,
utils,
}) => {
await utils.settings.enableWorkspaceEmbedding(page);
const attachments = [
{
name: 'document1.txt',
mimeType: 'text/plain',
buffer: Buffer.from('HelloWorld'),
},
];
await page.context().setOffline(true);
await utils.settings.uploadWorkspaceEmbedding(page, attachments);
const attachmentList = await page.getByTestId(
'workspace-embedding-setting-attachment-list'
);
const errorItem = await attachmentList.getByTestId(
'workspace-embedding-setting-attachment-error-item'
);
await errorItem.hover();
await expect(page.getByText(/Network error/i)).toBeVisible();
await page.context().setOffline(false);
});
test('should support hybrid search for both globally uploaded attachments and those uploaded in the current session', async ({ test('should support hybrid search for both globally uploaded attachments and those uploaded in the current session', async ({
loggedInPage: page, loggedInPage: page,
utils, utils,
@@ -216,7 +273,7 @@ test.describe('AISettings/Embedding', () => {
).toHaveText('document1.txt'); ).toHaveText('document1.txt');
}); });
test('should support remove attachment', async ({ test('should support remove attachment with confirm', async ({
loggedInPage: page, loggedInPage: page,
utils, utils,
}) => { }) => {
@@ -240,6 +297,25 @@ test.describe('AISettings/Embedding', () => {
await utils.settings.removeAttachment(page, 'document1.txt'); await utils.settings.removeAttachment(page, 'document1.txt');
}); });
test('should support remove error attachment directly', async ({
loggedInPage: page,
utils,
}) => {
await utils.settings.enableWorkspaceEmbedding(page);
const textContent = 'WorkspaceEBEEE is a cute cat';
const attachments = [
{
name: 'document1.txt',
mimeType: 'text/plain',
buffer: Buffer.from(textContent),
},
];
await page.context().setOffline(true);
await utils.settings.uploadWorkspaceEmbedding(page, attachments);
await utils.settings.removeAttachment(page, 'document1.txt', false);
await page.context().setOffline(false);
});
// FIXME: wait for indexer // FIXME: wait for indexer
test.skip('should support ignore docs for embedding', async ({ test.skip('should support ignore docs for embedding', async ({
loggedInPage: page, loggedInPage: page,

View File

@@ -87,23 +87,35 @@ export class SettingsPanelUtils {
while (count > 0) { while (count > 0) {
const attachmentItem = await page.getByTestId(itemId).first(); const attachmentItem = await page.getByTestId(itemId).first();
const hasErrorItem = await attachmentItem
.getByTestId('workspace-embedding-setting-attachment-error-item')
.isVisible();
await attachmentItem await attachmentItem
.getByTestId('workspace-embedding-setting-attachment-delete-button') .getByTestId('workspace-embedding-setting-attachment-delete-button')
.click(); .click();
await page.getByTestId('confirm-modal-confirm').click();
if (!hasErrorItem) {
await page.getByTestId('confirm-modal-confirm').click();
}
await page.waitForTimeout(1000); await page.waitForTimeout(1000);
count = await page.getByTestId(itemId).count(); count = await page.getByTestId(itemId).count();
} }
} }
public static async removeAttachment(page: Page, attachment: string) { public static async removeAttachment(
page: Page,
attachment: string,
shouldConfirm = true
) {
const attachmentItem = await page const attachmentItem = await page
.getByTestId('workspace-embedding-setting-attachment-item') .getByTestId('workspace-embedding-setting-attachment-item')
.filter({ hasText: attachment }); .filter({ hasText: attachment });
await attachmentItem await attachmentItem
.getByTestId('workspace-embedding-setting-attachment-delete-button') .getByTestId('workspace-embedding-setting-attachment-delete-button')
.click(); .click();
await page.getByTestId('confirm-modal-confirm').click(); if (shouldConfirm) {
await page.getByTestId('confirm-modal-confirm').click();
}
await page await page
.getByTestId('workspace-embedding-setting-attachment-item') .getByTestId('workspace-embedding-setting-attachment-item')
.filter({ hasText: attachment }) .filter({ hasText: attachment })