mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-23 17:32:48 +08:00
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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user