(frontend) add "Add Emoji" button to doc options menu

- Add "Add Emoji" button to doc options menu
- Remove default emoji when none selected
- Improve doc options styling
This commit is contained in:
Anthony LC
2025-11-05 10:14:10 +01:00
parent f9ff578c6b
commit 66f83db0e5
4 changed files with 102 additions and 70 deletions

View File

@@ -68,9 +68,18 @@ test.describe('Doc Header', () => {
await createDoc(page, 'doc-update-emoji', browserName, 1); await createDoc(page, 'doc-update-emoji', browserName, 1);
const emojiPicker = page.locator('.--docs--doc-title').getByRole('button'); const emojiPicker = page.locator('.--docs--doc-title').getByRole('button');
const optionMenu = page.getByLabel('Open the document options');
const addEmojiMenuItem = page.getByRole('menuitem', { name: 'Add emoji' });
const removeEmojiMenuItem = page.getByRole('menuitem', {
name: 'Remove emoji',
});
// Top parent should not have emoji picker // Top parent should not have emoji picker
await expect(emojiPicker).toBeHidden(); await expect(emojiPicker).toBeHidden();
await optionMenu.click();
await expect(addEmojiMenuItem).toBeHidden();
await expect(removeEmojiMenuItem).toBeHidden();
await page.keyboard.press('Escape');
const { name: docChild } = await createRootSubPage( const { name: docChild } = await createRootSubPage(
page, page,
@@ -80,13 +89,23 @@ test.describe('Doc Header', () => {
await verifyDocName(page, docChild); await verifyDocName(page, docChild);
await expect(emojiPicker).toBeVisible(); // Emoji picker should be hidden initially
await expect(emojiPicker).toBeHidden();
// Add emoji
await optionMenu.click();
await expect(removeEmojiMenuItem).toBeHidden();
await addEmojiMenuItem.click();
await expect(emojiPicker).toHaveText('📄');
// Change emoji
await emojiPicker.click({ await emojiPicker.click({
delay: 100, delay: 100,
}); });
await page.getByRole('button', { name: '😀' }).first().click(); await page.getByRole('button', { name: '😀' }).first().click();
await expect(emojiPicker).toHaveText('😀'); await expect(emojiPicker).toHaveText('😀');
// Update title
const docTitle = page.getByRole('textbox', { name: 'Document title' }); const docTitle = page.getByRole('textbox', { name: 'Document title' });
await docTitle.fill('Hello Emoji World'); await docTitle.fill('Hello Emoji World');
await docTitle.blur(); await docTitle.blur();
@@ -95,6 +114,12 @@ test.describe('Doc Header', () => {
// Check the tree // Check the tree
const row = await getTreeRow(page, 'Hello Emoji World'); const row = await getTreeRow(page, 'Hello Emoji World');
await expect(row.getByText('😀')).toBeVisible(); await expect(row.getByText('😀')).toBeVisible();
// Remove emoji
await optionMenu.click();
await expect(addEmojiMenuItem).toBeHidden();
await removeEmojiMenuItem.click();
await expect(emojiPicker).toBeHidden();
}); });
test('it deletes the doc', async ({ page, browserName }) => { test('it deletes the doc', async ({ page, browserName }) => {

View File

@@ -352,7 +352,7 @@ test.describe('Doc Tree', () => {
await page.getByRole('menuitem', { name: 'Remove emoji' }).click(); await page.getByRole('menuitem', { name: 'Remove emoji' }).click();
await expect(row.getByText('😀')).toBeHidden(); await expect(row.getByText('😀')).toBeHidden();
await expect(titleEmojiPicker).not.toHaveText('😀'); await expect(titleEmojiPicker).toBeHidden();
}); });
}); });

View File

@@ -55,8 +55,16 @@ const DocTitleEmojiPicker = ({ doc }: DocTitleProps) => {
const { colorsTokens } = useCunninghamTheme(); const { colorsTokens } = useCunninghamTheme();
const { emoji } = getEmojiAndTitle(doc.title ?? ''); const { emoji } = getEmojiAndTitle(doc.title ?? '');
if (!emoji) {
return null;
}
return ( return (
<Tooltip content={t('Document emoji')} aria-hidden={true} placement="top"> <Tooltip
content={t('Edit document emoji')}
aria-hidden={true}
placement="top"
>
<Box <Box
$css={css` $css={css`
padding: 4px; padding: 4px;
@@ -70,11 +78,17 @@ const DocTitleEmojiPicker = ({ doc }: DocTitleProps) => {
`} `}
> >
<DocIcon <DocIcon
buttonProps={{
$width: '32px',
$height: '32px',
$justify: 'space-between',
$align: 'center',
}}
withEmojiPicker={doc.abilities.partial_update} withEmojiPicker={doc.abilities.partial_update}
docId={doc.id} docId={doc.id}
title={doc.title} title={doc.title}
emoji={emoji} emoji={emoji}
$size="25px" $size="23px"
defaultIcon={ defaultIcon={
<SimpleFileIcon <SimpleFileIcon
width="25px" width="25px"
@@ -94,7 +108,6 @@ const DocTitleInput = ({ doc }: DocTitleProps) => {
const { isDesktop } = useResponsiveStore(); const { isDesktop } = useResponsiveStore();
const { t } = useTranslation(); const { t } = useTranslation();
const { colorsTokens } = useCunninghamTheme(); const { colorsTokens } = useCunninghamTheme();
const { spacingsTokens } = useCunninghamTheme();
const { isTopRoot } = useDocUtils(doc); const { isTopRoot } = useDocUtils(doc);
const { untitledDocument } = useTrans(); const { untitledDocument } = useTrans();
const { emoji, titleWithoutEmoji } = getEmojiAndTitle(doc.title ?? ''); const { emoji, titleWithoutEmoji } = getEmojiAndTitle(doc.title ?? '');
@@ -139,19 +152,9 @@ const DocTitleInput = ({ doc }: DocTitleProps) => {
className="--docs--doc-title" className="--docs--doc-title"
$direction="row" $direction="row"
$align="center" $align="center"
$gap={spacingsTokens['xs']} $gap="4px"
$minHeight="40px" $minHeight="40px"
> >
{isTopRoot && (
<SimpleFileIcon
width="25px"
height="25px"
aria-hidden="true"
aria-label={t('Simple document icon')}
color={colorsTokens['primary-500']}
style={{ flexShrink: '0' }}
/>
)}
{!isTopRoot && <DocTitleEmojiPicker doc={doc} />} {!isTopRoot && <DocTitleEmojiPicker doc={doc} />}
<Tooltip content={t('Rename')} aria-hidden={true} placement="top"> <Tooltip content={t('Rename')} aria-hidden={true} placement="top">

View File

@@ -60,7 +60,7 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
const selectHistoryModal = useModal(); const selectHistoryModal = useModal();
const modalShare = useModal(); const modalShare = useModal();
const { isSmallMobile, isDesktop } = useResponsiveStore(); const { isSmallMobile, isMobile } = useResponsiveStore();
const copyDocLink = useCopyDocLink(doc.id); const copyDocLink = useCopyDocLink(doc.id);
const { mutate: duplicateDoc } = useDuplicateDoc({ const { mutate: duplicateDoc } = useDuplicateDoc({
onSuccess: (data) => { onSuccess: (data) => {
@@ -90,28 +90,20 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
const { updateDocEmoji } = useDocTitleUpdate(); const { updateDocEmoji } = useDocTitleUpdate();
const options: DropdownMenuOption[] = [ const options: DropdownMenuOption[] = [
...(isSmallMobile {
? [ label: t('Share'),
{ icon: 'group',
label: t('Share'), callback: modalShare.open,
icon: 'group', show: isSmallMobile,
callback: modalShare.open, },
}, {
{ label: t('Export'),
label: t('Export'), icon: 'download',
icon: 'download', callback: () => {
callback: () => { setIsModalExportOpen(true);
setIsModalExportOpen(true); },
}, show: !!ModalExport && isSmallMobile,
show: !!ModalExport, },
},
{
label: t('Copy link'),
icon: 'add_link',
callback: copyDocLink,
},
]
: []),
{ {
label: doc.is_favorite ? t('Unpin') : t('Pin'), label: doc.is_favorite ? t('Unpin') : t('Pin'),
icon: 'push_pin', icon: 'push_pin',
@@ -124,17 +116,6 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
}, },
testId: `docs-actions-${doc.is_favorite ? 'unpin' : 'pin'}-${doc.id}`, testId: `docs-actions-${doc.is_favorite ? 'unpin' : 'pin'}-${doc.id}`,
}, },
...(emoji && doc.abilities.partial_update && !isTopRoot
? [
{
label: t('Remove emoji'),
icon: 'emoji_emotions',
callback: () => {
updateDocEmoji(doc.id, doc.title ?? '', '');
},
},
]
: []),
{ {
label: t('Version history'), label: t('Version history'),
icon: 'history', icon: 'history',
@@ -142,7 +123,31 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
callback: () => { callback: () => {
selectHistoryModal.open(); selectHistoryModal.open();
}, },
show: isDesktop, show: !isMobile,
showSeparator: isTopRoot ? true : false,
},
{
label: t('Remove emoji'),
icon: 'emoji_emotions',
callback: () => {
updateDocEmoji(doc.id, doc.title ?? '', '');
},
showSeparator: true,
show: !!emoji && doc.abilities.partial_update && !isTopRoot,
},
{
label: t('Add emoji'),
icon: 'emoji_emotions',
callback: () => {
updateDocEmoji(doc.id, doc.title ?? '', '📄');
},
showSeparator: true,
show: !emoji && doc.abilities.partial_update && !isTopRoot,
},
{
label: t('Copy link'),
icon: 'add_link',
callback: copyDocLink,
}, },
{ {
label: t('Copy as {{format}}', { format: 'Markdown' }), label: t('Copy as {{format}}', { format: 'Markdown' }),
@@ -158,6 +163,7 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
void copyCurrentEditorToClipboard('html'); void copyCurrentEditorToClipboard('html');
}, },
show: isFeatureFlagActivated('CopyAsHTML'), show: isFeatureFlagActivated('CopyAsHTML'),
showSeparator: true,
}, },
{ {
label: t('Duplicate'), label: t('Duplicate'),
@@ -170,6 +176,7 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
canSave: doc.abilities.partial_update, canSave: doc.abilities.partial_update,
}); });
}, },
showSeparator: true,
}, },
{ {
label: isChild ? t('Delete sub-document') : t('Delete document'), label: isChild ? t('Delete sub-document') : t('Delete document'),
@@ -224,25 +231,22 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
aria-label={t('Export the document')} aria-label={t('Export the document')}
/> />
)} )}
<DropdownMenu options={options} label={t('Open the document options')}> <DropdownMenu
<IconOptions options={options}
aria-hidden="true" label={t('Open the document options')}
isHorizontal buttonCss={css`
$theme="primary" padding: ${spacingsTokens['xs']};
$padding={{ all: 'xs' }} &:hover {
$css={css` background-color: ${colorsTokens['greyscale-100']};
border-radius: 4px; }
&:hover { ${isSmallMobile
background-color: ${colorsTokens['greyscale-100']}; ? css`
} border: 1px solid ${colorsTokens['greyscale-300']};
${isSmallMobile `
? css` : ''}
padding: 10px; `}
border: 1px solid ${colorsTokens['greyscale-300']}; >
` <IconOptions aria-hidden="true" isHorizontal $theme="primary" />
: ''}
`}
/>
</DropdownMenu> </DropdownMenu>
</Box> </Box>