🐛(frontend) preserve title when adding an emoji

The emoji action could reuse a stale document title while a rename was being
submitted, replacing the new title with the emoji alone.

Keep the latest submitted title in the header and cover the interaction
with a regression test.

Signed-off-by: fch-aa <21101725+fch-aa@users.noreply.github.com>
This commit is contained in:
fch-aa
2026-09-07 09:27:57 +02:00
committed by Anthony LC
parent 3c1275c88d
commit 1f126cf629
4 changed files with 51 additions and 8 deletions
+4
View File
@@ -6,6 +6,10 @@ and this project adheres to
## [Unreleased]
### Fixed
- 🐛(frontend) preserve page titles when adding an emoji #2586
## [v5.6.1] - 2026-09-04
### Added
@@ -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(<DocHeader doc={{ ...doc, title: '' }} />, {
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',
'📄',
);
});
});
@@ -1,4 +1,5 @@
import { Button } from '@gouvfr-lasuite/ui-components';
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) => {
</Button>
)}
</Box>
<DocTitle doc={doc} />
<DocTitle
doc={doc}
onTitleUpdate={(title) => {
latestTitleRef.current = title;
}}
/>
<DocHeaderInfo doc={doc} />
</Box>
<HorizontalSeparator $margin={{ top: '24px' }} />
@@ -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 <DocTitleText />;
}
return <DocTitleInput doc={doc} />;
return <DocTitleInput doc={doc} onTitleUpdate={onTitleUpdate} />;
};
export const DocTitleText = () => {
@@ -103,7 +104,7 @@ const DocTitleEmojiPicker = ({ doc }: DocTitleProps) => {
);
};
const DocTitleInput = ({ doc }: DocTitleProps) => {
const DocTitleInput = ({ doc, onTitleUpdate }: DocTitleProps) => {
const { isSmallMobile } = useResponsiveStore();
const { t } = useTranslation();
const { isTopRoot } = useDocUtils(doc);
@@ -121,6 +122,8 @@ const DocTitleInput = ({ doc }: DocTitleProps) => {
if (isTopRoot) {
const sanitizedTitle = updateDocTitle(doc, inputText);
setTitleDisplay(sanitizedTitle);
onTitleUpdate?.(sanitizedTitle);
return sanitizedTitle;
} else {
const { emoji: pastedEmoji } = getEmojiAndTitle(inputText);
@@ -136,9 +139,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) => {