diff --git a/CHANGELOG.md b/CHANGELOG.md index 53dd6ff375..09108a1f08 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ and this project adheres to ### Fixed +- 🐛(frontend) preserve page titles when adding an emoji - 🐛(frontend) refresh pins after document deletion and restoration - 🐛(frontend) redirect homepage to login when homepage feat is disabled #2521 - 🐛(backend) ignore CSPs for API docs in development diff --git a/src/frontend/apps/impress/src/features/docs/doc-header/__tests__/DocHeaderEmoji.spec.tsx b/src/frontend/apps/impress/src/features/docs/doc-header/__tests__/DocHeaderEmoji.spec.tsx index caa82a8da0..636ff918ed 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-header/__tests__/DocHeaderEmoji.spec.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-header/__tests__/DocHeaderEmoji.spec.tsx @@ -4,12 +4,16 @@ import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'; import { AppWrapper } from '@/tests/utils'; const mockUpdateDocEmoji = vi.fn(); +const mockUpdateDocTitle = vi.fn((_doc: unknown, title: string) => title); vi.mock('@/docs/doc-management', async () => { const actual = await vi.importActual('@/docs/doc-management'); return { ...actual, - useDocTitleUpdate: () => ({ updateDocEmoji: mockUpdateDocEmoji }), + useDocTitleUpdate: () => ({ + updateDocEmoji: mockUpdateDocEmoji, + updateDocTitle: mockUpdateDocTitle, + }), }; }); @@ -33,6 +37,7 @@ describe('DocHeader - Add emoji (April Fools easter egg)', () => { beforeEach(() => { vi.useFakeTimers(); mockUpdateDocEmoji.mockClear(); + mockUpdateDocTitle.mockClear(); }); afterEach(() => { @@ -58,4 +63,23 @@ describe('DocHeader - Add emoji (April Fools easter egg)', () => { ); }); }); + + test('preserves a title changed immediately before adding an emoji', () => { + vi.setSystemTime(new Date('2026-03-30')); + + render(, { + wrapper: AppWrapper, + }); + + const titleInput = screen.getByRole('textbox', { name: 'Document title' }); + titleInput.textContent = 'My new document'; + fireEvent.blur(titleInput); + fireEvent.click(screen.getByRole('button', { name: 'Add icon' })); + + expect(mockUpdateDocEmoji).toHaveBeenCalledWith( + 'doc-1', + 'My new document', + '📄', + ); + }); }); diff --git a/src/frontend/apps/impress/src/features/docs/doc-header/components/DocHeader.tsx b/src/frontend/apps/impress/src/features/docs/doc-header/components/DocHeader.tsx index 6e24e17fbd..364ef76a28 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-header/components/DocHeader.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-header/components/DocHeader.tsx @@ -1,4 +1,5 @@ import { Button } from '@gouvfr-lasuite/cunningham-react'; +import { useEffect, useRef } from 'react'; import { useTranslation } from 'react-i18next'; import { css } from 'styled-components'; @@ -31,6 +32,11 @@ export const DocHeader = ({ doc }: DocHeaderProps) => { const { updateDocEmoji } = useDocTitleUpdate(); const { isTopRoot } = useDocUtils(doc); const displayEmojiButton = doc.abilities.partial_update && !isTopRoot; + const latestTitleRef = useRef(doc.title ?? ''); + + useEffect(() => { + latestTitleRef.current = doc.title ?? ''; + }, [doc.title]); return ( <> @@ -74,10 +80,10 @@ export const DocHeader = ({ doc }: DocHeaderProps) => { const isAprilFools = today.getMonth() === 3 && today.getDate() === 1; emoji - ? updateDocEmoji(doc.id, doc.title ?? '', '') + ? updateDocEmoji(doc.id, latestTitleRef.current, '') : updateDocEmoji( doc.id, - doc.title ?? '', + latestTitleRef.current, isAprilFools ? '🐟' : '📄', ); }} @@ -97,7 +103,12 @@ export const DocHeader = ({ doc }: DocHeaderProps) => { )} - + { + latestTitleRef.current = title; + }} + /> diff --git a/src/frontend/apps/impress/src/features/docs/doc-header/components/DocTitle.tsx b/src/frontend/apps/impress/src/features/docs/doc-header/components/DocTitle.tsx index 23a06c1075..342fb8aad4 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-header/components/DocTitle.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-header/components/DocTitle.tsx @@ -21,9 +21,10 @@ export const CLASS_DOC_TITLE = '--docs--doc-title'; interface DocTitleProps { doc: Doc; + onTitleUpdate?: (title: string) => void; } -export const DocTitle = ({ doc }: DocTitleProps) => { +export const DocTitle = ({ doc, onTitleUpdate }: DocTitleProps) => { const { isEditable, isLoading } = useIsCollaborativeEditable(doc); const readOnly = !doc.abilities.partial_update || !isEditable || isLoading; @@ -31,7 +32,7 @@ export const DocTitle = ({ doc }: DocTitleProps) => { return ; } - return ; + return ; }; export const DocTitleText = () => { @@ -98,7 +99,7 @@ const DocTitleEmojiPicker = ({ doc }: DocTitleProps) => { ); }; -const DocTitleInput = ({ doc }: DocTitleProps) => { +const DocTitleInput = ({ doc, onTitleUpdate }: DocTitleProps) => { const { isSmallMobile } = useResponsiveStore(); const { t } = useTranslation(); const { isTopRoot } = useDocUtils(doc); @@ -116,6 +117,8 @@ const DocTitleInput = ({ doc }: DocTitleProps) => { if (isTopRoot) { const sanitizedTitle = updateDocTitle(doc, inputText); setTitleDisplay(sanitizedTitle); + onTitleUpdate?.(sanitizedTitle); + return sanitizedTitle; } else { const { emoji: pastedEmoji } = getEmojiAndTitle(inputText); @@ -131,9 +134,10 @@ const DocTitleInput = ({ doc }: DocTitleProps) => { getEmojiAndTitle(sanitizedTitle); setTitleDisplay(sanitizedTitleWithoutEmoji); + onTitleUpdate?.(sanitizedTitle); } }, - [updateDocTitle, doc, emoji, isTopRoot], + [updateDocTitle, doc, emoji, isTopRoot, onTitleUpdate], ); const handleKeyDown = (e: React.KeyboardEvent) => {