refactor(editor): split openFileOrFiles into openSingleFileWith and openFilesWith (#12523)

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## 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.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
fundon
2025-05-28 03:06:32 +00:00
parent cf456c888f
commit c43e1bcc4e
12 changed files with 72 additions and 118 deletions
@@ -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);
@@ -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;
@@ -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<MindMapNode> {
const file = await openFileOrFiles({
acceptType: 'MindMap',
});
const file = await openSingleFileWith('MindMap');
if (!file) {
throw new BlockSuiteError(ErrorCode.UserAbortError, 'Aborted by user');
@@ -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);
@@ -112,21 +112,11 @@ type AcceptTypes =
| 'Html'
| 'Zip'
| 'MindMap';
export function openFileOrFiles(options?: {
acceptType?: AcceptTypes;
}): Promise<File | null>;
export function openFileOrFiles(options: {
acceptType?: AcceptTypes;
multiple: false;
}): Promise<File | null>;
export function openFileOrFiles(options: {
acceptType?: AcceptTypes;
multiple: true;
}): Promise<File[] | null>;
export async function openFileOrFiles({
acceptType = 'Any',
multiple = false,
} = {}) {
export async function openFilesWith(
acceptType: AcceptTypes = 'Any',
multiple: boolean = true
): Promise<File[] | null> {
// 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 `<input type="file" multiple? />` 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 `<input type="file" multiple? />` 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<File | null> {
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;
}
@@ -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);
@@ -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) {
@@ -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,
@@ -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/'));
@@ -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`);
@@ -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<
>
<Button
onClick={() => {
openFileOrFiles({ multiple: true })
openFilesWith()
.then(files => {
files?.forEach(file => {
manager.uploadFile(file);
@@ -447,7 +447,7 @@ const FileCellComponent: ForwardRefRenderFunction<
<div className={styles.uploadContainer}>
<div
onClick={() => {
openFileOrFiles({ multiple: true })
openFilesWith()
.then(files => {
files?.forEach(file => {
manager.uploadFile(file);
@@ -13,7 +13,7 @@ import {
import { DebugLogger } from '@affine/debug';
import { useI18n } from '@affine/i18n';
import track from '@affine/track';
import { openFileOrFiles } from '@blocksuite/affine/shared/utils';
import { openFilesWith } from '@blocksuite/affine/shared/utils';
import type { Workspace } from '@blocksuite/affine/store';
import {
HtmlTransformer,
@@ -56,7 +56,7 @@ type ImportConfig = {
fileOptions: { acceptType: AcceptType; multiple: boolean };
importFunction: (
docCollection: Workspace,
file: File | File[]
files: File[]
) => Promise<ImportResult>;
};
@@ -134,9 +134,6 @@ const importConfigs: Record<ImportType, ImportConfig> = {
markdown: {
fileOptions: { acceptType: 'Markdown', multiple: true },
importFunction: async (docCollection, files) => {
if (!Array.isArray(files)) {
throw new Error('Expected an array of files for markdown files import');
}
const docIds: string[] = [];
for (const file of files) {
const text = await file.text();
@@ -157,8 +154,9 @@ const importConfigs: Record<ImportType, ImportConfig> = {
},
markdownZip: {
fileOptions: { acceptType: 'Zip', multiple: false },
importFunction: async (docCollection, file) => {
if (Array.isArray(file)) {
importFunction: async (docCollection, files) => {
const file = files.length === 1 ? files[0] : null;
if (!file) {
throw new Error('Expected a single zip file for markdownZip import');
}
const docIds = await MarkdownTransformer.importMarkdownZip({
@@ -175,9 +173,6 @@ const importConfigs: Record<ImportType, ImportConfig> = {
html: {
fileOptions: { acceptType: 'Html', multiple: true },
importFunction: async (docCollection, files) => {
if (!Array.isArray(files)) {
throw new Error('Expected an array of files for html files import');
}
const docIds: string[] = [];
for (const file of files) {
const text = await file.text();
@@ -198,8 +193,9 @@ const importConfigs: Record<ImportType, ImportConfig> = {
},
notion: {
fileOptions: { acceptType: 'Zip', multiple: false },
importFunction: async (docCollection, file) => {
if (Array.isArray(file)) {
importFunction: async (docCollection, files) => {
const file = files.length === 1 ? files[0] : null;
if (!file) {
throw new Error('Expected a single zip file for notion import');
}
const { entryId, pageIds, isWorkspaceFile } =
@@ -218,8 +214,9 @@ const importConfigs: Record<ImportType, ImportConfig> = {
},
snapshot: {
fileOptions: { acceptType: 'Zip', multiple: false },
importFunction: async (docCollection, file) => {
if (Array.isArray(file)) {
importFunction: async (docCollection, files) => {
const file = files.length === 1 ? files[0] : null;
if (!file) {
throw new Error('Expected a single zip file for snapshot import');
}
const docIds = (
@@ -412,9 +409,10 @@ export const ImportDialog = ({
setImportError(null);
try {
const importConfig = importConfigs[type];
const file = await openFileOrFiles(importConfig.fileOptions);
const { acceptType, multiple } = importConfig.fileOptions;
const files = await openFilesWith(acceptType, multiple);
if (!file || (Array.isArray(file) && file.length === 0)) {
if (!files || files.length === 0) {
throw new Error(
t['com.affine.import.status.failed.message.no-file-selected']()
);
@@ -427,7 +425,7 @@ export const ImportDialog = ({
});
const { docIds, entryId, isWorkspaceFile } =
await importConfig.importFunction(docCollection, file);
await importConfig.importFunction(docCollection, files);
setImportResult({ docIds, entryId, isWorkspaceFile });
setStatus('success');