refactor(editor): simplify attachment and image upload handling (#11987)

Closes: [BS-3303](https://linear.app/affine-design/issue/BS-3303/改進-pack-attachment-props-流程)

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

- **New Features**
  - Enhanced attachment and image uploads with improved file size validation and clearer notifications.
  - Upload telemetry tracking added for attachments to monitor upload success or failure.

- **Refactor**
  - Streamlined and unified the process of adding attachments and images, making uploads more reliable and efficient.
  - Parameter names updated for clarity across attachment and image insertion features.

- **Documentation**
  - Updated API documentation to reflect parameter name changes for consistency.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
fundon
2025-04-28 07:03:30 +00:00
parent 3fdab1bec6
commit 85e40e4026
8 changed files with 250 additions and 307 deletions

View File

@@ -9,6 +9,7 @@ import {
EMBED_CARD_WIDTH,
} from '@blocksuite/affine-shared/consts';
import {
type AttachmentUploadedEvent,
FileSizeLimitService,
TelemetryProvider,
} from '@blocksuite/affine-shared/services';
@@ -18,7 +19,7 @@ import type { BlockStdScope } from '@blocksuite/std';
import { GfxControllerIdentifier } from '@blocksuite/std/gfx';
import type { BlockModel } from '@blocksuite/store';
import type { AttachmentBlockComponent } from './attachment-block.js';
import type { AttachmentBlockComponent } from './attachment-block';
const attachmentUploads = new Set<string>();
export function setAttachmentUploading(blockId: string) {
@@ -34,6 +35,7 @@ function isAttachmentUploading(blockId: string) {
/**
* This function will not verify the size of the file.
*/
// TODO(@fundon): should remove
export async function uploadAttachmentBlob(
std: BlockStdScope,
blockId: string,
@@ -41,9 +43,7 @@ export async function uploadAttachmentBlob(
filetype: string,
isEdgeless?: boolean
): Promise<void> {
if (isAttachmentUploading(blockId)) {
return;
}
if (isAttachmentUploading(blockId)) return;
let sourceId: string | undefined;
@@ -98,6 +98,7 @@ export async function getAttachmentBlob(model: AttachmentBlockModel) {
return blob;
}
// TODO(@fundon): should remove
export async function checkAttachmentBlob(block: AttachmentBlockComponent) {
const model = block.model;
const { id } = model;
@@ -192,6 +193,57 @@ export async function getFileType(file: File) {
return fileType ? fileType.mime : '';
}
function hasExceeded(
std: BlockStdScope,
files: File[],
maxFileSize = std.store.get(FileSizeLimitService).maxFileSize
) {
const exceeded = files.some(file => file.size > maxFileSize);
if (exceeded) {
const size = humanFileSize(maxFileSize, true, 0);
toast(std.host, `You can only upload files less than ${size}`);
}
return exceeded;
}
async function buildPropsWith(
std: BlockStdScope,
file: File,
embed?: boolean,
mode: 'doc' | 'whiteboard' = 'doc'
) {
let type = file.type;
let category: AttachmentUploadedEvent['category'] = 'success';
try {
const { name, size } = file;
const sourceId = await std.store.blobSync.set(file);
type = await getFileType(file);
return {
name,
size,
type,
sourceId,
embed,
} satisfies Partial<AttachmentBlockProps>;
} catch (err) {
category = 'failure';
throw err;
} finally {
std.getOptional(TelemetryProvider)?.track('AttachmentUploadedEvent', {
page: `${mode} editor`,
module: 'attachment',
segment: 'attachment',
control: 'uploader',
type,
category,
});
}
}
/**
* Add a new attachment block before / after the specified block.
*/
@@ -199,59 +251,25 @@ export async function addSiblingAttachmentBlocks(
std: BlockStdScope,
files: File[],
targetModel: BlockModel,
place: 'before' | 'after' = 'after',
isEmbed?: boolean
placement: 'before' | 'after' = 'after',
embed?: boolean
) {
if (!files.length) {
return;
}
if (!files.length) return [];
const maxFileSize = std.store.get(FileSizeLimitService).maxFileSize;
const isSizeExceeded = files.some(file => file.size > maxFileSize);
if (isSizeExceeded) {
toast(
std.host,
`You can only upload files less than ${humanFileSize(
maxFileSize,
true,
0
)}`
);
return;
}
if (hasExceeded(std, files)) return [];
const doc = targetModel.doc;
const flavour = AttachmentBlockSchema.model.flavour;
const droppedInfos = await Promise.all(
files.map(async file => {
const { name, size } = file;
const type = await getFileType(file);
const props = {
flavour,
name,
size,
type,
embed: isEmbed,
} satisfies Partial<AttachmentBlockProps> & {
flavour: typeof flavour;
};
return { props, file };
})
const propsArray = await Promise.all(
files.map(file => buildPropsWith(std, file, embed))
);
const blockIds = doc.addSiblingBlocks(
const blockIds = std.store.addSiblingBlocks(
targetModel,
droppedInfos.map(info => info.props),
place
propsArray.map(props => ({ ...props, flavour })),
placement
);
const uploadPromises = blockIds.map(async (blockId, index) => {
const { props, file } = droppedInfos[index];
await uploadAttachmentBlob(std, blockId, file, props.type);
});
await Promise.all(uploadPromises);
return blockIds;
}
@@ -259,29 +277,21 @@ export async function addAttachments(
std: BlockStdScope,
files: File[],
point?: IVec,
transformPoint?: boolean // determines whether we should use `toModelCoord` to convert the point
shouldTransformPoint?: boolean // determines whether we should use `toModelCoord` to convert the point
): Promise<string[]> {
if (!files.length) return [];
const gfx = std.get(GfxControllerIdentifier);
const maxFileSize = std.store.get(FileSizeLimitService).maxFileSize;
const isSizeExceeded = files.some(file => file.size > maxFileSize);
if (isSizeExceeded) {
toast(
std.host,
`You can only upload files less than ${humanFileSize(
maxFileSize,
true,
0
)}`
);
return [];
}
if (hasExceeded(std, files)) return [];
const propsArray = await Promise.all(
files.map(file => buildPropsWith(std, file, undefined, 'whiteboard'))
);
const gfx = std.get(GfxControllerIdentifier);
let { x, y } = gfx.viewport.center;
if (point) {
let transform = transformPoint ?? true;
if (transform) {
shouldTransformPoint = shouldTransformPoint ?? true;
if (shouldTransformPoint) {
[x, y] = gfx.viewport.toModelCoord(...point);
} else {
[x, y] = point;
@@ -294,35 +304,15 @@ export async function addAttachments(
const width = EMBED_CARD_WIDTH.cubeThick;
const height = EMBED_CARD_HEIGHT.cubeThick;
const droppedInfos = files.map((file, index) => {
const { name, size } = file;
const flavour = AttachmentBlockSchema.model.flavour;
const blocks = propsArray.map((props, index) => {
const center = Vec.addScalar(xy, index * gap);
const xywh = Bound.fromCenter(center, width, height).serialize();
const props = {
style,
name,
size,
xywh,
} satisfies Partial<AttachmentBlockProps>;
return { file, props };
return { flavour, blockProps: { ...props, style, xywh } };
});
// upload file and update the attachment model
const uploadPromises = droppedInfos.map(async ({ props, file }) => {
const type = await getFileType(file);
const blockId = std.store.addBlock(
AttachmentBlockSchema.model.flavour,
{ ...props, type },
gfx.surface
);
await uploadAttachmentBlob(std, blockId, file, type, true);
return blockId;
});
const blockIds = await Promise.all(uploadPromises);
const blockIds = std.store.addBlocks(blocks);
gfx.selection.set({
elements: blockIds,