diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ef9fac21..bb8f6c7ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-blocks/UploadLoaderBlock.tsx b/src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-blocks/UploadLoaderBlock.tsx index dc81107b9..019a2af88 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-blocks/UploadLoaderBlock.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-blocks/UploadLoaderBlock.tsx @@ -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', }, diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-blocks/__tests__/UploadLoaderBlock.test.tsx b/src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-blocks/__tests__/UploadLoaderBlock.test.tsx new file mode 100644 index 000000000..21a9ea30f --- /dev/null +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-blocks/__tests__/UploadLoaderBlock.test.tsx @@ -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> | undefined, + }, +})); + +vi.mock('../../../api', () => ({ + loopCheckDocMediaStatus, +})); + +vi.mock('@blocknote/react', async () => { + const actual = + await vi.importActual( + '@blocknote/react', + ); + + return { + ...actual, + createReactBlockSpec: ( + config: Parameters[0], + implementation: Parameters[1], + ) => { + capturedRender.current = implementation.render as ComponentType< + Record + >; + 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( + , + { wrapper: AppWrapper }, + ); + + await waitFor(() => { + expect(replaceBlocks).toHaveBeenCalledWith( + ['loader'], + [ + expect.objectContaining({ + type: 'image', + props: expect.objectContaining({ + caption: 'Pont Neuf', + textAlignment: 'center', + }), + }), + ], + ); + }); + }); +}); diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/hook/__tests__/useUploadFile.test.tsx b/src/frontend/apps/impress/src/features/docs/doc-editor/hook/__tests__/useUploadFile.test.tsx index 18cd282a1..366816054 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-editor/hook/__tests__/useUploadFile.test.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/hook/__tests__/useUploadFile.test.tsx @@ -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', + }), + }), + ], + ); + }); +}); diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/hook/useUploadFile.tsx b/src/frontend/apps/impress/src/features/docs/doc-editor/hook/useUploadFile.tsx index 062dca223..9c0fff081 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-editor/hook/useUploadFile.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/hook/useUploadFile.tsx @@ -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, }, }, ],