From 85e40e4026cfe21a8565b8584c62ea799bebb5ad Mon Sep 17 00:00:00 2001 From: fundon Date: Mon, 28 Apr 2025 07:03:30 +0000 Subject: [PATCH] refactor(editor): simplify attachment and image upload handling (#11987) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes: [BS-3303](https://linear.app/affine-design/issue/BS-3303/改進-pack-attachment-props-流程) ## 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. --- .../attachment/src/configs/slash-menu.ts | 4 +- .../affine/blocks/attachment/src/utils.ts | 168 +++++----- .../image/src/commands/insert-images.ts | 50 ++- .../affine/blocks/image/src/image-service.ts | 17 +- blocksuite/affine/blocks/image/src/utils.ts | 306 ++++++++---------- .../mindmap/src/toolbar/basket-elements.ts | 2 +- .../api/@blocksuite/store/classes/Store.md | 4 +- .../framework/store/src/model/store/store.ts | 6 +- 8 files changed, 250 insertions(+), 307 deletions(-) diff --git a/blocksuite/affine/blocks/attachment/src/configs/slash-menu.ts b/blocksuite/affine/blocks/attachment/src/configs/slash-menu.ts index 9b549b1f5..6f6900fd0 100644 --- a/blocksuite/affine/blocks/attachment/src/configs/slash-menu.ts +++ b/blocksuite/affine/blocks/attachment/src/configs/slash-menu.ts @@ -24,7 +24,7 @@ export const attachmentSlashMenuConfig: SlashMenuConfig = { const file = await openFileOrFiles(); if (!file) return; - await addSiblingAttachmentBlocks(std, [file], model, 'after'); + await addSiblingAttachmentBlocks(std, [file], model); if (model.text?.length === 0) { std.store.deleteBlock(model); } @@ -47,7 +47,7 @@ export const attachmentSlashMenuConfig: SlashMenuConfig = { const file = await openFileOrFiles(); if (!file) return; - await addSiblingAttachmentBlocks(std, [file], model, 'after'); + await addSiblingAttachmentBlocks(std, [file], model); if (model.text?.length === 0) { std.store.deleteBlock(model); } diff --git a/blocksuite/affine/blocks/attachment/src/utils.ts b/blocksuite/affine/blocks/attachment/src/utils.ts index b1d039f0d..631d19cb5 100644 --- a/blocksuite/affine/blocks/attachment/src/utils.ts +++ b/blocksuite/affine/blocks/attachment/src/utils.ts @@ -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(); 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 { - 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; + } 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 & { - 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 { 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; - - 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, diff --git a/blocksuite/affine/blocks/image/src/commands/insert-images.ts b/blocksuite/affine/blocks/image/src/commands/insert-images.ts index 4bbfe275c..65645034a 100644 --- a/blocksuite/affine/blocks/image/src/commands/insert-images.ts +++ b/blocksuite/affine/blocks/image/src/commands/insert-images.ts @@ -1,48 +1,40 @@ -import { FileSizeLimitService } from '@blocksuite/affine-shared/services'; import { getImageFilesFromLocal } from '@blocksuite/affine-shared/utils'; import type { Command } from '@blocksuite/std'; import type { BlockModel } from '@blocksuite/store'; -import { addSiblingImageBlock } from '../utils.js'; +import { addSiblingImageBlocks } from '../utils'; export const insertImagesCommand: Command< { selectedModels?: BlockModel[]; removeEmptyLine?: boolean; - place?: 'after' | 'before'; + placement?: 'after' | 'before'; }, { insertedImageIds: Promise; } > = (ctx, next) => { - const { selectedModels, place, removeEmptyLine, std } = ctx; - if (!selectedModels) return; + const { selectedModels, placement, removeEmptyLine, std } = ctx; + if (!selectedModels?.length) return; + + const targetModel = + placement === 'before' + ? selectedModels[0] + : selectedModels[selectedModels.length - 1]; return next({ - insertedImageIds: getImageFilesFromLocal().then(imageFiles => { - if (imageFiles.length === 0) return []; + insertedImageIds: getImageFilesFromLocal() + .then(files => addSiblingImageBlocks(std, files, targetModel, placement)) + .then(result => { + if ( + result.length && + removeEmptyLine && + targetModel.text?.length === 0 + ) { + std.store.deleteBlock(targetModel); + } - if (selectedModels.length === 0) return []; - - const targetModel = - place === 'before' - ? selectedModels[0] - : selectedModels[selectedModels.length - 1]; - - const maxFileSize = std.store.get(FileSizeLimitService).maxFileSize; - - const result = addSiblingImageBlock( - std.host, - imageFiles, - maxFileSize, - targetModel, - place - ); - if (removeEmptyLine && targetModel.text?.length === 0) { - std.store.deleteBlock(targetModel); - } - - return result ?? []; - }), + return result; + }), }); }; diff --git a/blocksuite/affine/blocks/image/src/image-service.ts b/blocksuite/affine/blocks/image/src/image-service.ts index 70cdb6000..e6183f91e 100644 --- a/blocksuite/affine/blocks/image/src/image-service.ts +++ b/blocksuite/affine/blocks/image/src/image-service.ts @@ -1,17 +1,14 @@ import { SurfaceBlockModel } from '@blocksuite/affine-block-surface'; import { FileDropConfigExtension } from '@blocksuite/affine-components/drop-indicator'; import { ImageBlockSchema, MAX_IMAGE_WIDTH } from '@blocksuite/affine-model'; -import { - FileSizeLimitService, - TelemetryProvider, -} from '@blocksuite/affine-shared/services'; +import { TelemetryProvider } from '@blocksuite/affine-shared/services'; import { isInsideEdgelessEditor, matchModels, } from '@blocksuite/affine-shared/utils'; import { GfxControllerIdentifier } from '@blocksuite/std/gfx'; -import { addImages, addSiblingImageBlock } from './utils.js'; +import { addImages, addSiblingImageBlocks } from './utils.js'; export const ImageDropOption = FileDropConfigExtension({ flavour: ImageBlockSchema.model.flavour, @@ -19,15 +16,9 @@ export const ImageDropOption = FileDropConfigExtension({ const imageFiles = files.filter(file => file.type.startsWith('image/')); if (!imageFiles.length) return false; - const maxFileSize = std.store.get(FileSizeLimitService).maxFileSize; - if (targetModel && !matchModels(targetModel, [SurfaceBlockModel])) { - addSiblingImageBlock( - std.host, - imageFiles, - maxFileSize, - targetModel, - placement + addSiblingImageBlocks(std, imageFiles, targetModel, placement).catch( + console.error ); return true; } diff --git a/blocksuite/affine/blocks/image/src/utils.ts b/blocksuite/affine/blocks/image/src/utils.ts index 69f273b16..72812e7d4 100644 --- a/blocksuite/affine/blocks/image/src/utils.ts +++ b/blocksuite/affine/blocks/image/src/utils.ts @@ -1,9 +1,10 @@ import { autoResizeElementsCommand } from '@blocksuite/affine-block-surface'; import { toast } from '@blocksuite/affine-components/toast'; -import type { - AttachmentBlockProps, - ImageBlockModel, - ImageBlockProps, +import { + type AttachmentBlockProps, + type ImageBlockModel, + type ImageBlockProps, + ImageBlockSchema, } from '@blocksuite/affine-model'; import { FileSizeLimitService, @@ -18,7 +19,7 @@ import { transformModel, withTempBlobData, } 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 { BlockSelection, type BlockStdScope, @@ -312,93 +313,6 @@ export async function copyImageBlob( } } -export function shouldResizeImage(node: Node, target: EventTarget | null) { - return !!( - target && - target instanceof HTMLElement && - node.contains(target) && - target.classList.contains('resize') - ); -} - -export function addSiblingImageBlock( - editorHost: EditorHost, - files: File[], - maxFileSize: number, - targetModel: BlockModel, - place: 'after' | 'before' = 'after' -) { - const imageFiles = files.filter(file => file.type.startsWith('image/')); - if (!imageFiles.length) { - return; - } - - const isSizeExceeded = imageFiles.some(file => file.size > maxFileSize); - if (isSizeExceeded) { - toast( - editorHost, - `You can only upload files less than ${humanFileSize( - maxFileSize, - true, - 0 - )}` - ); - return; - } - - const imageBlockProps: Partial & - { - flavour: 'affine:image'; - }[] = imageFiles.map(file => ({ - flavour: 'affine:image', - size: file.size, - })); - - const doc = editorHost.doc; - const blockIds = doc.addSiblingBlocks(targetModel, imageBlockProps, place); - blockIds.forEach( - (blockId, index) => - void uploadBlobForImage(editorHost, blockId, imageFiles[index]) - ); - return blockIds; -} - -export function addImageBlocks( - editorHost: EditorHost, - files: File[], - maxFileSize: number, - parent?: BlockModel | string | null, - parentIndex?: number -) { - const imageFiles = files.filter(file => file.type.startsWith('image/')); - if (!imageFiles.length) { - return; - } - - const isSizeExceeded = imageFiles.some(file => file.size > maxFileSize); - if (isSizeExceeded) { - toast( - editorHost, - `You can only upload files less than ${humanFileSize( - maxFileSize, - true, - 0 - )}` - ); - return; - } - - const doc = editorHost.doc; - const blockIds = imageFiles.map(file => - doc.addBlock('affine:image', { size: file.size }, parent, parentIndex) - ); - blockIds.forEach( - (blockId, index) => - void uploadBlobForImage(editorHost, blockId, imageFiles[index]) - ); - return blockIds; -} - /** * Turn the image block into a attachment block. */ @@ -436,115 +350,171 @@ export async function turnImageIntoCardView( transformModel(model, 'affine:attachment', attachmentProp); } +export function shouldResizeImage(node: Node, target: EventTarget | null) { + return !!( + target && + target instanceof HTMLElement && + node.contains(target) && + target.classList.contains('resize') + ); +} + +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) { + const { size } = file; + const [imageSize, sourceId] = await Promise.all([ + readImageSize(file), + std.store.blobSync.set(file), + ]); + + if (!(imageSize.width * imageSize.height)) { + toast(std.host, 'Failed to read image size, please try another image'); + throw new Error('Failed to read image size'); + } + + return { size, sourceId, ...imageSize } satisfies Partial; +} + +export async function addSiblingImageBlocks( + std: BlockStdScope, + files: File[], + targetModel: BlockModel, + placement: 'after' | 'before' = 'after' +) { + files = files.filter(file => file.type.startsWith('image/')); + if (!files.length) return []; + + if (hasExceeded(std, files)) return []; + + const flavour = ImageBlockSchema.model.flavour; + + const propsArray = await Promise.all( + files.map(file => buildPropsWith(std, file)) + ); + + const blockIds = std.store.addSiblingBlocks( + targetModel, + propsArray.map(props => ({ ...props, flavour })), + placement + ); + + return blockIds; +} + +export async function addImageBlocks( + std: BlockStdScope, + files: File[], + parent?: BlockModel | string | null, + parentIndex?: number +) { + files = files.filter(file => file.type.startsWith('image/')); + if (!files.length) return []; + + if (hasExceeded(std, files)) return []; + + const flavour = ImageBlockSchema.model.flavour; + + const propsArray = await Promise.all( + files.map(file => buildPropsWith(std, file)) + ); + + const blockIds = propsArray.map(props => + std.store.addBlock(flavour, props, parent, parentIndex) + ); + + return blockIds; +} + export async function addImages( std: BlockStdScope, files: File[], options: { point?: IVec; maxWidth?: number; - 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 { - const imageFiles = [...files].filter(file => file.type.startsWith('image/')); - if (!imageFiles.length) return []; + files = files.filter(file => file.type.startsWith('image/')); + if (!files.length) return []; + + if (hasExceeded(std, files)) return []; + + const flavour = ImageBlockSchema.model.flavour; + + const propsArray = await Promise.all( + files.map(file => buildPropsWith(std, file)) + ); const gfx = std.get(GfxControllerIdentifier); + const isMultiple = propsArray.length > 1; + const inTopLeft = isMultiple; + const gap = 32; + const { point, maxWidth, shouldTransformPoint = true } = options; - const maxFileSize = std.store.get(FileSizeLimitService).maxFileSize; - const isSizeExceeded = imageFiles.some(file => file.size > maxFileSize); - if (isSizeExceeded) { - toast( - std.host, - `You can only upload files less than ${humanFileSize( - maxFileSize, - true, - 0 - )}` - ); - return []; - } - - const { point, maxWidth, transformPoint = true } = options; let { x, y } = gfx.viewport.center; if (point) { - if (transformPoint) { + if (shouldTransformPoint) { [x, y] = gfx.viewport.toModelCoord(...point); } else { [x, y] = point; } } - const dropInfos: { point: Point; blockId: string }[] = []; - const IMAGE_STACK_GAP = 32; - const isMultipleFiles = imageFiles.length > 1; - const inTopLeft = isMultipleFiles ? true : false; + const xy = [x, y]; - // create image cards without image data - imageFiles.forEach((file, index) => { - const point = new Point( - x + index * IMAGE_STACK_GAP, - y + index * IMAGE_STACK_GAP - ); - const center = Vec.toVec(point); - const bound = calcBoundByOrigin(center, inTopLeft); - const blockId = std.store.addBlock( - 'affine:image', + const blockIds = propsArray.map((props, index) => { + const center = Vec.addScalar(xy, index * gap); + + // If maxWidth is provided, limit the width of the image to maxWidth + // Otherwise, use the original width + const width = maxWidth ? Math.min(props.width, maxWidth) : props.width; + const height = maxWidth + ? (props.height / props.width) * width + : props.height; + + const xywh = calcBoundByOrigin( + center, + inTopLeft, + width, + height + ).serialize(); + + return std.store.addBlock( + flavour, { - size: file.size, - xywh: bound.serialize(), + ...props, + width, + height, + xywh, index: gfx.layer.generateIndex(), }, gfx.surface ); - dropInfos.push({ point, blockId }); }); - // upload image data and update the image model - const uploadPromises = imageFiles.map(async (file, index) => { - const { point, blockId } = dropInfos[index]; - const block = std.store.getBlock(blockId); - const imageSize = await readImageSize(file); - - if (!imageSize.width || !imageSize.height) { - std.store.deleteBlock(block!.model); - - toast(std.host, 'Failed to read image size, please try another image'); - throw new Error('Failed to read image size'); - } - - const sourceId = await std.store.blobSync.set(file); - - const center = Vec.toVec(point); - // If maxWidth is provided, limit the width of the image to maxWidth - // Otherwise, use the original width - const width = maxWidth - ? Math.min(imageSize.width, maxWidth) - : imageSize.width; - const height = maxWidth - ? (imageSize.height / imageSize.width) * width - : imageSize.height; - const bound = calcBoundByOrigin(center, inTopLeft, width, height); - - std.store.withoutTransact(() => { - gfx.updateElement(blockId, { - sourceId, - ...imageSize, - width, - height, - xywh: bound.serialize(), - } satisfies Partial); - }); - }); - await Promise.all(uploadPromises); - - const blockIds = dropInfos.map(info => info.blockId); gfx.selection.set({ elements: blockIds, editing: false, }); - if (isMultipleFiles) { + + if (isMultiple) { std.command.exec(autoResizeElementsCommand); } + return blockIds; } diff --git a/blocksuite/affine/gfx/mindmap/src/toolbar/basket-elements.ts b/blocksuite/affine/gfx/mindmap/src/toolbar/basket-elements.ts index 92197f2b6..07321a5d0 100644 --- a/blocksuite/affine/gfx/mindmap/src/toolbar/basket-elements.ts +++ b/blocksuite/affine/gfx/mindmap/src/toolbar/basket-elements.ts @@ -170,7 +170,7 @@ export const mediaRender: DraggableTool['render'] = async (bound, edgeless) => { const [id] = await addImages(edgeless.std, [file], { point: [bound.x, bound.y], maxWidth: MAX_IMAGE_WIDTH, - transformPoint: false, + shouldTransformPoint: false, }); if (id) return id; return null; diff --git a/blocksuite/docs/api/@blocksuite/store/classes/Store.md b/blocksuite/docs/api/@blocksuite/store/classes/Store.md index 3e670af59..1745c1b89 100644 --- a/blocksuite/docs/api/@blocksuite/store/classes/Store.md +++ b/blocksuite/docs/api/@blocksuite/store/classes/Store.md @@ -196,7 +196,7 @@ Array of IDs of the newly created blocks ### addSiblingBlocks() -> **addSiblingBlocks**(`targetModel`, `props`, `place`): `string`[] +> **addSiblingBlocks**(`targetModel`, `props`, `placement`): `string`[] Add sibling blocks to the store @@ -214,7 +214,7 @@ The target block model Array of block properties -##### place +##### placement Optional position to place the new blocks ('after' or 'before') diff --git a/blocksuite/framework/store/src/model/store/store.ts b/blocksuite/framework/store/src/model/store/store.ts index 13abd518a..27d17a12b 100644 --- a/blocksuite/framework/store/src/model/store/store.ts +++ b/blocksuite/framework/store/src/model/store/store.ts @@ -764,7 +764,7 @@ export class Store { * Add sibling blocks to the store * @param targetModel - The target block model * @param props - Array of block properties - * @param place - Optional position to place the new blocks ('after' or 'before') + * @param placement - Optional position to place the new blocks ('after' or 'before') * @returns Array of IDs of the newly created blocks * * @category Block CRUD @@ -772,7 +772,7 @@ export class Store { addSiblingBlocks( targetModel: BlockModel, props: Array>, - place: 'after' | 'before' = 'after' + placement: 'after' | 'before' = 'after' ): string[] { if (!props.length) return []; const parent = this.getParent(targetModel); @@ -780,7 +780,7 @@ export class Store { const targetIndex = parent.children.findIndex(({ id }) => id === targetModel.id) ?? 0; - const insertIndex = place === 'before' ? targetIndex : targetIndex + 1; + const insertIndex = placement === 'before' ? targetIndex : targetIndex + 1; if (props.length <= 1) { if (!props[0]?.flavour) return [];