diff --git a/CHANGELOG.md b/CHANGELOG.md index b644f79eb0..3620daf456 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to ### Added - ✨(frontend) add pdf block to the editor #1293 +- ✨(frontend) add an EmojiPicker in the document tree and title #1381 ### Changed @@ -44,6 +45,10 @@ and this project adheres to - 🐛(frontend) exclude h4-h6 headings from table of contents #1441 - 🔒(frontend) prevent readers from changing callout emoji #1449 +## Removed + +- 🔥(frontend) remove emoji buttons in doc grid #1419 + ## [3.7.0] - 2025-09-12 ### Added @@ -62,6 +67,8 @@ and this project adheres to - ✨unify tab focus style for better visual consistency #1341 - ♿hide decorative icons, label menus, avoid accessible name… #1362 - ♻️(tilt) use helm dev-backend chart +- 🩹(frontend) on main pages do not display leading emoji as page icon #1381 +- 🩹(frontend) handle properly emojis in interlinking #1381 ### Removed diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/components/EmojiPicker.tsx b/src/frontend/apps/impress/src/features/docs/doc-editor/components/EmojiPicker.tsx index de4a5c90a2..c9246f6589 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-editor/components/EmojiPicker.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/components/EmojiPicker.tsx @@ -9,16 +9,18 @@ interface EmojiPickerProps { emojiData: EmojiMartData; onClickOutside: () => void; onEmojiSelect: ({ native }: { native: string }) => void; + withOverlay?: boolean; } export const EmojiPicker = ({ emojiData, onClickOutside, onEmojiSelect, + withOverlay = false, }: EmojiPickerProps) => { const { i18n } = useTranslation(); - return ( + const pickerContent = ( ); + + if (withOverlay) { + return ( + <> + {/* Overlay transparent pour fermer en cliquant à l'extérieur */} +
+ {pickerContent} + + ); + } + + return pickerContent; }; diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-blocks/CalloutBlock.tsx b/src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-blocks/CalloutBlock.tsx index b76cee1fde..208787ffbf 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-blocks/CalloutBlock.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-blocks/CalloutBlock.tsx @@ -90,6 +90,7 @@ export const CalloutBlock = createReactBlockSpec( emojiData={emojidata} onClickOutside={onClickOutside} onEmojiSelect={onEmojiSelect} + withOverlay={true} /> )} diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-blocks/index.ts b/src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-blocks/index.ts index 7aad893b84..3e7ac610a3 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-blocks/index.ts +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-blocks/index.ts @@ -3,3 +3,4 @@ export * from './CalloutBlock'; export * from './DividerBlock'; export * from './PdfBlock'; export * from './UploadLoaderBlock'; +export { default as emojidata } from './initEmojiCallout'; diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-inline-content/Interlinking/InterlinkingLinkInlineContent.tsx b/src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-inline-content/Interlinking/InterlinkingLinkInlineContent.tsx index 395a77a754..a36d36b67e 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-inline-content/Interlinking/InterlinkingLinkInlineContent.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-inline-content/Interlinking/InterlinkingLinkInlineContent.tsx @@ -3,10 +3,10 @@ import { createReactInlineContentSpec } from '@blocknote/react'; import { useEffect } from 'react'; import { css } from 'styled-components'; -import { StyledLink, Text } from '@/components'; +import { Icon, StyledLink, Text } from '@/components'; import { useCunninghamTheme } from '@/cunningham'; import SelectedPageIcon from '@/docs/doc-editor/assets/doc-selected.svg'; -import { useDoc } from '@/docs/doc-management'; +import { getEmojiAndTitle, useDoc } from '@/docs/doc-management'; export const InterlinkingLinkInlineContent = createReactInlineContentSpec( { @@ -52,6 +52,8 @@ interface LinkSelectedProps { const LinkSelected = ({ url, title }: LinkSelectedProps) => { const { colorsTokens } = useCunninghamTheme(); + const { emoji, titleWithoutEmoji } = getEmojiAndTitle(title); + return ( { transition: background-color 0.2s ease-in-out; `} > - - - {title} + {emoji ? ( + + ) : ( + + )} + + {titleWithoutEmoji} ); diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/components/index.ts b/src/frontend/apps/impress/src/features/docs/doc-editor/components/index.ts index 643b57fa45..3a390c3311 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-editor/components/index.ts +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/components/index.ts @@ -1,2 +1,3 @@ export * from './DocEditor'; +export * from './EmojiPicker'; export * from './custom-blocks/'; 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 42fea42895..b187f595dc 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 @@ -1,4 +1,3 @@ -import { useTreeContext } from '@gouvfr-lasuite/ui-kit'; import { Tooltip } from '@openfun/cunningham-react'; import React, { useCallback, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; @@ -8,14 +7,15 @@ import { Box, Text } from '@/components'; import { useCunninghamTheme } from '@/cunningham'; import { Doc, - KEY_DOC, - KEY_LIST_DOC, + DocIcon, + getEmojiAndTitle, useDocStore, + useDocTitleUpdate, useIsCollaborativeEditable, useTrans, - useUpdateDoc, } from '@/docs/doc-management'; -import { useBroadcastStore, useResponsiveStore } from '@/stores'; +import SimpleFileIcon from '@/features/docs/doc-management/assets/simple-document.svg'; +import { useResponsiveStore } from '@/stores'; interface DocTitleProps { doc: Doc; @@ -53,48 +53,26 @@ const DocTitleInput = ({ doc }: DocTitleProps) => { const { isDesktop } = useResponsiveStore(); const { t } = useTranslation(); const { colorsTokens } = useCunninghamTheme(); - const [titleDisplay, setTitleDisplay] = useState(doc.title); - const treeContext = useTreeContext(); + const { emoji, titleWithoutEmoji } = getEmojiAndTitle(doc.title ?? ''); + const { spacingsTokens } = useCunninghamTheme(); const { untitledDocument } = useTrans(); + const [titleDisplay, setTitleDisplay] = useState(titleWithoutEmoji); - const { broadcast } = useBroadcastStore(); - - const { mutate: updateDoc } = useUpdateDoc({ - listInvalideQueries: [KEY_DOC, KEY_LIST_DOC], - onSuccess(updatedDoc) { - // Broadcast to every user connected to the document - broadcast(`${KEY_DOC}-${updatedDoc.id}`); - - if (!treeContext) { - return; - } - - if (treeContext.root?.id === updatedDoc.id) { - treeContext?.setRoot(updatedDoc); - } else { - treeContext?.treeData.updateNode(updatedDoc.id, updatedDoc); - } - }, - }); + const { updateDocTitle } = useDocTitleUpdate(); const handleTitleSubmit = useCallback( (inputText: string) => { - let sanitizedTitle = inputText.trim(); - sanitizedTitle = sanitizedTitle.replace(/(\r\n|\n|\r)/gm, ''); - - // When blank we set to untitled - if (!sanitizedTitle) { - setTitleDisplay(''); - } - - // If mutation we update - if (sanitizedTitle !== doc.title) { - setTitleDisplay(sanitizedTitle); - updateDoc({ id: doc.id, title: sanitizedTitle }); - } + const sanitizedTitle = updateDocTitle( + doc, + emoji ? `${emoji} ${inputText}` : inputText, + ); + const { titleWithoutEmoji: sanitizedTitleWithoutEmoji } = + getEmojiAndTitle(sanitizedTitle); + + setTitleDisplay(sanitizedTitleWithoutEmoji); }, - [doc.id, doc.title, updateDoc], + [doc, updateDocTitle, emoji], ); const handleKeyDown = (e: React.KeyboardEvent) => { @@ -105,43 +83,82 @@ const DocTitleInput = ({ doc }: DocTitleProps) => { }; useEffect(() => { - setTitleDisplay(doc.title); - }, [doc]); + setTitleDisplay(titleWithoutEmoji); + }, [doc, titleWithoutEmoji]); return ( - - - handleTitleSubmit(event.target.textContent || '') - } - $color={colorsTokens['greyscale-1000']} - $minHeight="40px" - $padding={{ right: 'big' }} - $css={css` - &[contenteditable='true']:empty:not(:focus):before { - content: '${untitledDocument}'; - color: grey; - pointer-events: none; - font-style: italic; + + + + + + + + + handleTitleSubmit(event.target.textContent || '') } - font-size: ${isDesktop - ? css`var(--c--theme--font--sizes--h2)` - : css`var(--c--theme--font--sizes--sm)`}; - font-weight: 700; - outline: none; - `} - > - {titleDisplay} - - + $color={colorsTokens['greyscale-1000']} + $padding={{ right: 'big' }} + $css={css` + &[contenteditable='true']:empty:not(:focus):before { + content: '${untitledDocument}'; + color: grey; + pointer-events: none; + font-style: italic; + } + font-size: ${isDesktop + ? css`var(--c--theme--font--sizes--h2)` + : css`var(--c--theme--font--sizes--sm)`}; + font-weight: 700; + outline: none; + `} + > + {titleDisplay} + + + ); }; 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 7d5eacfedc..a973f66b8d 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 @@ -20,6 +20,7 @@ import { KEY_DOC, KEY_LIST_DOC, ModalRemoveDoc, + getEmojiAndTitle, useCopyDocLink, useCreateFavoriteDoc, useDeleteFavoriteDoc, @@ -33,6 +34,7 @@ import { import { useAnalytics } from '@/libs'; import { useResponsiveStore } from '@/stores'; +import { useDocTitleUpdate } from '../../doc-management/hooks/useDocTitleUpdate'; import { useCopyCurrentEditorToClipboard } from '../hooks/useCopyCurrentEditorToClipboard'; const ModalExport = Export?.ModalExport; @@ -92,6 +94,13 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => { }); }, [selectHistoryModal.isOpen, queryClient]); + // Emoji Management + const { emoji } = getEmojiAndTitle(doc.title ?? ''); + const { updateDocEmoji } = useDocTitleUpdate(); + const removeEmoji = () => { + updateDocEmoji(doc.id, doc.title ?? '', ''); + }; + const options: DropdownMenuOption[] = [ ...(isSmallMobile ? [ @@ -127,6 +136,15 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => { }, testId: `docs-actions-${doc.is_favorite ? 'unpin' : 'pin'}-${doc.id}`, }, + ...(emoji && doc.abilities.partial_update + ? [ + { + label: t('Remove emoji'), + icon: 'emoji_emotions', + callback: removeEmoji, + }, + ] + : []), { label: t('Version history'), icon: 'history', diff --git a/src/frontend/apps/impress/src/features/docs/doc-management/assets/simple-document.svg b/src/frontend/apps/impress/src/features/docs/doc-management/assets/simple-document.svg index ee656f0d47..bddcff8a19 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-management/assets/simple-document.svg +++ b/src/frontend/apps/impress/src/features/docs/doc-management/assets/simple-document.svg @@ -1,6 +1,4 @@ void; }; export const DocIcon = ({ emoji, + withEmojiPicker = false, defaultIcon, $size = 'sm', $variation = '1000', $weight = '400', + docId, + title, + onEmojiUpdate, ...textProps }: DocIconProps) => { - if (!emoji) { - return <>{defaultIcon}; + const { updateDocEmoji } = useDocTitleUpdate(); + + const iconRef = React.useRef(null); + + const [openEmojiPicker, setOpenEmojiPicker] = React.useState(false); + const [pickerPosition, setPickerPosition] = React.useState<{ + top: number; + left: number; + }>({ top: 0, left: 0 }); + + if (!withEmojiPicker && !emoji) { + return defaultIcon; } + const toggleEmojiPicker = (e: React.MouseEvent) => { + if (withEmojiPicker) { + e.stopPropagation(); + e.preventDefault(); + + if (!openEmojiPicker && iconRef.current) { + const rect = iconRef.current.getBoundingClientRect(); + setPickerPosition({ + top: rect.bottom + window.scrollY + 8, + left: rect.left + window.scrollX, + }); + } + + setOpenEmojiPicker(!openEmojiPicker); + } + }; + + const handleEmojiSelect = ({ native }: { native: string }) => { + setOpenEmojiPicker(false); + + // Update document emoji if docId is provided + if (docId && title !== undefined) { + updateDocEmoji(docId, title ?? '', native); + } + + // Call the optional callback + onEmojiUpdate?.(native); + }; + + const handleClickOutside = () => { + setOpenEmojiPicker(false); + }; + return ( - + <> + + {!emoji ? ( + defaultIcon + ) : ( + + )} + + {openEmojiPicker && + createPortal( +
+ +
, + document.body, + )} + ); }; diff --git a/src/frontend/apps/impress/src/features/docs/doc-management/components/SimpleDocItem.tsx b/src/frontend/apps/impress/src/features/docs/doc-management/components/SimpleDocItem.tsx index 41a753a471..517f268938 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-management/components/SimpleDocItem.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-management/components/SimpleDocItem.tsx @@ -4,14 +4,12 @@ import { css } from 'styled-components'; import { Box, Text } from '@/components'; import { useCunninghamTheme } from '@/cunningham'; -import { Doc, getEmojiAndTitle, useTrans } from '@/docs/doc-management'; +import { Doc, useTrans } from '@/docs/doc-management'; import { useResponsiveStore } from '@/stores'; import PinnedDocumentIcon from '../assets/pinned-document.svg'; import SimpleFileIcon from '../assets/simple-document.svg'; -import { DocIcon } from './DocIcon'; - const ItemTextCss = css` overflow: hidden; text-overflow: ellipsis; @@ -38,10 +36,6 @@ export const SimpleDocItem = ({ const { isDesktop } = useResponsiveStore(); const { untitledDocument } = useTrans(); - const { emoji, titleWithoutEmoji: displayTitle } = getEmojiAndTitle( - doc.title || untitledDocument, - ); - return ( ) : ( - @@ -91,7 +81,7 @@ export const SimpleDocItem = ({ $css={ItemTextCss} data-testid="doc-title" > - {displayTitle} + {doc.title || untitledDocument} {(!isDesktop || showAccesses) && ( ({ + useBroadcastStore: () => ({ + broadcast: vi.fn(), + }), +})); + +// Mock useTreeContext +vi.mock('@gouvfr-lasuite/ui-kit', () => ({ + useTreeContext: vi.fn(() => ({ + root: { id: 'test-doc-id', title: 'Test Document' }, + setRoot: vi.fn(), + treeData: { + updateNode: vi.fn(), + }, + })), + TreeProvider: ({ children }: { children: React.ReactNode }) => children, +})); + +// Mock useUpdateDoc +const mockMutate = vi.fn(); +let mockOnSuccess: ((data: any) => void) | undefined; +let mockOnError: ((error: any) => void) | undefined; +let _mockOnMutate: ((variables: any) => void) | undefined; + +vi.mock('@/docs/doc-management', async () => { + const actual = await vi.importActual('@/docs/doc-management'); + return { + ...actual, + useUpdateDoc: vi.fn((config) => { + mockOnSuccess = config?.onSuccess; + mockOnError = config?.onError; + _mockOnMutate = config?.onMutate; + + return { + mutate: mockMutate, + isLoading: false, + isError: false, + error: null, + isSuccess: false, + data: undefined, + }; + }), + KEY_DOC: 'doc', + KEY_LIST_DOC: 'list-doc', + }; +}); + +// Create a simple wrapper for tests +const TestWrapper = ({ children }: { children: React.ReactNode }) => { + return <>{children}; +}; + +describe('useDocTitleUpdate', () => { + beforeEach(() => { + vi.clearAllMocks(); + fetchMock.restore(); + }); + + it('should return the correct functions and state', () => { + const { result } = renderHook(() => useDocTitleUpdate(), { + wrapper: TestWrapper, + }); + + expect(result.current.updateDocTitle).toBeDefined(); + expect(result.current.updateDocEmoji).toBeDefined(); + expect(typeof result.current.updateDocTitle).toBe('function'); + expect(typeof result.current.updateDocEmoji).toBe('function'); + }); + + describe('updateDocTitle', () => { + it('should call updateDoc with sanitized title', () => { + const { result } = renderHook(() => useDocTitleUpdate(), { + wrapper: TestWrapper, + }); + + act(() => { + result.current.updateDocTitle( + { id: 'test-doc-id', title: '' } as Doc, + ' My Document \n\r', + ); + }); + + expect(mockMutate).toHaveBeenCalledWith({ + id: 'test-doc-id', + title: 'My Document', + }); + }); + + it('should handle empty title and not call updateDoc', () => { + const { result } = renderHook(() => useDocTitleUpdate(), { + wrapper: TestWrapper, + }); + + act(() => { + result.current.updateDocTitle( + { id: 'test-doc-id', title: '' } as Doc, + '', + ); + }); + + expect(mockMutate).not.toHaveBeenCalledWith({ + id: 'test-doc-id', + title: '', + }); + }); + + it('should remove newlines and carriage returns', () => { + const { result } = renderHook(() => useDocTitleUpdate(), { + wrapper: TestWrapper, + }); + + act(() => { + result.current.updateDocTitle( + { id: 'test-doc-id', title: '' } as Doc, + 'Title\nwith\r\nnewlines', + ); + }); + + expect(mockMutate).toHaveBeenCalledWith({ + id: 'test-doc-id', + title: 'Titlewithnewlines', + }); + }); + }); + + describe('updateDocEmoji', () => { + it('should call updateDoc with emoji and title without existing emoji', () => { + const { result } = renderHook(() => useDocTitleUpdate(), { + wrapper: TestWrapper, + }); + + act(() => { + result.current.updateDocEmoji('test-doc-id', 'My Document', '🚀'); + }); + + expect(mockMutate).toHaveBeenCalledWith({ + id: 'test-doc-id', + title: '🚀 My Document', + }); + }); + + it('should replace existing emoji with new one', () => { + const { result } = renderHook(() => useDocTitleUpdate(), { + wrapper: TestWrapper, + }); + + act(() => { + result.current.updateDocEmoji('test-doc-id', '📝 My Document', '🚀'); + }); + + expect(mockMutate).toHaveBeenCalledWith({ + id: 'test-doc-id', + title: '🚀 My Document', + }); + }); + + it('should handle title with only emoji', () => { + const { result } = renderHook(() => useDocTitleUpdate(), { + wrapper: TestWrapper, + }); + + act(() => { + result.current.updateDocEmoji('test-doc-id', '📝', '🚀'); + }); + + expect(mockMutate).toHaveBeenCalledWith({ + id: 'test-doc-id', + title: '🚀 ', + }); + }); + + it('should handle empty title', () => { + const { result } = renderHook(() => useDocTitleUpdate(), { + wrapper: TestWrapper, + }); + + act(() => { + result.current.updateDocEmoji('test-doc-id', '', '🚀'); + }); + + expect(mockMutate).toHaveBeenCalledWith({ + id: 'test-doc-id', + title: '🚀 ', + }); + }); + }); + + describe('onSuccess callback', () => { + it('should call onSuccess when provided', async () => { + const onSuccess = vi.fn(); + renderHook(() => useDocTitleUpdate({ onSuccess }), { + wrapper: TestWrapper, + }); + + // Simulate successful mutation + const updatedDoc = { id: 'test-doc-id', title: 'Updated Document' }; + + if (mockOnSuccess) { + mockOnSuccess(updatedDoc); + } + + expect(onSuccess).toHaveBeenCalledWith(updatedDoc); + }); + }); + + describe('onError callback', () => { + it('should call onError when provided', async () => { + const onError = vi.fn(); + renderHook(() => useDocTitleUpdate({ onError }), { + wrapper: TestWrapper, + }); + + const error = new Error('Update failed'); + + if (mockOnError) { + mockOnError(error); + } + + expect(onError).toHaveBeenCalledWith(error); + }); + }); + + describe('mutation configuration', () => { + it('should configure useUpdateDoc with correct parameters', () => { + renderHook(() => useDocTitleUpdate(), { + wrapper: TestWrapper, + }); + + // The mock should have been called with the correct parameters + expect(mockMutate).toBeDefined(); + }); + }); +}); diff --git a/src/frontend/apps/impress/src/features/docs/doc-management/hooks/index.ts b/src/frontend/apps/impress/src/features/docs/doc-management/hooks/index.ts index 930e2d4984..5a71305e4b 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-management/hooks/index.ts +++ b/src/frontend/apps/impress/src/features/docs/doc-management/hooks/index.ts @@ -1,6 +1,8 @@ export * from './useCollaboration'; export * from './useCopyDocLink'; export * from './useCreateChildDocTree'; +export * from './useDocTitleUpdate'; export * from './useDocUtils'; export * from './useIsCollaborativeEditable'; export * from './useTrans'; +export * from './useDocTitleUpdate'; diff --git a/src/frontend/apps/impress/src/features/docs/doc-management/hooks/useDocTitleUpdate.tsx b/src/frontend/apps/impress/src/features/docs/doc-management/hooks/useDocTitleUpdate.tsx new file mode 100644 index 0000000000..7d49874129 --- /dev/null +++ b/src/frontend/apps/impress/src/features/docs/doc-management/hooks/useDocTitleUpdate.tsx @@ -0,0 +1,77 @@ +import { useTreeContext } from '@gouvfr-lasuite/ui-kit'; +import { useCallback } from 'react'; + +import { + Doc, + KEY_DOC, + KEY_LIST_DOC, + getEmojiAndTitle, + useUpdateDoc, +} from '@/docs/doc-management'; +import { useBroadcastStore } from '@/stores'; + +interface UseDocUpdateOptions { + onSuccess?: (updatedDoc: Doc) => void; + onError?: (error: Error) => void; +} + +export const useDocTitleUpdate = (options?: UseDocUpdateOptions) => { + const { broadcast } = useBroadcastStore(); + const treeContext = useTreeContext(); + + const { mutate: updateDoc, ...mutationResult } = useUpdateDoc({ + listInvalideQueries: [KEY_DOC, KEY_LIST_DOC], + onSuccess: (updatedDoc) => { + // Broadcast to every user connected to the document + broadcast(`${KEY_DOC}-${updatedDoc.id}`); + + if (treeContext) { + if (treeContext.root?.id === updatedDoc.id) { + treeContext?.setRoot(updatedDoc); + } else { + treeContext?.treeData.updateNode(updatedDoc.id, updatedDoc); + } + } + + options?.onSuccess?.(updatedDoc); + }, + onError: (error) => { + console.warn('updateDocTitle error', error); + options?.onError?.(error); + }, + }); + + const updateDocTitle = useCallback( + (doc: Doc, title: string) => { + const sanitizedTitle = title.trim().replace(/(\r\n|\n|\r)/gm, ''); + + // When blank we set to untitled + if (!sanitizedTitle) { + updateDoc({ id: doc.id, title: '' }); + return ''; + } + + // If mutation we update + if (sanitizedTitle !== doc.title) { + updateDoc({ id: doc.id, title: sanitizedTitle }); + } + + return sanitizedTitle; + }, + [updateDoc], + ); + + const updateDocEmoji = useCallback( + (docId: string, title: string, emoji: string) => { + const { titleWithoutEmoji } = getEmojiAndTitle(title); + updateDoc({ id: docId, title: `${emoji} ${titleWithoutEmoji}` }); + }, + [updateDoc], + ); + + return { + ...mutationResult, + updateDocTitle, + updateDocEmoji, + }; +}; diff --git a/src/frontend/apps/impress/src/features/docs/doc-tree/components/DocSubPageItem.tsx b/src/frontend/apps/impress/src/features/docs/doc-tree/components/DocSubPageItem.tsx index 0b0ee03842..05e6ea5f02 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-tree/components/DocSubPageItem.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-tree/components/DocSubPageItem.tsx @@ -164,7 +164,14 @@ export const DocSubPageItem = (props: TreeViewNodeProps) => { `} > - } $size="sm" /> + } + $size="sm" + docId={doc.id} + title={doc.title} + /> (); + const { mutate: duplicateDoc } = useDuplicateDoc({ onSuccess: (duplicatedDoc) => { // Reset the tree context root will reset the full tree view. @@ -52,6 +55,13 @@ export const DocTreeItemActions = ({ }, }); + // Emoji Management + const { emoji } = getEmojiAndTitle(doc.title ?? ''); + const { updateDocEmoji } = useDocTitleUpdate(); + const removeEmoji = () => { + updateDocEmoji(doc.id, doc.title ?? '', ''); + }; + const handleDetachDoc = () => { if (!treeContext?.root) { return; @@ -82,6 +92,15 @@ export const DocTreeItemActions = ({ }, ...(!isRoot ? [ + ...(emoji && doc.abilities.partial_update + ? [ + { + label: t('Remove emoji'), + icon: , + callback: removeEmoji, + }, + ] + : []), { label: t('Move to my docs'), isDisabled: doc.user_role !== Role.OWNER, diff --git a/src/frontend/apps/impress/src/i18n/translations.json b/src/frontend/apps/impress/src/i18n/translations.json index 662694431c..3ddbc64d1e 100644 --- a/src/frontend/apps/impress/src/i18n/translations.json +++ b/src/frontend/apps/impress/src/i18n/translations.json @@ -62,6 +62,7 @@ "Document access mode": "Doare moned ar restr", "Document accessible to any connected person": "Restr a c'hall bezañ tizhet gant ne vern piv a vefe kevreet", "Document duplicated successfully!": "Restr eilet gant berzh!", + "Document emoji": "Emoju ar restr", "Document owner": "Perc'henn ar restr", "Document sections": "Kevrennoù ar restr", "Document visibility": "Gwelusted ar restr", @@ -162,6 +163,7 @@ "Reading": "Lenn hepken", "Remove": "Dilemel", "Remove access": "Dilemel ar moned", + "Remove emoji": "Dilemel ar emoju", "Rename": "Adenvel", "Rephrase": "Adformulenniñ", "Request access": "Goulenn mont e-barzh", @@ -285,6 +287,7 @@ "Docs: Your new companion to collaborate on documents efficiently, intuitively, and securely.": "Pages: Ihr neuer Begleiter für eine effiziente, intuitive und sichere Zusammenarbeit bei Dokumenten.", "Document accessible to any connected person": "Dokument für jeden angemeldeten Benutzer zugänglich", "Document duplicated successfully!": "Dokument erfolgreich dupliziert!", + "Document emoji": "Dokument-Emoji", "Document owner": "Besitzer des Dokuments", "Docx": "Docx", "Download": "Herunterladen", @@ -370,6 +373,7 @@ "Reading": "Lesen", "Remove": "Löschen", "Remove access": "Zugriff entziehen", + "Remove emoji": "Emoji entfernen", "Rename": "Umbenennen", "Rephrase": "Umformulieren", "Request access": "Zugriff anfragen", @@ -434,7 +438,8 @@ "Share with {{count}} users_one": "Share with {{count}} user", "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" + "Shared with {{count}} users_other": "Shared with {{count}} users", + "Remove emoji": "Remove emoji" } }, "es": { @@ -488,6 +493,7 @@ "Docs transforms your documents into knowledge bases thanks to subpages, powerful search and the ability to pin your important documents.": "Docs transforma sus documentos en bases de conocimiento gracias a las subpáginas, una potente herramienta de búsqueda y la capacidad de marcar como favorito sus documentos más importantes.", "Docs: Your new companion to collaborate on documents efficiently, intuitively, and securely.": "Docs: su nuevo compañero para colaborar en documentos de forma eficiente, intuitiva y segura.", "Document accessible to any connected person": "Documento accesible a cualquier persona conectada", + "Document emoji": "Emoji del documento", "Document owner": "Propietario del documento", "Docx": "Docx", "Download": "Descargar", @@ -560,6 +566,7 @@ "Reader": "Lector", "Reading": "Lectura", "Remove": "Eliminar", + "Remove emoji": "Eliminar emoji", "Rename": "Cambiar el nombre", "Rephrase": "Reformular", "Request access": "Solicitar acceso", @@ -681,6 +688,7 @@ "Document access mode": "Mode d'accès au document", "Document accessible to any connected person": "Document accessible à toute personne connectée", "Document duplicated successfully!": "Document dupliqué avec succès !", + "Document emoji": "Emoji du document", "Document emoji icon": "Émoticônes du document", "Document owner": "Propriétaire du document", "Document role text": "Texte du rôle du document", @@ -790,6 +798,7 @@ "Reading": "Lecture seule", "Remove": "Supprimer", "Remove access": "Supprimer l'accès", + "Remove emoji": "Supprimer l'emoji", "Rename": "Renommer", "Rephrase": "Reformuler", "Request access": "Demander l'accès", @@ -903,6 +912,7 @@ "Docs transforms your documents into knowledge bases thanks to subpages, powerful search and the ability to pin your important documents.": "Docs trasforma i tuoi documenti in piattaforme di conoscenza grazie alle sotto-pagine, alla ricerca potente e alla capacità di fissare i tuoi documenti importanti.", "Docs: Your new companion to collaborate on documents efficiently, intuitively, and securely.": "Docs: Il tuo nuovo compagno di collaborare sui documenti in modo efficiente, intuitivo e sicuro.", "Document accessible to any connected person": "Documento accessibile a qualsiasi persona collegata", + "Document emoji": "Emoji del documento", "Document owner": "Proprietario del documento", "Docx": "Docx", "Download": "Scarica", @@ -966,6 +976,7 @@ "Reader": "Lettore", "Reading": "Leggendo", "Remove": "Rimuovi", + "Remove emoji": "Rimuovi emoji", "Rename": "Rinomina", "Rephrase": "Riformula", "Restore": "Ripristina", @@ -1054,6 +1065,7 @@ "Docs transforms your documents into knowledge bases thanks to subpages, powerful search and the ability to pin your important documents.": "Documentatie transformeert uw documenten in een kennisbasis, dankzij subpagina's, krachtig zoeken en de mogelijkheid om uw belangrijke documenten te pinnen.", "Docs: Your new companion to collaborate on documents efficiently, intuitively, and securely.": "Docs: Je nieuwe metgezel om efficiënt, intuïtief en veilig samen te werken aan documenten.", "Document accessible to any connected person": "Document is toegankelijk voor ieder verbonden persoon", + "Document emoji": "Document emoji", "Document owner": "Document eigenaar", "Docx": "Docx", "Download": "Download", @@ -1124,6 +1136,7 @@ "Reader": "Lezer", "Reading": "Lezen", "Remove": "Verwijderen", + "Remove emoji": "Emoji verwijderen", "Rename": "Hernoemen", "Rephrase": "Herschrijf", "Restore": "Herstel", @@ -1173,7 +1186,11 @@ "home-content-open-source-part3": "Docs is het resultaat van een gezamenlijke inspanning geleid door de Franse 🇫🇷🥖 <1>(DINUM) en Duitse 🇩🇪🥨 <5>(ZenDiS) overheden." } }, - "pt": { "translation": {} }, + "pt": { + "translation": { + "Remove emoji": "Remover emoji" + } + }, "ru": { "translation": { "\"{{email}}\" is already invited to the document.": "\"{{email}}\" уже имеет приглашение для этого документа.", @@ -1245,6 +1262,7 @@ "Document access mode": "Режим доступа к документу", "Document accessible to any connected person": "Документ доступен всем, кто присоединится", "Document duplicated successfully!": "Документ успешно дублирован!", + "Document emoji": "Эмодзи документа", "Document emoji icon": "Значок эмодзи документа", "Document owner": "Владелец документа", "Document role text": "Текст роли документа", @@ -1354,6 +1372,7 @@ "Reading": "Чтение", "Remove": "Удалить", "Remove access": "Отменить доступ", + "Remove emoji": "Убрать эмодзи", "Rename": "Переименовать", "Rephrase": "Переформулировать", "Request access": "Запрос доступа", @@ -1428,6 +1447,7 @@ "sl": { "translation": { "Load more": "Naloži več", + "Remove emoji": "Odstrani emoji", "Untitled document": "Dokument brez naslova" } }, @@ -1461,7 +1481,8 @@ "This file is flagged as unsafe.": "Denna fil är flaggad som osäker.", "Too many requests. Please wait 60 seconds.": "För många förfrågningar. Vänligen vänta 60 sekunder.", "Use as prompt": "Använd som prompt", - "Warning": "Varning" + "Warning": "Varning", + "Remove emoji": "Ta bort emoji" } }, "tr": { @@ -1488,6 +1509,7 @@ "Docs": "Docs", "Docs Logo": "Docs logosu", "Document accessible to any connected person": "Bağlanan herhangi bir kişi tarafından erişilebilen belge", + "Document emoji": "Belge emojisi", "Docx": "Docx", "Download": "İndir", "Download anyway": "Yine de indir", @@ -1531,7 +1553,8 @@ "Version history": "Sürüm geçmişi", "Warning": "Uyarı", "Write": "Yaz", - "Your {{format}} was downloaded succesfully": "{{format}} indirildi" + "Your {{format}} was downloaded succesfully": "{{format}} indirildi", + "Remove emoji": "Emoji kaldır" } }, "uk": { @@ -1605,6 +1628,7 @@ "Document access mode": "Режим доступу до документа", "Document accessible to any connected person": "Документ, доступний для будь-якої особи, що приєдналася", "Document duplicated successfully!": "Документ успішно продубльовано!", + "Document emoji": "Емодзі документа", "Document emoji icon": "Піктограма emoji документа", "Document owner": "Власник документа", "Document role text": "Текст ролі документа", @@ -1714,6 +1738,7 @@ "Reading": "Читання", "Remove": "Видалити", "Remove access": "Вилучити доступ", + "Remove emoji": "Видалити емодзі", "Rename": "Перейменувати", "Rephrase": "Перефразувати", "Request access": "Запит доступу", @@ -1834,6 +1859,7 @@ "Docs transforms your documents into knowledge bases thanks to subpages, powerful search and the ability to pin your important documents.": "Docs 通过子页面、强大的搜索功能以及固定重要文档的能力,将您的文档转化为知识库。", "Docs: Your new companion to collaborate on documents efficiently, intuitively, and securely.": "Docs 为您提供高效、直观且安全的文档协作解决方案。", "Document accessible to any connected person": "任何来访的人都可以访问文档", + "Document emoji": "文档表情符号", "Document owner": "文档所有者", "Docx": "Doc", "Download": "下载", @@ -1904,6 +1930,7 @@ "Reader": "阅读者", "Reading": "阅读中", "Remove": "移除", + "Remove emoji": "移除表情符号", "Rename": "重命名", "Rephrase": "改写", "Restore": "恢复", diff --git a/src/frontend/apps/impress/src/stores/useBroadcastStore.tsx b/src/frontend/apps/impress/src/stores/useBroadcastStore.tsx index 7e8812f768..74b85b0d96 100644 --- a/src/frontend/apps/impress/src/stores/useBroadcastStore.tsx +++ b/src/frontend/apps/impress/src/stores/useBroadcastStore.tsx @@ -78,11 +78,13 @@ export const useBroadcastStore = create((set, get) => ({ })); }, broadcast: (taskLabel) => { - const { task } = get().tasks[taskLabel]; - if (!task) { + if (!get().tasks[taskLabel]) { console.warn(`Task ${taskLabel} is not defined`); return; } + + const { task } = get().tasks[taskLabel]; + task.push([`broadcast: ${taskLabel}`]); }, }));