🐛(frontend) keep caption and alignment when replacing an image

The antivirus loader rebuilt the image block with an empty caption and
left alignment, so both were lost after a replace.

Signed-off-by: Ovgodd <c.gromoff@gmail.com>
This commit is contained in:
Ovgodd
2026-09-24 15:29:20 +02:00
parent 139dd17ca0
commit 16db180b99
5 changed files with 181 additions and 3 deletions
+1
View File
@@ -77,6 +77,7 @@ and this project adheres to
### Fixed
- 🐛(frontend) open search results in a new tab with ctrl/cmd+click #2719
- 🐛(frontend) keep caption and alignment when replacing an image #2700
- 🐛(frontend) clear callout background on Backspace #2052
- 🐛(export) keep image aspect ratio in PDF columns #2670
@@ -29,6 +29,11 @@ type UploadLoaderPropSchema = {
readonly default: '';
};
readonly blockUploadUrl: { readonly default: '' };
readonly blockUploadCaption: { readonly default: '' };
readonly blockUploadTextAlignment: {
readonly default: 'left';
readonly values: readonly ['left', 'center', 'right', 'justify'];
};
};
type UploadLoaderBlockConfig = BlockConfig<
@@ -90,9 +95,9 @@ const UploadLoaderBlockComponent = ({
url: `${mediaUrl}${response.file}`,
showPreview: block.props.blockUploadShowPreview,
name: block.props.blockUploadName,
caption: '',
caption: block.props.blockUploadCaption,
backgroundColor: 'default',
textAlignment: 'left',
textAlignment: block.props.blockUploadTextAlignment,
},
} as never,
],
@@ -160,6 +165,11 @@ export const UploadLoaderBlock = createReactBlockSpec(
default: '',
},
blockUploadUrl: { default: '' },
blockUploadCaption: { default: '' },
blockUploadTextAlignment: {
default: 'left',
values: ['left', 'center', 'right', 'justify'],
},
},
content: 'none',
},
@@ -0,0 +1,100 @@
import { createReactBlockSpec } from '@blocknote/react';
import { render, waitFor } from '@testing-library/react';
import { ComponentType } from 'react';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { AppWrapper } from '@/tests/utils';
import { UploadLoaderBlock } from '../UploadLoaderBlock';
const { loopCheckDocMediaStatus, capturedRender } = vi.hoisted(() => ({
loopCheckDocMediaStatus: vi.fn(),
capturedRender: {
current: undefined as ComponentType<Record<string, unknown>> | undefined,
},
}));
vi.mock('../../../api', () => ({
loopCheckDocMediaStatus,
}));
vi.mock('@blocknote/react', async () => {
const actual =
await vi.importActual<typeof import('@blocknote/react')>(
'@blocknote/react',
);
return {
...actual,
createReactBlockSpec: (
config: Parameters<typeof createReactBlockSpec>[0],
implementation: Parameters<typeof createReactBlockSpec>[1],
) => {
capturedRender.current = implementation.render as ComponentType<
Record<string, unknown>
>;
return actual.createReactBlockSpec(config, implementation);
},
};
});
void UploadLoaderBlock;
const analyzeUrl = 'https://docs.example/media-check/abc';
describe('UploadLoaderBlock', () => {
beforeEach(() => {
vi.clearAllMocks();
vi.spyOn(Math, 'random').mockReturnValue(0);
loopCheckDocMediaStatus.mockResolvedValue({
status: 'ready',
file: '/media/doc/photo.png',
});
});
it('restores the caption and alignment once the analysis is done', async () => {
const replaceBlocks = vi.fn();
const Render = capturedRender.current!;
render(
<Render
block={{
id: 'loader',
type: 'uploadLoader',
props: {
information: 'Analyzing file...',
type: 'loading',
blockUploadName: 'photo.png',
blockUploadShowPreview: true,
blockUploadType: 'image',
blockUploadUrl: analyzeUrl,
blockUploadCaption: 'Pont Neuf',
blockUploadTextAlignment: 'center',
},
content: undefined,
children: [],
}}
editor={{
isEditable: true,
replaceBlocks,
}}
/>,
{ wrapper: AppWrapper },
);
await waitFor(() => {
expect(replaceBlocks).toHaveBeenCalledWith(
['loader'],
[
expect.objectContaining({
type: 'image',
props: expect.objectContaining({
caption: 'Pont Neuf',
textAlignment: 'center',
}),
}),
],
);
});
});
});
@@ -4,7 +4,8 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
import { AppWrapper } from '@/tests/utils';
import { useUploadFile } from '../useUploadFile';
import { DocsBlockNoteEditor } from '../../types';
import { useUploadFile, useUploadStatus } from '../useUploadFile';
const { mockToast } = vi.hoisted(() => ({ mockToast: vi.fn() }));
@@ -71,3 +72,41 @@ describe('useUploadFile', () => {
);
});
});
describe('useUploadStatus', () => {
it('keeps the caption and alignment while the new file is analyzed', () => {
const replaceBlocks = vi.fn();
const editor = {
document: [
{
id: 'img',
type: 'image',
props: {
url: 'https://docs.example/media-check/abc',
name: 'photo.png',
showPreview: true,
caption: 'Pont Neuf',
textAlignment: 'center',
},
},
],
replaceBlocks,
onUploadEnd: vi.fn(),
} as unknown as DocsBlockNoteEditor;
renderHook(() => useUploadStatus(editor), { wrapper: AppWrapper });
expect(replaceBlocks).toHaveBeenCalledWith(
['img'],
[
expect.objectContaining({
type: 'uploadLoader',
props: expect.objectContaining({
blockUploadCaption: 'Pont Neuf',
blockUploadTextAlignment: 'center',
}),
}),
],
);
});
});
@@ -16,6 +16,30 @@ import { DocsBlockNoteEditor } from '../types';
const DEFAULT_MAX_FILE_SIZE = 10 * 1024 * 1024; // Default to 10MB
const TEXT_ALIGNMENTS = ['left', 'center', 'right', 'justify'] as const;
type TextAlignment = (typeof TEXT_ALIGNMENTS)[number];
const fileCaption = (props: object) => {
if ('caption' in props && typeof props.caption === 'string') {
return props.caption;
}
return '';
};
const fileTextAlignment = (props: object): TextAlignment => {
if (
'textAlignment' in props &&
typeof props.textAlignment === 'string' &&
TEXT_ALIGNMENTS.includes(props.textAlignment as TextAlignment)
) {
return props.textAlignment as TextAlignment;
}
return 'left';
};
export const useUploadFile = (docId: string) => {
const { t } = useTranslation();
const { toast } = useToast();
@@ -98,6 +122,8 @@ export const useUploadStatus = (editor: DocsBlockNoteEditor) => {
const blockUploadName = block.props.name;
const blockUploadShowPreview =
('showPreview' in block.props && block.props.showPreview) || false;
const blockUploadCaption = fileCaption(block.props);
const blockUploadTextAlignment = fileTextAlignment(block.props);
try {
editor.replaceBlocks(
@@ -112,6 +138,8 @@ export const useUploadStatus = (editor: DocsBlockNoteEditor) => {
blockUploadType,
blockUploadUrl,
blockUploadShowPreview,
blockUploadCaption,
blockUploadTextAlignment,
},
},
],