refactor(editor): add attachments (#11842)
Closes: [BS-3256](https://linear.app/affine-design/issue/BS-3256/重构-add-attachments-方法)
This commit is contained in:
@@ -1,10 +1,7 @@
|
|||||||
import { SurfaceBlockModel } from '@blocksuite/affine-block-surface';
|
import { SurfaceBlockModel } from '@blocksuite/affine-block-surface';
|
||||||
import { FileDropConfigExtension } from '@blocksuite/affine-components/drop-indicator';
|
import { FileDropConfigExtension } from '@blocksuite/affine-components/drop-indicator';
|
||||||
import { AttachmentBlockSchema } from '@blocksuite/affine-model';
|
import { AttachmentBlockSchema } from '@blocksuite/affine-model';
|
||||||
import {
|
import { TelemetryProvider } from '@blocksuite/affine-shared/services';
|
||||||
FileSizeLimitService,
|
|
||||||
TelemetryProvider,
|
|
||||||
} from '@blocksuite/affine-shared/services';
|
|
||||||
import {
|
import {
|
||||||
isInsideEdgelessEditor,
|
isInsideEdgelessEditor,
|
||||||
matchModels,
|
matchModels,
|
||||||
@@ -22,13 +19,10 @@ export const AttachmentDropOption = FileDropConfigExtension({
|
|||||||
);
|
);
|
||||||
if (!attachmentFiles.length) return false;
|
if (!attachmentFiles.length) return false;
|
||||||
|
|
||||||
const maxFileSize = std.store.get(FileSizeLimitService).maxFileSize;
|
|
||||||
|
|
||||||
if (targetModel && !matchModels(targetModel, [SurfaceBlockModel])) {
|
if (targetModel && !matchModels(targetModel, [SurfaceBlockModel])) {
|
||||||
addSiblingAttachmentBlocks(
|
addSiblingAttachmentBlocks(
|
||||||
std.host,
|
std,
|
||||||
attachmentFiles,
|
attachmentFiles,
|
||||||
maxFileSize,
|
|
||||||
targetModel,
|
targetModel,
|
||||||
placement
|
placement
|
||||||
).catch(console.error);
|
).catch(console.error);
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { FileSizeLimitService } from '@blocksuite/affine-shared/services';
|
|
||||||
import { openFileOrFiles } from '@blocksuite/affine-shared/utils';
|
import { openFileOrFiles } from '@blocksuite/affine-shared/utils';
|
||||||
import { type SlashMenuConfig } from '@blocksuite/affine-widget-slash-menu';
|
import { type SlashMenuConfig } from '@blocksuite/affine-widget-slash-menu';
|
||||||
import { ExportToPdfIcon, FileIcon } from '@blocksuite/icons/lit';
|
import { ExportToPdfIcon, FileIcon } from '@blocksuite/icons/lit';
|
||||||
@@ -24,14 +23,8 @@ export const attachmentSlashMenuConfig: SlashMenuConfig = {
|
|||||||
(async () => {
|
(async () => {
|
||||||
const file = await openFileOrFiles();
|
const file = await openFileOrFiles();
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
const maxFileSize = std.store.get(FileSizeLimitService).maxFileSize;
|
|
||||||
await addSiblingAttachmentBlocks(
|
await addSiblingAttachmentBlocks(std, [file], model, 'after');
|
||||||
std.host,
|
|
||||||
[file],
|
|
||||||
maxFileSize,
|
|
||||||
model,
|
|
||||||
'after'
|
|
||||||
);
|
|
||||||
if (model.text?.length === 0) {
|
if (model.text?.length === 0) {
|
||||||
std.store.deleteBlock(model);
|
std.store.deleteBlock(model);
|
||||||
}
|
}
|
||||||
@@ -54,15 +47,7 @@ export const attachmentSlashMenuConfig: SlashMenuConfig = {
|
|||||||
const file = await openFileOrFiles();
|
const file = await openFileOrFiles();
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
|
|
||||||
const maxFileSize = std.store.get(FileSizeLimitService).maxFileSize;
|
await addSiblingAttachmentBlocks(std, [file], model, 'after');
|
||||||
|
|
||||||
await addSiblingAttachmentBlocks(
|
|
||||||
std.host,
|
|
||||||
[file],
|
|
||||||
maxFileSize,
|
|
||||||
model,
|
|
||||||
'after'
|
|
||||||
);
|
|
||||||
if (model.text?.length === 0) {
|
if (model.text?.length === 0) {
|
||||||
std.store.deleteBlock(model);
|
std.store.deleteBlock(model);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { toast } from '@blocksuite/affine-components/toast';
|
import { toast } from '@blocksuite/affine-components/toast';
|
||||||
import type {
|
import {
|
||||||
AttachmentBlockModel,
|
type AttachmentBlockModel,
|
||||||
AttachmentBlockProps,
|
type AttachmentBlockProps,
|
||||||
|
AttachmentBlockSchema,
|
||||||
} from '@blocksuite/affine-model';
|
} from '@blocksuite/affine-model';
|
||||||
import {
|
import {
|
||||||
EMBED_CARD_HEIGHT,
|
EMBED_CARD_HEIGHT,
|
||||||
@@ -12,8 +13,8 @@ import {
|
|||||||
TelemetryProvider,
|
TelemetryProvider,
|
||||||
} from '@blocksuite/affine-shared/services';
|
} from '@blocksuite/affine-shared/services';
|
||||||
import { humanFileSize } from '@blocksuite/affine-shared/utils';
|
import { humanFileSize } from '@blocksuite/affine-shared/utils';
|
||||||
import { Bound, type IVec, Point, Vec } from '@blocksuite/global/gfx';
|
import { Bound, type IVec, Vec } from '@blocksuite/global/gfx';
|
||||||
import type { BlockStdScope, EditorHost } from '@blocksuite/std';
|
import type { BlockStdScope } from '@blocksuite/std';
|
||||||
import { GfxControllerIdentifier } from '@blocksuite/std/gfx';
|
import { GfxControllerIdentifier } from '@blocksuite/std/gfx';
|
||||||
import type { BlockModel } from '@blocksuite/store';
|
import type { BlockModel } from '@blocksuite/store';
|
||||||
|
|
||||||
@@ -34,7 +35,7 @@ function isAttachmentUploading(blockId: string) {
|
|||||||
* This function will not verify the size of the file.
|
* This function will not verify the size of the file.
|
||||||
*/
|
*/
|
||||||
export async function uploadAttachmentBlob(
|
export async function uploadAttachmentBlob(
|
||||||
editorHost: EditorHost,
|
std: BlockStdScope,
|
||||||
blockId: string,
|
blockId: string,
|
||||||
blob: Blob,
|
blob: Blob,
|
||||||
filetype: string,
|
filetype: string,
|
||||||
@@ -44,43 +45,40 @@ export async function uploadAttachmentBlob(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const doc = editorHost.doc;
|
|
||||||
let sourceId: string | undefined;
|
let sourceId: string | undefined;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setAttachmentUploading(blockId);
|
setAttachmentUploading(blockId);
|
||||||
sourceId = await doc.blobSync.set(blob);
|
sourceId = await std.store.blobSync.set(blob);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
toast(
|
toast(
|
||||||
editorHost,
|
std.host,
|
||||||
`Failed to upload attachment! ${error.message || error.toString()}`
|
`Failed to upload attachment! ${error.message || error.toString()}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setAttachmentUploaded(blockId);
|
setAttachmentUploaded(blockId);
|
||||||
|
|
||||||
const block = doc.getBlock(blockId);
|
const block = std.store.getBlock(blockId);
|
||||||
|
|
||||||
doc.withoutTransact(() => {
|
std.store.withoutTransact(() => {
|
||||||
if (!block) return;
|
if (!block) return;
|
||||||
|
|
||||||
doc.updateBlock(block.model, {
|
std.store.updateBlock(block.model, {
|
||||||
sourceId,
|
sourceId,
|
||||||
} satisfies Partial<AttachmentBlockProps>);
|
} satisfies Partial<AttachmentBlockProps>);
|
||||||
});
|
});
|
||||||
|
|
||||||
editorHost.std
|
std.getOptional(TelemetryProvider)?.track('AttachmentUploadedEvent', {
|
||||||
.getOptional(TelemetryProvider)
|
page: `${isEdgeless ? 'whiteboard' : 'doc'} editor`,
|
||||||
?.track('AttachmentUploadedEvent', {
|
module: 'attachment',
|
||||||
page: `${isEdgeless ? 'whiteboard' : 'doc'} editor`,
|
segment: 'attachment',
|
||||||
module: 'attachment',
|
control: 'uploader',
|
||||||
segment: 'attachment',
|
type: filetype,
|
||||||
control: 'uploader',
|
category: block && sourceId ? 'success' : 'failure',
|
||||||
type: filetype,
|
});
|
||||||
category: block && sourceId ? 'success' : 'failure',
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,9 +183,8 @@ export function downloadAttachmentBlob(block: AttachmentBlockComponent) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function getFileType(file: File) {
|
export async function getFileType(file: File) {
|
||||||
if (file.type) {
|
if (file.type) return file.type;
|
||||||
return file.type;
|
|
||||||
}
|
|
||||||
// If the file type is not available, try to get it from the buffer.
|
// If the file type is not available, try to get it from the buffer.
|
||||||
const buffer = await file.arrayBuffer();
|
const buffer = await file.arrayBuffer();
|
||||||
const FileType = await import('file-type');
|
const FileType = await import('file-type');
|
||||||
@@ -199,9 +196,8 @@ export async function getFileType(file: File) {
|
|||||||
* Add a new attachment block before / after the specified block.
|
* Add a new attachment block before / after the specified block.
|
||||||
*/
|
*/
|
||||||
export async function addSiblingAttachmentBlocks(
|
export async function addSiblingAttachmentBlocks(
|
||||||
editorHost: EditorHost,
|
std: BlockStdScope,
|
||||||
files: File[],
|
files: File[],
|
||||||
maxFileSize: number,
|
|
||||||
targetModel: BlockModel,
|
targetModel: BlockModel,
|
||||||
place: 'before' | 'after' = 'after',
|
place: 'before' | 'after' = 'after',
|
||||||
isEmbed?: boolean
|
isEmbed?: boolean
|
||||||
@@ -210,10 +206,11 @@ export async function addSiblingAttachmentBlocks(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const maxFileSize = std.store.get(FileSizeLimitService).maxFileSize;
|
||||||
const isSizeExceeded = files.some(file => file.size > maxFileSize);
|
const isSizeExceeded = files.some(file => file.size > maxFileSize);
|
||||||
if (isSizeExceeded) {
|
if (isSizeExceeded) {
|
||||||
toast(
|
toast(
|
||||||
editorHost,
|
std.host,
|
||||||
`You can only upload files less than ${humanFileSize(
|
`You can only upload files less than ${humanFileSize(
|
||||||
maxFileSize,
|
maxFileSize,
|
||||||
true,
|
true,
|
||||||
@@ -224,29 +221,36 @@ export async function addSiblingAttachmentBlocks(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const doc = targetModel.doc;
|
const doc = targetModel.doc;
|
||||||
|
const flavour = AttachmentBlockSchema.model.flavour;
|
||||||
|
|
||||||
// Get the types of all files
|
const droppedInfos = await Promise.all(
|
||||||
const types = await Promise.all(files.map(file => getFileType(file)));
|
files.map(async file => {
|
||||||
const attachmentBlockProps: (Partial<AttachmentBlockProps> & {
|
const { name, size } = file;
|
||||||
flavour: 'affine:attachment';
|
const type = await getFileType(file);
|
||||||
})[] = files.map((file, index) => ({
|
const props = {
|
||||||
flavour: 'affine:attachment',
|
flavour,
|
||||||
name: file.name,
|
name,
|
||||||
size: file.size,
|
size,
|
||||||
type: types[index],
|
type,
|
||||||
embed: isEmbed,
|
embed: isEmbed,
|
||||||
}));
|
} satisfies Partial<AttachmentBlockProps> & {
|
||||||
|
flavour: typeof flavour;
|
||||||
|
};
|
||||||
|
return { props, file };
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
const blockIds = doc.addSiblingBlocks(
|
const blockIds = doc.addSiblingBlocks(
|
||||||
targetModel,
|
targetModel,
|
||||||
attachmentBlockProps,
|
droppedInfos.map(info => info.props),
|
||||||
place
|
place
|
||||||
);
|
);
|
||||||
|
|
||||||
blockIds.forEach(
|
const uploadPromises = blockIds.map(async (blockId, index) => {
|
||||||
(blockId, index) =>
|
const { props, file } = droppedInfos[index];
|
||||||
void uploadAttachmentBlob(editorHost, blockId, files[index], types[index])
|
await uploadAttachmentBlob(std, blockId, file, props.type);
|
||||||
);
|
});
|
||||||
|
await Promise.all(uploadPromises);
|
||||||
|
|
||||||
return blockIds;
|
return blockIds;
|
||||||
}
|
}
|
||||||
@@ -284,40 +288,38 @@ export async function addAttachments(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const CARD_STACK_GAP = 32;
|
const xy = [x, y];
|
||||||
|
const style = 'cubeThick';
|
||||||
|
const gap = 32;
|
||||||
|
const width = EMBED_CARD_WIDTH.cubeThick;
|
||||||
|
const height = EMBED_CARD_HEIGHT.cubeThick;
|
||||||
|
|
||||||
const dropInfos: { blockId: string; file: File }[] = files.map(
|
const droppedInfos = files.map((file, index) => {
|
||||||
(file, index) => {
|
const { name, size } = file;
|
||||||
const point = new Point(
|
const center = Vec.addScalar(xy, index * gap);
|
||||||
x + index * CARD_STACK_GAP,
|
const xywh = Bound.fromCenter(center, width, height).serialize();
|
||||||
y + index * CARD_STACK_GAP
|
const props = {
|
||||||
);
|
style,
|
||||||
const center = Vec.toVec(point);
|
name,
|
||||||
const bound = Bound.fromCenter(
|
size,
|
||||||
center,
|
xywh,
|
||||||
EMBED_CARD_WIDTH.cubeThick,
|
} satisfies Partial<AttachmentBlockProps>;
|
||||||
EMBED_CARD_HEIGHT.cubeThick
|
|
||||||
);
|
|
||||||
const blockId = std.store.addBlock(
|
|
||||||
'affine:attachment',
|
|
||||||
{
|
|
||||||
name: file.name,
|
|
||||||
size: file.size,
|
|
||||||
type: file.type,
|
|
||||||
style: 'cubeThick',
|
|
||||||
xywh: bound.serialize(),
|
|
||||||
} satisfies Partial<AttachmentBlockProps>,
|
|
||||||
gfx.surface
|
|
||||||
);
|
|
||||||
|
|
||||||
return { blockId, file };
|
return { file, props };
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
// upload file and update the attachment model
|
// upload file and update the attachment model
|
||||||
const uploadPromises = dropInfos.map(async ({ blockId, file }) => {
|
const uploadPromises = droppedInfos.map(async ({ props, file }) => {
|
||||||
const filetype = await getFileType(file);
|
const type = await getFileType(file);
|
||||||
await uploadAttachmentBlob(std.host, blockId, file, filetype, true);
|
|
||||||
|
const blockId = std.store.addBlock(
|
||||||
|
AttachmentBlockSchema.model.flavour,
|
||||||
|
{ ...props, type },
|
||||||
|
gfx.surface
|
||||||
|
);
|
||||||
|
|
||||||
|
await uploadAttachmentBlob(std, blockId, file, type, true);
|
||||||
|
|
||||||
return blockId;
|
return blockId;
|
||||||
});
|
});
|
||||||
const blockIds = await Promise.all(uploadPromises);
|
const blockIds = await Promise.all(uploadPromises);
|
||||||
|
|||||||
@@ -45,7 +45,6 @@ import {
|
|||||||
getTextSelectionCommand,
|
getTextSelectionCommand,
|
||||||
} from '@blocksuite/affine-shared/commands';
|
} from '@blocksuite/affine-shared/commands';
|
||||||
import { REFERENCE_NODE } from '@blocksuite/affine-shared/consts';
|
import { REFERENCE_NODE } from '@blocksuite/affine-shared/consts';
|
||||||
import { FileSizeLimitService } from '@blocksuite/affine-shared/services';
|
|
||||||
import type { AffineTextAttributes } from '@blocksuite/affine-shared/types';
|
import type { AffineTextAttributes } from '@blocksuite/affine-shared/types';
|
||||||
import {
|
import {
|
||||||
createDefaultDoc,
|
createDefaultDoc,
|
||||||
@@ -415,9 +414,7 @@ const contentMediaToolGroup: KeyboardToolPanelGroup = {
|
|||||||
const file = await openFileOrFiles();
|
const file = await openFileOrFiles();
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
|
|
||||||
const maxFileSize = std.store.get(FileSizeLimitService).maxFileSize;
|
await addSiblingAttachmentBlocks(std, [file], model);
|
||||||
|
|
||||||
await addSiblingAttachmentBlocks(std.host, [file], maxFileSize, model);
|
|
||||||
if (model.text?.length === 0) {
|
if (model.text?.length === 0) {
|
||||||
std.store.deleteBlock(model);
|
std.store.deleteBlock(model);
|
||||||
}
|
}
|
||||||
@@ -1015,9 +1012,7 @@ export const defaultKeyboardToolbarConfig: KeyboardToolbarConfig = {
|
|||||||
const file = await openFileOrFiles();
|
const file = await openFileOrFiles();
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
|
|
||||||
const maxFileSize = std.store.get(FileSizeLimitService).maxFileSize;
|
await addSiblingAttachmentBlocks(std, [file], model);
|
||||||
|
|
||||||
await addSiblingAttachmentBlocks(std.host, [file], maxFileSize, model);
|
|
||||||
if (model.text?.length === 0) {
|
if (model.text?.length === 0) {
|
||||||
std.store.deleteBlock(model);
|
std.store.deleteBlock(model);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user