Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ and this project adheres to
### Added

- ✨(api) add API route to fetch document content #1206
- ✨(frontend) doc emojis improvements #1381
- add an EmojiPicker in the document tree and document title
- remove emoji buttons in menus

### Changed

Expand All @@ -30,6 +33,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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 = (
<Box $position="absolute" $zIndex={1000} $margin="2rem 0 0 0">
<Picker
data={emojiData}
Expand All @@ -30,4 +32,27 @@ export const EmojiPicker = ({
/>
</Box>
);

if (withOverlay) {
return (
<>
{/* Overlay transparent pour fermer en cliquant à l'extérieur */}
<div
style={{
position: 'fixed',
top: 0,
left: 0,
width: '100vw',
height: '100vh',
zIndex: 999,
backgroundColor: 'transparent',
}}
onClick={onClickOutside}
/>
{pickerContent}
</>
);
}

return pickerContent;
};
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ export const CalloutBlock = createReactBlockSpec(
emojiData={emojidata}
onClickOutside={onClickOutside}
onEmojiSelect={onEmojiSelect}
withOverlay={true}
/>
)}
<Box as="p" className="inline-content" ref={contentRef} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ import { createReactInlineContentSpec } from '@blocknote/react';
import { useEffect } from 'react';
import { css } from 'styled-components';

import { StyledLink, Text } from '@/components';
import { Box, 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(
{
Expand Down Expand Up @@ -52,6 +52,8 @@ interface LinkSelectedProps {
const LinkSelected = ({ url, title }: LinkSelectedProps) => {
const { colorsTokens } = useCunninghamTheme();

const { emoji, titleWithoutEmoji } = getEmojiAndTitle(title);

return (
<StyledLink
href={url}
Expand All @@ -71,9 +73,20 @@ const LinkSelected = ({ url, title }: LinkSelectedProps) => {
transition: background-color 0.2s ease-in-out;
`}
>
<SelectedPageIcon width={11.5} />
<Box
$display="inline-block"
$css={css`
margin-right: 0.3rem;
`}
>
{emoji ? (
<Icon iconName={emoji} $size="16px" />
) : (
<SelectedPageIcon width={11.5} />
)}
</Box>
<Text $weight="500" spellCheck="false" $size="16px" $display="inline">
{title}
{titleWithoutEmoji}
</Text>
</StyledLink>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -8,13 +7,15 @@ import { Box, Text } from '@/components';
import { useCunninghamTheme } from '@/cunningham';
import {
Doc,
KEY_DOC,
KEY_LIST_DOC,
getEmojiAndTitle,
useDocStore,
useTrans,
useUpdateDoc,
} from '@/docs/doc-management';
import { useBroadcastStore, useResponsiveStore } from '@/stores';
import SimpleFileIcon from '@/features/docs/doc-management/assets/simple-document.svg';
import { useDocTitleUpdate } from '@/features/docs/doc-management/hooks/useDocTitleUpdate';
import { useResponsiveStore } from '@/stores';

import { DocIcon } from '../../doc-management/components/DocIcon';

interface DocTitleProps {
doc: Doc;
Expand Down Expand Up @@ -49,48 +50,26 @@ const DocTitleInput = ({ doc }: DocTitleProps) => {
const { isDesktop } = useResponsiveStore();
const { t } = useTranslation();
const { colorsTokens } = useCunninghamTheme();
const [titleDisplay, setTitleDisplay] = useState(doc.title);
const treeContext = useTreeContext<Doc>();
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.trim()}` : inputText.trim(),
);
const { titleWithoutEmoji: sanitizedTitleWithoutEmoji } =
getEmojiAndTitle(sanitizedTitle);

setTitleDisplay(sanitizedTitleWithoutEmoji);
},
[doc.id, doc.title, updateDoc],
[doc, updateDocTitle, emoji],
);

const handleKeyDown = (e: React.KeyboardEvent) => {
Expand All @@ -101,43 +80,75 @@ const DocTitleInput = ({ doc }: DocTitleProps) => {
};

useEffect(() => {
setTitleDisplay(doc.title);
}, [doc]);
setTitleDisplay(titleWithoutEmoji);
}, [doc, titleWithoutEmoji]);

return (
<Tooltip content={t('Rename')} aria-hidden={true} placement="top">
<Box
as="span"
role="textbox"
className="--docs--doc-title-input"
contentEditable
defaultValue={titleDisplay || undefined}
onKeyDownCapture={handleKeyDown}
suppressContentEditableWarning={true}
aria-label={`${t('Document title')}`}
aria-multiline={false}
onBlurCapture={(event) =>
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;
<Box
$direction="row"
$align="flex-end"
$gap={spacingsTokens['s']}
$minHeight="40px"
>
<Tooltip content={t('Document emoji')} aria-hidden={true} placement="top">
<Box
$css={css`
height: 58px;
cursor: pointer;
`}
>
<DocIcon
emojiPicker
docId={doc.id}
title={doc.title}
emoji={emoji}
$size="50px"
defaultIcon={
<SimpleFileIcon
width="50px"
height="50px"
aria-hidden="true"
aria-label={t('Simple document icon')}
color={colorsTokens['primary-500']}
/>
}
/>
</Box>
</Tooltip>

<Tooltip content={t('Rename')} aria-hidden={true} placement="top">
<Box
as="span"
role="textbox"
className="--docs--doc-title-input"
contentEditable
defaultValue={titleDisplay || undefined}
onKeyDownCapture={handleKeyDown}
suppressContentEditableWarning={true}
aria-label={`${t('Document title')}`}
aria-multiline={false}
onBlurCapture={(event) =>
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}
</Box>
</Tooltip>
$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}
</Box>
</Tooltip>
</Box>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
KEY_DOC,
KEY_LIST_DOC,
ModalRemoveDoc,
getEmojiAndTitle,
useCopyDocLink,
useCreateFavoriteDoc,
useDeleteFavoriteDoc,
Expand All @@ -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;
Expand Down Expand Up @@ -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
? [
Expand Down Expand Up @@ -127,6 +136,15 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
},
testId: `docs-actions-${doc.is_favorite ? 'unpin' : 'pin'}-${doc.id}`,
},
...(emoji
? [
{
label: t('Remove emoji'),
icon: 'emoji_emotions',
callback: removeEmoji,
},
]
: []),
{
label: t('Version history'),
icon: 'history',
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading