From c43e1bcc4eb340ac72e6a92d755a620ae0b1a2ed Mon Sep 17 00:00:00 2001 From: fundon Date: Wed, 28 May 2025 03:06:32 +0000 Subject: [PATCH] refactor(editor): split openFileOrFiles into openSingleFileWith and openFilesWith (#12523) ## Summary by CodeRabbit - **New Features** - Improved file selection dialogs for attachments, imports, and uploads, allowing for more consistent and streamlined file picking across the app. - **Bug Fixes** - Resolved inconsistencies when selecting single or multiple files, ensuring a smoother user experience during file import and upload. - **Refactor** - Unified and simplified file selection logic throughout the app for better reliability and maintainability. - Standardized import functions to uniformly handle arrays of files, enhancing consistency in file processing. --- .../attachment/src/configs/slash-menu.ts | 6 +- .../mindmap/src/toolbar/basket-elements.ts | 4 +- .../src/toolbar/utils/import-mindmap.ts | 6 +- .../affine/gfx/note/src/toolbar/note-menu.ts | 4 +- .../affine/shared/src/utils/file/filesys.ts | 72 ++++++------------- .../widgets/keyboard-toolbar/src/config.ts | 6 +- .../linked-doc/src/import-doc/import-doc.ts | 14 ++-- .../_common/components/starter-debug-menu.ts | 27 +++---- .../components/ai-chat-chips/add-popover.ts | 6 +- .../components/ai-chat-input/ai-chat-input.ts | 7 +- .../database-block/properties/file/view.tsx | 6 +- .../core/src/desktop/dialogs/import/index.tsx | 32 ++++----- 12 files changed, 72 insertions(+), 118 deletions(-) diff --git a/blocksuite/affine/blocks/attachment/src/configs/slash-menu.ts b/blocksuite/affine/blocks/attachment/src/configs/slash-menu.ts index 9572e95de..b723345dd 100644 --- a/blocksuite/affine/blocks/attachment/src/configs/slash-menu.ts +++ b/blocksuite/affine/blocks/attachment/src/configs/slash-menu.ts @@ -1,4 +1,4 @@ -import { openFileOrFiles } from '@blocksuite/affine-shared/utils'; +import { openSingleFileWith } from '@blocksuite/affine-shared/utils'; import { type SlashMenuConfig } from '@blocksuite/affine-widget-slash-menu'; import { ExportToPdfIcon, FileIcon } from '@blocksuite/icons/lit'; @@ -21,7 +21,7 @@ export const attachmentSlashMenuConfig: SlashMenuConfig = { model.store.schema.flavourSchemaMap.has('affine:attachment'), action: ({ std, model }) => { (async () => { - const file = await openFileOrFiles(); + const file = await openSingleFileWith(); if (!file) return; await addSiblingAttachmentBlocks(std, [file], model); @@ -44,7 +44,7 @@ export const attachmentSlashMenuConfig: SlashMenuConfig = { model.store.schema.flavourSchemaMap.has('affine:attachment'), action: ({ std, model }) => { (async () => { - const file = await openFileOrFiles(); + const file = await openSingleFileWith(); if (!file) return; await addSiblingAttachmentBlocks(std, [file], model); diff --git a/blocksuite/affine/gfx/mindmap/src/toolbar/basket-elements.ts b/blocksuite/affine/gfx/mindmap/src/toolbar/basket-elements.ts index 592a28441..bc0cefa6d 100644 --- a/blocksuite/affine/gfx/mindmap/src/toolbar/basket-elements.ts +++ b/blocksuite/affine/gfx/mindmap/src/toolbar/basket-elements.ts @@ -17,7 +17,7 @@ import { FeatureFlagService, TelemetryProvider, } from '@blocksuite/affine-shared/services'; -import { openFileOrFiles } from '@blocksuite/affine-shared/utils'; +import { openSingleFileWith } from '@blocksuite/affine-shared/utils'; import { Bound, type IVec } from '@blocksuite/global/gfx'; import type { BlockComponent } from '@blocksuite/std'; import type { TemplateResult } from 'lit'; @@ -158,7 +158,7 @@ export const textRender: DraggableTool['render'] = async (bound, edgeless) => { export const mediaRender: DraggableTool['render'] = async (bound, edgeless) => { let file: File | null = null; try { - file = await openFileOrFiles(); + file = await openSingleFileWith(); } catch (e) { console.error(e); return null; diff --git a/blocksuite/affine/gfx/mindmap/src/toolbar/utils/import-mindmap.ts b/blocksuite/affine/gfx/mindmap/src/toolbar/utils/import-mindmap.ts index 3429a2d22..43c2fb1e2 100644 --- a/blocksuite/affine/gfx/mindmap/src/toolbar/utils/import-mindmap.ts +++ b/blocksuite/affine/gfx/mindmap/src/toolbar/utils/import-mindmap.ts @@ -1,4 +1,4 @@ -import { openFileOrFiles } from '@blocksuite/affine-shared/utils'; +import { openSingleFileWith } from '@blocksuite/affine-shared/utils'; import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions'; import type { Bound } from '@blocksuite/global/gfx'; import c from 'simple-xml-to-json'; @@ -12,9 +12,7 @@ type MindMapNode = { }; export async function importMindmap(bound: Bound): Promise { - const file = await openFileOrFiles({ - acceptType: 'MindMap', - }); + const file = await openSingleFileWith('MindMap'); if (!file) { throw new BlockSuiteError(ErrorCode.UserAbortError, 'Aborted by user'); diff --git a/blocksuite/affine/gfx/note/src/toolbar/note-menu.ts b/blocksuite/affine/gfx/note/src/toolbar/note-menu.ts index 091d7bd23..791e967d2 100644 --- a/blocksuite/affine/gfx/note/src/toolbar/note-menu.ts +++ b/blocksuite/affine/gfx/note/src/toolbar/note-menu.ts @@ -7,7 +7,7 @@ import { TelemetryProvider } from '@blocksuite/affine-shared/services'; import type { NoteChildrenFlavour } from '@blocksuite/affine-shared/types'; import { getImageFilesFromLocal, - openFileOrFiles, + openSingleFileWith, } from '@blocksuite/affine-shared/utils'; import { EdgelessToolbarToolMixin } from '@blocksuite/affine-widget-edgeless-toolbar'; import { AttachmentIcon, ImageIcon, LinkIcon } from '@blocksuite/icons/lit'; @@ -139,7 +139,7 @@ export class EdgelessNoteMenu extends EdgelessToolbarToolMixin(LitElement) { .activeMode=${'background'} .tooltip=${'File'} @click=${async () => { - const file = await openFileOrFiles(); + const file = await openSingleFileWith(); if (!file) return; await addAttachments(this.edgeless.std, [file]); this.gfx.tool.setTool(DefaultTool); diff --git a/blocksuite/affine/shared/src/utils/file/filesys.ts b/blocksuite/affine/shared/src/utils/file/filesys.ts index 250e4c1f9..a650a9ad3 100644 --- a/blocksuite/affine/shared/src/utils/file/filesys.ts +++ b/blocksuite/affine/shared/src/utils/file/filesys.ts @@ -112,21 +112,11 @@ type AcceptTypes = | 'Html' | 'Zip' | 'MindMap'; -export function openFileOrFiles(options?: { - acceptType?: AcceptTypes; -}): Promise; -export function openFileOrFiles(options: { - acceptType?: AcceptTypes; - multiple: false; -}): Promise; -export function openFileOrFiles(options: { - acceptType?: AcceptTypes; - multiple: true; -}): Promise; -export async function openFileOrFiles({ - acceptType = 'Any', - multiple = false, -} = {}) { + +export async function openFilesWith( + acceptType: AcceptTypes = 'Any', + multiple: boolean = true +): Promise { // Feature detection. The API needs to be supported // and the app not run in an iframe. const supportsFileSystemAccess = @@ -138,6 +128,7 @@ export async function openFileOrFiles({ return false; } })(); + // If the File System Access API is supported… if (supportsFileSystemAccess && window.showOpenFilePicker) { try { @@ -153,30 +144,14 @@ export async function openFileOrFiles({ } satisfies OpenFilePickerOptions; // Show the file picker, optionally allowing multiple files. const handles = await window.showOpenFilePicker(pickerOpts); - // Only one file is requested. - if (!multiple) { - // Add the `FileSystemFileHandle` as `.handle`. - const file = await handles[0].getFile(); - // Add the `FileSystemFileHandle` as `.handle`. - // file.handle = handles[0]; - return file; - } else { - const files = await Promise.all( - handles.map(async handle => { - const file = await handle.getFile(); - // Add the `FileSystemFileHandle` as `.handle`. - // file.handle = handles[0]; - return file; - }) - ); - return files; - } + + return await Promise.all(handles.map(handle => handle.getFile())); } catch (err) { - console.error('Error opening file'); console.error(err); return null; } } + // Fallback if the File System Access API is not supported. return new Promise(resolve => { // Append a new `` and hide it. @@ -184,9 +159,8 @@ export async function openFileOrFiles({ input.classList.add('affine-upload-input'); input.style.display = 'none'; input.type = 'file'; - if (multiple) { - input.multiple = true; - } + input.multiple = multiple; + if (acceptType !== 'Any') { // For example, `accept="image/*"` or `accept="video/*,audio/*"`. input.accept = Object.keys( @@ -198,17 +172,8 @@ export async function openFileOrFiles({ input.addEventListener('change', () => { // Remove the `` again from the DOM. input.remove(); - // If no files were selected, return. - if (!input.files) { - resolve(null); - return; - } - // Return all files or just one file. - if (multiple) { - resolve(Array.from(input.files)); - return; - } - resolve(input.files[0]); + + resolve(input.files ? Array.from(input.files) : null); }); // The `cancel` event fires when the user cancels the dialog. input.addEventListener('cancel', () => { @@ -223,11 +188,14 @@ export async function openFileOrFiles({ }); } +export function openSingleFileWith( + acceptType?: AcceptTypes +): Promise { + return openFilesWith(acceptType, false).then(files => files?.at(0) ?? null); +} + export async function getImageFilesFromLocal() { - const imageFiles = await openFileOrFiles({ - acceptType: 'Images', - multiple: true, - }); + const imageFiles = await openFilesWith('Images'); if (!imageFiles) return []; return imageFiles; } diff --git a/blocksuite/affine/widgets/keyboard-toolbar/src/config.ts b/blocksuite/affine/widgets/keyboard-toolbar/src/config.ts index eb1acc585..9c1bbe3ee 100644 --- a/blocksuite/affine/widgets/keyboard-toolbar/src/config.ts +++ b/blocksuite/affine/widgets/keyboard-toolbar/src/config.ts @@ -48,7 +48,7 @@ import { REFERENCE_NODE } from '@blocksuite/affine-shared/consts'; import type { AffineTextAttributes } from '@blocksuite/affine-shared/types'; import { createDefaultDoc, - openFileOrFiles, + openSingleFileWith, type Signal, } from '@blocksuite/affine-shared/utils'; import type { AffineLinkedDocWidget } from '@blocksuite/affine-widget-linked-doc'; @@ -418,7 +418,7 @@ const contentMediaToolGroup: KeyboardToolPanelGroup = { const model = selectedModels?.[0]; if (!model) return; - const file = await openFileOrFiles(); + const file = await openSingleFileWith(); if (!file) return; await addSiblingAttachmentBlocks(std, [file], model); @@ -1040,7 +1040,7 @@ export const defaultKeyboardToolbarConfig: KeyboardToolbarConfig = { const model = selectedModels?.[0]; if (!model) return; - const file = await openFileOrFiles(); + const file = await openSingleFileWith(); if (!file) return; await addSiblingAttachmentBlocks(std, [file], model); diff --git a/blocksuite/affine/widgets/linked-doc/src/import-doc/import-doc.ts b/blocksuite/affine/widgets/linked-doc/src/import-doc/import-doc.ts index a9a5c5577..a43aa1547 100644 --- a/blocksuite/affine/widgets/linked-doc/src/import-doc/import-doc.ts +++ b/blocksuite/affine/widgets/linked-doc/src/import-doc/import-doc.ts @@ -6,7 +6,10 @@ import { NewIcon, NotionIcon, } from '@blocksuite/affine-components/icons'; -import { openFileOrFiles } from '@blocksuite/affine-shared/utils'; +import { + openFilesWith, + openSingleFileWith, +} from '@blocksuite/affine-shared/utils'; import { WithDisposable } from '@blocksuite/global/lit'; import type { ExtensionType, Schema, Workspace } from '@blocksuite/store'; import { html, LitElement, type PropertyValues } from 'lit'; @@ -50,7 +53,7 @@ export class ImportDoc extends WithDisposable(LitElement) { } private async _importHtml() { - const files = await openFileOrFiles({ acceptType: 'Html', multiple: true }); + const files = await openFilesWith('Html'); if (!files) return; const pageIds: string[] = []; for (const file of files) { @@ -79,10 +82,7 @@ export class ImportDoc extends WithDisposable(LitElement) { } private async _importMarkDown() { - const files = await openFileOrFiles({ - acceptType: 'Markdown', - multiple: true, - }); + const files = await openFilesWith('Markdown'); if (!files) return; const pageIds: string[] = []; for (const file of files) { @@ -111,7 +111,7 @@ export class ImportDoc extends WithDisposable(LitElement) { } private async _importNotion() { - const file = await openFileOrFiles({ acceptType: 'Zip' }); + const file = await openSingleFileWith('Zip'); if (!file) return; const needLoading = file.size > SHOW_LOADING_SIZE; if (needLoading) { diff --git a/blocksuite/playground/apps/_common/components/starter-debug-menu.ts b/blocksuite/playground/apps/_common/components/starter-debug-menu.ts index b3c6e9d24..15abde883 100644 --- a/blocksuite/playground/apps/_common/components/starter-debug-menu.ts +++ b/blocksuite/playground/apps/_common/components/starter-debug-menu.ts @@ -41,7 +41,11 @@ import { SizeVariables, StyleVariables, } from '@blocksuite/affine/shared/theme'; -import { openFileOrFiles, printToPdf } from '@blocksuite/affine/shared/utils'; +import { + openFilesWith, + openSingleFileWith, + printToPdf, +} from '@blocksuite/affine/shared/utils'; import { ShadowlessElement } from '@blocksuite/affine/std'; import { GfxControllerIdentifier } from '@blocksuite/affine/std/gfx'; import { @@ -339,10 +343,7 @@ export class StarterDebugMenu extends ShadowlessElement { private async _importHTML() { try { - const files = await openFileOrFiles({ - acceptType: 'Html', - multiple: true, - }); + const files = await openFilesWith('Html'); if (!files) return; @@ -373,7 +374,7 @@ export class StarterDebugMenu extends ShadowlessElement { private async _importHTMLZip() { try { - const file = await openFileOrFiles({ acceptType: 'Zip' }); + const file = await openSingleFileWith('Zip'); if (!file) return; const result = await HtmlTransformer.importHTMLZip({ collection: this.collection, @@ -393,10 +394,7 @@ export class StarterDebugMenu extends ShadowlessElement { private async _importMarkdown() { try { - const files = await openFileOrFiles({ - acceptType: 'Markdown', - multiple: true, - }); + const files = await openFilesWith('Markdown'); if (!files) return; @@ -427,7 +425,7 @@ export class StarterDebugMenu extends ShadowlessElement { private async _importMarkdownZip() { try { - const file = await openFileOrFiles({ acceptType: 'Zip' }); + const file = await openSingleFileWith('Zip'); if (!file) return; const result = await MarkdownTransformer.importMarkdownZip({ collection: this.collection, @@ -447,10 +445,7 @@ export class StarterDebugMenu extends ShadowlessElement { private async _importNotionHTML() { try { - const file = await openFileOrFiles({ - acceptType: 'Html', - multiple: false, - }); + const file = await openSingleFileWith('Html'); if (!file) return; const doc = this.editor.doc; const job = doc.getTransformer([defaultImageProxyMiddleware]); @@ -467,7 +462,7 @@ export class StarterDebugMenu extends ShadowlessElement { private async _importNotionHTMLZip() { try { - const file = await openFileOrFiles({ acceptType: 'Zip' }); + const file = await openSingleFileWith('Zip'); if (!file) return; const result = await NotionHtmlTransformer.importNotionZip({ collection: this.collection, diff --git a/packages/frontend/core/src/blocksuite/ai/components/ai-chat-chips/add-popover.ts b/packages/frontend/core/src/blocksuite/ai/components/ai-chat-chips/add-popover.ts index cb99bda4c..c692e631a 100644 --- a/packages/frontend/core/src/blocksuite/ai/components/ai-chat-chips/add-popover.ts +++ b/packages/frontend/core/src/blocksuite/ai/components/ai-chat-chips/add-popover.ts @@ -5,7 +5,7 @@ import track from '@affine/track'; import { SignalWatcher, WithDisposable } from '@blocksuite/affine/global/lit'; import { scrollbarStyle } from '@blocksuite/affine/shared/styles'; import { unsafeCSSVar, unsafeCSSVarV2 } from '@blocksuite/affine/shared/theme'; -import { openFileOrFiles } from '@blocksuite/affine/shared/utils'; +import { openFilesWith } from '@blocksuite/affine/shared/utils'; import { ShadowlessElement } from '@blocksuite/affine/std'; import type { DocMeta } from '@blocksuite/affine/store'; import { @@ -159,9 +159,7 @@ export class ChatPanelAddPopover extends SignalWatcher( }; private readonly _addFileChip = async () => { - const files = await openFileOrFiles({ - multiple: true, - }); + const files = await openFilesWith(); if (!files || files.length === 0) return; const images = files.filter(file => file.type.startsWith('image/')); diff --git a/packages/frontend/core/src/blocksuite/ai/components/ai-chat-input/ai-chat-input.ts b/packages/frontend/core/src/blocksuite/ai/components/ai-chat-input/ai-chat-input.ts index 850f66205..383032687 100644 --- a/packages/frontend/core/src/blocksuite/ai/components/ai-chat-input/ai-chat-input.ts +++ b/packages/frontend/core/src/blocksuite/ai/components/ai-chat-input/ai-chat-input.ts @@ -3,7 +3,7 @@ import { stopPropagation } from '@affine/core/utils'; import type { CopilotSessionType } from '@affine/graphql'; import { SignalWatcher, WithDisposable } from '@blocksuite/affine/global/lit'; import { unsafeCSSVar, unsafeCSSVarV2 } from '@blocksuite/affine/shared/theme'; -import { openFileOrFiles } from '@blocksuite/affine/shared/utils'; +import { openFilesWith } from '@blocksuite/affine/shared/utils'; import type { EditorHost } from '@blocksuite/affine/std'; import { CloseIcon, @@ -533,10 +533,7 @@ export class AIChatInput extends SignalWatcher(WithDisposable(LitElement)) { private readonly _uploadImageFiles = async (_e: MouseEvent) => { if (this._isImageUploadDisabled) return; - const images = await openFileOrFiles({ - acceptType: 'Images', - multiple: true, - }); + const images = await openFilesWith('Images'); if (!images) return; if (this.chatContextValue.images.length + images.length > MAX_IMAGE_COUNT) { toast(`You can only upload up to ${MAX_IMAGE_COUNT} images`); diff --git a/packages/frontend/core/src/blocksuite/database-block/properties/file/view.tsx b/packages/frontend/core/src/blocksuite/database-block/properties/file/view.tsx index 29a94e189..dd6c177a3 100644 --- a/packages/frontend/core/src/blocksuite/database-block/properties/file/view.tsx +++ b/packages/frontend/core/src/blocksuite/database-block/properties/file/view.tsx @@ -9,7 +9,7 @@ import { type DataViewCellLifeCycle, EditorHostKey, } from '@blocksuite/affine/blocks/database'; -import { openFileOrFiles } from '@blocksuite/affine/shared/utils'; +import { openFilesWith } from '@blocksuite/affine/shared/utils'; import type { BlobEngine } from '@blocksuite/affine/sync'; import { DeleteIcon, @@ -402,7 +402,7 @@ const FileCellComponent: ForwardRefRenderFunction< >