From 6107fbb01eb0056e1dc7352b33bab3f59ee3d1a8 Mon Sep 17 00:00:00 2001 From: Anthony LC Date: Fri, 31 Jul 2026 10:21:16 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8(frontend)=20add=20word=20count=20to?= =?UTF-8?q?=20doc=20header=20toolbox?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We can now see the word count in the doc header toolbox. --- CHANGELOG.md | 1 + .../__tests__/app-impress/doc-header.spec.ts | 5 ++++ .../src/features/docs/doc-editor/utils.ts | 10 ++++++++ .../docs/doc-header/components/DocToolBox.tsx | 25 ++++++++++++++++--- .../apps/impress/src/i18n/translations.json | 3 +++ 5 files changed, 41 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ac490d64ab..ae32fa610a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to - ♿️(frontend) restore skip to content link after header redesign #2510 - 🌐(i18n) rename cn_CN to zh_CN, add eo_PL and zh_TW locales #2486 +- ✨(frontend) add word count to doc header toolbox #2549 ### Fixed diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-header.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-header.spec.ts index b3eaab1d60..cd75a059ce 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/doc-header.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-header.spec.ts @@ -69,6 +69,8 @@ test.describe('Doc Header', () => { }) => { await createDoc(page, 'doc-update', browserName, 1); + await writeInEditor({ page, text: 'Hello Content' }); + const card = page.getByLabel( 'It is the card information about the document.', ); @@ -93,6 +95,9 @@ test.describe('Doc Header', () => { await expect( page.getByRole('menuitem', { name: 'Download' }), ).toBeVisible(); + await expect( + page.getByRole('menuitem', { name: 'Word count: 2 words' }), + ).toBeVisible(); }); test('it updates the title doc and check the broadcast', async ({ diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/utils.ts b/src/frontend/apps/impress/src/features/docs/doc-editor/utils.ts index e3e5e92b97..804f29e989 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-editor/utils.ts +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/utils.ts @@ -1,5 +1,15 @@ +import { DocsBlockNoteEditor } from './types'; + const HEX_COLOR_REGEX = /^#[0-9a-fA-F]{6}$/; +export const getWordCount = (editor?: DocsBlockNoteEditor): number => { + const doc = editor?._tiptapEditor?.state.doc; + const text = doc ? doc.textBetween(0, doc.content.size, '\n') : ''; + const trimmed = text.trim(); + + return trimmed ? trimmed.split(/\s+/).length : 0; +}; + export const sanitizeColor = (color: string): string => { return HEX_COLOR_REGEX.test(color) ? color : randomColor(); }; diff --git a/src/frontend/apps/impress/src/features/docs/doc-header/components/DocToolBox.tsx b/src/frontend/apps/impress/src/features/docs/doc-header/components/DocToolBox.tsx index 8ad47dda48..41668a5f0f 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-header/components/DocToolBox.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-header/components/DocToolBox.tsx @@ -7,7 +7,7 @@ import { import { Present } from '@gouvfr-lasuite/ui-kit/icons'; import dynamic from 'next/dynamic'; import { useRouter } from 'next/router'; -import { useState } from 'react'; +import { useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import AddLinkSVG from '@/assets/icons/ui-kit/add_link.svg'; @@ -21,6 +21,8 @@ import KeepOffSVG from '@/assets/icons/ui-kit/keep_off.svg'; import LeaveSVG from '@/assets/icons/ui-kit/leave.svg'; import MarkdownCopySVG from '@/assets/icons/ui-kit/markdown_copy.svg'; import MoreSVG from '@/assets/icons/ui-kit/more_horiz.svg'; +import { useEditorStore } from '@/docs/doc-editor/stores/useEditorStore'; +import { getWordCount } from '@/docs/doc-editor/utils'; import { Doc, KEY_DOC, @@ -102,17 +104,23 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => { const [isModalHistoryOpen, setIsModalHistoryOpen] = useState(false); const [isModalLeaveOpen, setIsModalLeaveOpen] = useState(false); + const { editor } = useEditorStore(); + const wordCount = useMemo( + () => (openDropdown ? getWordCount(editor) : 0), + [openDropdown, editor], + ); + const { restoreFocus, addLastFocus } = useFocusStore(); const { isMobile } = useResponsiveStore(); const copyDocLink = useCopyDocLink(doc.id); - // Deep-link (#2397) and slide/URL sync live in PresenterRoot; here we only - // trigger the manual "Present" action. + const openPresenter = usePresenterStore((state) => state.open); const { mutate: duplicateDoc } = useDuplicateDoc({ onSuccess: (data) => { void router.push(`/docs/${data.id}`); }, }); + const removeFavoriteDoc = useDeleteFavoriteDoc({ listInvalidQueries: [KEY_LIST_DOC, KEY_DOC, KEY_LIST_FAVORITE_DOC], }); @@ -222,6 +230,17 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => { }, isHidden: !doc.abilities.destroy, }, + { + type: 'separator', + }, + { + label: '', + subText: t('Word count: {{count}} words', { + count: wordCount, + description: + 'In the document options menu, showing the number of words in the document.', + }), + }, ]; return ( diff --git a/src/frontend/apps/impress/src/i18n/translations.json b/src/frontend/apps/impress/src/i18n/translations.json index f2a2a6e538..ced73f97ad 100644 --- a/src/frontend/apps/impress/src/i18n/translations.json +++ b/src/frontend/apps/impress/src/i18n/translations.json @@ -895,6 +895,9 @@ "Shared with {{count}} users_many": "Shared with {{count}} users", "Shared with {{count}} users_one": "Shared with {{count}} user", "Shared with {{count}} users_other": "Shared with {{count}} users", + "Word count: {{count}} words_one": "Word count: {{count}} word", + "Word count: {{count}} words_many": "Word count: {{count}} words", + "Word count: {{count}} words_other": "Word count: {{count}} words", "days_many": "days", "days_one": "day", "days_other": "days"