(frontend) use title first emoji as doc icon in tree

Implemented emoji detection system, new DocIcon component.
This commit is contained in:
Olivier Laurendeau
2025-08-29 10:25:40 +02:00
committed by Anthony LC
parent 0b64417058
commit d1cbdfd819
13 changed files with 397 additions and 14 deletions

View File

@@ -11,6 +11,7 @@ and this project adheres to
### Added ### Added
- 👷(CI) add bundle size check job #1268 - 👷(CI) add bundle size check job #1268
- ✨(frontend) use title first emoji as doc icon in tree
### Changed ### Changed

View File

@@ -406,6 +406,10 @@ run-frontend-development: ## Run the frontend in development mode
cd $(PATH_FRONT_IMPRESS) && yarn dev cd $(PATH_FRONT_IMPRESS) && yarn dev
.PHONY: run-frontend-development .PHONY: run-frontend-development
frontend-test: ## Run the frontend tests
cd $(PATH_FRONT_IMPRESS) && yarn test
.PHONY: frontend-test
frontend-i18n-extract: ## Extract the frontend translation inside a json to be used for crowdin frontend-i18n-extract: ## Extract the frontend translation inside a json to be used for crowdin
cd $(PATH_FRONT) && yarn i18n:extract cd $(PATH_FRONT) && yarn i18n:extract
.PHONY: frontend-i18n-extract .PHONY: frontend-i18n-extract

View File

@@ -140,6 +140,12 @@ To start all the services, except the frontend container, you can use the follow
$ make run-backend $ make run-backend
``` ```
To execute frontend tests & linting only
```shellscript
$ make frontend-test
$ make frontend-lint
```
**Adding content** **Adding content**
You can create a basic demo site by running this command: You can create a basic demo site by running this command:

View File

@@ -61,6 +61,31 @@ test.describe('Doc Header', () => {
await verifyDocName(page, 'Hello World'); await verifyDocName(page, 'Hello World');
}); });
test('it updates the title doc adding a leading emoji', async ({
page,
browserName,
}) => {
await createDoc(page, 'doc-update', browserName, 1);
const docTitle = page.getByRole('textbox', { name: 'doc title input' });
await expect(docTitle).toBeVisible();
await docTitle.fill('👍 Hello Emoji World');
await docTitle.blur();
await verifyDocName(page, '👍 Hello Emoji World');
// Check the tree
const docTree = page.getByTestId('doc-tree');
await expect(docTree.getByText('Hello Emoji World')).toBeVisible();
await expect(docTree.getByLabel('Document emoji icon')).toBeVisible();
await expect(docTree.getByLabel('Simple document icon')).toBeHidden();
await page.getByTestId('home-button').click();
// Check the documents grid
const gridRow = await getGridRow(page, 'Hello Emoji World');
await expect(gridRow.getByLabel('Document emoji icon')).toBeVisible();
await expect(gridRow.getByLabel('Simple document icon')).toBeHidden();
});
test('it deletes the doc', async ({ page, browserName }) => { test('it deletes the doc', async ({ page, browserName }) => {
const [randomDoc] = await createDoc(page, 'doc-delete', browserName, 1); const [randomDoc] = await createDoc(page, 'doc-delete', browserName, 1);

View File

@@ -136,9 +136,11 @@ export const getGridRow = async (page: Page, title: string) => {
const rows = docsGrid.getByRole('row'); const rows = docsGrid.getByRole('row');
const row = rows.filter({ const row = rows
hasText: title, .filter({
}); hasText: title,
})
.first();
await expect(row).toBeVisible(); await expect(row).toBeVisible();

View File

@@ -41,6 +41,7 @@
"crisp-sdk-web": "1.0.25", "crisp-sdk-web": "1.0.25",
"docx": "9.5.0", "docx": "9.5.0",
"emoji-mart": "5.6.0", "emoji-mart": "5.6.0",
"emoji-regex": "10.4.0",
"i18next": "25.3.2", "i18next": "25.3.2",
"i18next-browser-languagedetector": "8.2.0", "i18next-browser-languagedetector": "8.2.0",
"idb": "8.0.3", "idb": "8.0.3",

View File

@@ -0,0 +1,264 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import * as Y from 'yjs';
import { LinkReach, LinkRole, Role } from '../types';
import {
base64ToBlocknoteXmlFragment,
base64ToYDoc,
currentDocRole,
getDocLinkReach,
getDocLinkRole,
getEmojiAndTitle,
} from '../utils';
// Mock Y.js
vi.mock('yjs', () => ({
Doc: vi.fn().mockImplementation(() => ({
getXmlFragment: vi.fn().mockReturnValue('mocked-xml-fragment'),
})),
applyUpdate: vi.fn(),
}));
describe('doc-management utils', () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe('currentDocRole', () => {
it('should return OWNER when destroy ability is true', () => {
const abilities = {
destroy: true,
accesses_manage: false,
partial_update: false,
} as any;
const result = currentDocRole(abilities);
expect(result).toBe(Role.OWNER);
});
it('should return ADMIN when accesses_manage ability is true and destroy is false', () => {
const abilities = {
destroy: false,
accesses_manage: true,
partial_update: false,
} as any;
const result = currentDocRole(abilities);
expect(result).toBe(Role.ADMIN);
});
it('should return EDITOR when partial_update ability is true and higher abilities are false', () => {
const abilities = {
destroy: false,
accesses_manage: false,
partial_update: true,
} as any;
const result = currentDocRole(abilities);
expect(result).toBe(Role.EDITOR);
});
it('should return READER when no higher abilities are true', () => {
const abilities = {
destroy: false,
accesses_manage: false,
partial_update: false,
} as any;
const result = currentDocRole(abilities);
expect(result).toBe(Role.READER);
});
});
describe('base64ToYDoc', () => {
it('should convert base64 string to Y.Doc', () => {
const base64String = 'dGVzdA=='; // "test" in base64
const mockYDoc = { getXmlFragment: vi.fn() };
(Y.Doc as any).mockReturnValue(mockYDoc);
const result = base64ToYDoc(base64String);
expect(Y.Doc).toHaveBeenCalled();
expect(Y.applyUpdate).toHaveBeenCalledWith(mockYDoc, expect.any(Buffer));
expect(result).toBe(mockYDoc);
});
it('should handle empty base64 string', () => {
const base64String = '';
const mockYDoc = { getXmlFragment: vi.fn() };
(Y.Doc as any).mockReturnValue(mockYDoc);
const result = base64ToYDoc(base64String);
expect(Y.Doc).toHaveBeenCalled();
expect(Y.applyUpdate).toHaveBeenCalledWith(mockYDoc, expect.any(Buffer));
expect(result).toBe(mockYDoc);
});
});
describe('base64ToBlocknoteXmlFragment', () => {
it('should convert base64 to Blocknote XML fragment', () => {
const base64String = 'dGVzdA==';
const mockYDoc = {
getXmlFragment: vi.fn().mockReturnValue('mocked-xml-fragment'),
};
(Y.Doc as any).mockReturnValue(mockYDoc);
const result = base64ToBlocknoteXmlFragment(base64String);
expect(Y.Doc).toHaveBeenCalled();
expect(Y.applyUpdate).toHaveBeenCalledWith(mockYDoc, expect.any(Buffer));
expect(mockYDoc.getXmlFragment).toHaveBeenCalledWith('document-store');
expect(result).toBe('mocked-xml-fragment');
});
});
describe('getDocLinkReach', () => {
it('should return computed_link_reach when available', () => {
const doc = {
computed_link_reach: LinkReach.PUBLIC,
link_reach: LinkReach.RESTRICTED,
} as any;
const result = getDocLinkReach(doc);
expect(result).toBe(LinkReach.PUBLIC);
});
it('should fallback to link_reach when computed_link_reach is not available', () => {
const doc = {
link_reach: LinkReach.AUTHENTICATED,
} as any;
const result = getDocLinkReach(doc);
expect(result).toBe(LinkReach.AUTHENTICATED);
});
it('should handle undefined computed_link_reach', () => {
const doc = {
computed_link_reach: undefined,
link_reach: LinkReach.RESTRICTED,
} as any;
const result = getDocLinkReach(doc);
expect(result).toBe(LinkReach.RESTRICTED);
});
});
describe('getDocLinkRole', () => {
it('should return computed_link_role when available', () => {
const doc = {
computed_link_role: LinkRole.EDITOR,
link_role: LinkRole.READER,
} as any;
const result = getDocLinkRole(doc);
expect(result).toBe(LinkRole.EDITOR);
});
it('should fallback to link_role when computed_link_role is not available', () => {
const doc = {
link_role: LinkRole.READER,
} as any;
const result = getDocLinkRole(doc);
expect(result).toBe(LinkRole.READER);
});
it('should handle undefined computed_link_role', () => {
const doc = {
computed_link_role: undefined,
link_role: LinkRole.EDITOR,
} as any;
const result = getDocLinkRole(doc);
expect(result).toBe(LinkRole.EDITOR);
});
});
describe('getEmojiAndTitle', () => {
it('should extract emoji and title when emoji is present at the beginning', () => {
const title = '🚀 My Awesome Document';
const result = getEmojiAndTitle(title);
expect(result.emoji).toBe('🚀');
expect(result.titleWithoutEmoji).toBe('My Awesome Document');
});
it('should handle complex emojis with modifiers', () => {
const title = '👨‍💻 Developer Notes';
const result = getEmojiAndTitle(title);
expect(result.emoji).toBe('👨‍💻');
expect(result.titleWithoutEmoji).toBe('Developer Notes');
});
it('should handle emojis with skin tone modifiers', () => {
const title = '👍 Great Work!';
const result = getEmojiAndTitle(title);
expect(result.emoji).toBe('👍');
expect(result.titleWithoutEmoji).toBe('Great Work!');
});
it('should return null emoji and full title when no emoji is present', () => {
const title = 'Document Without Emoji';
const result = getEmojiAndTitle(title);
expect(result.emoji).toBeNull();
expect(result.titleWithoutEmoji).toBe('Document Without Emoji');
});
it('should handle empty title', () => {
const title = '';
const result = getEmojiAndTitle(title);
expect(result.emoji).toBeNull();
expect(result.titleWithoutEmoji).toBe('');
});
it('should handle title with only emoji', () => {
const title = '📝';
const result = getEmojiAndTitle(title);
expect(result.emoji).toBe('📝');
expect(result.titleWithoutEmoji).toBe('');
});
it('should handle title with emoji in the middle (should not extract)', () => {
const title = 'My 📝 Document';
const result = getEmojiAndTitle(title);
expect(result.emoji).toBeNull();
expect(result.titleWithoutEmoji).toBe('My 📝 Document');
});
it('should handle title with multiple emojis at the beginning', () => {
const title = '🚀📚 Project Documentation';
const result = getEmojiAndTitle(title);
expect(result.emoji).toBe('🚀');
expect(result.titleWithoutEmoji).toBe('📚 Project Documentation');
});
});
});

View File

@@ -0,0 +1,36 @@
import { useTranslation } from 'react-i18next';
import { Text, TextType } from '@/components';
type DocIconProps = TextType & {
emoji?: string | null;
defaultIcon: React.ReactNode;
};
export const DocIcon = ({
emoji,
defaultIcon,
$size = 'sm',
$variation = '1000',
$weight = '400',
...textProps
}: DocIconProps) => {
const { t } = useTranslation();
if (!emoji) {
return <>{defaultIcon}</>;
}
return (
<Text
{...textProps}
$size={$size}
$variation={$variation}
$weight={$weight}
aria-hidden="true"
aria-label={t('Document emoji icon')}
>
{emoji}
</Text>
);
};

View File

@@ -4,12 +4,14 @@ import { css } from 'styled-components';
import { Box, Text } from '@/components'; import { Box, Text } from '@/components';
import { useCunninghamTheme } from '@/cunningham'; import { useCunninghamTheme } from '@/cunningham';
import { Doc, useTrans } from '@/docs/doc-management'; import { Doc, getEmojiAndTitle, useTrans } from '@/docs/doc-management';
import { useResponsiveStore } from '@/stores'; import { useResponsiveStore } from '@/stores';
import PinnedDocumentIcon from '../assets/pinned-document.svg'; import PinnedDocumentIcon from '../assets/pinned-document.svg';
import SimpleFileIcon from '../assets/simple-document.svg'; import SimpleFileIcon from '../assets/simple-document.svg';
import { DocIcon } from './DocIcon';
const ItemTextCss = css` const ItemTextCss = css`
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
@@ -36,6 +38,10 @@ export const SimpleDocItem = ({
const { isDesktop } = useResponsiveStore(); const { isDesktop } = useResponsiveStore();
const { untitledDocument } = useTrans(); const { untitledDocument } = useTrans();
const { emoji, titleWithoutEmoji: displayTitle } = getEmojiAndTitle(
doc.title || untitledDocument,
);
return ( return (
<Box <Box
$direction="row" $direction="row"
@@ -61,23 +67,29 @@ export const SimpleDocItem = ({
color={colorsTokens['primary-500']} color={colorsTokens['primary-500']}
/> />
) : ( ) : (
<SimpleFileIcon <DocIcon
aria-hidden="true" emoji={emoji}
aria-label={t('Simple document icon')} defaultIcon={
color={colorsTokens['primary-500']} <SimpleFileIcon
aria-hidden="true"
aria-label={t('Simple document icon')}
color={colorsTokens['primary-500']}
/>
}
$size="25px"
/> />
)} )}
</Box> </Box>
<Box $justify="center" $overflow="auto"> <Box $justify="center" $overflow="auto">
<Text <Text
aria-describedby="doc-title" aria-describedby="doc-title"
aria-label={doc.title} aria-label={displayTitle}
$size="sm" $size="sm"
$variation="1000" $variation="1000"
$weight="500" $weight="500"
$css={ItemTextCss} $css={ItemTextCss}
> >
{doc.title || untitledDocument} {displayTitle}
</Text> </Text>
{(!isDesktop || showAccesses) && ( {(!isDesktop || showAccesses) && (
<Box <Box

View File

@@ -1,3 +1,4 @@
import emojiRegex from 'emoji-regex';
import * as Y from 'yjs'; import * as Y from 'yjs';
import { Doc, LinkReach, LinkRole, Role } from './types'; import { Doc, LinkReach, LinkRole, Role } from './types';
@@ -30,3 +31,19 @@ export const getDocLinkReach = (doc: Doc): LinkReach => {
export const getDocLinkRole = (doc: Doc): LinkRole => { export const getDocLinkRole = (doc: Doc): LinkRole => {
return doc.computed_link_role ?? doc.link_role; return doc.computed_link_role ?? doc.link_role;
}; };
export const getEmojiAndTitle = (title: string) => {
// Use emoji-regex library for comprehensive emoji detection compatible with ES5
const regex = emojiRegex();
// Check if the title starts with an emoji
const match = title.match(regex);
if (match && title.startsWith(match[0])) {
const emoji = match[0];
const titleWithoutEmoji = title.substring(emoji.length).trim();
return { emoji, titleWithoutEmoji };
}
return { emoji: null, titleWithoutEmoji: title };
};

View File

@@ -9,7 +9,12 @@ import { css } from 'styled-components';
import { Box, Icon, Text } from '@/components'; import { Box, Icon, Text } from '@/components';
import { useCunninghamTheme } from '@/cunningham'; import { useCunninghamTheme } from '@/cunningham';
import { Doc, useTrans } from '@/features/docs/doc-management'; import {
Doc,
getEmojiAndTitle,
useTrans,
} from '@/features/docs/doc-management';
import { DocIcon } from '@/features/docs/doc-management/components/DocIcon';
import { useLeftPanelStore } from '@/features/left-panel'; import { useLeftPanelStore } from '@/features/left-panel';
import { useResponsiveStore } from '@/stores'; import { useResponsiveStore } from '@/stores';
@@ -38,6 +43,9 @@ export const DocSubPageItem = (props: TreeViewNodeProps<Doc>) => {
const router = useRouter(); const router = useRouter();
const { togglePanel } = useLeftPanelStore(); const { togglePanel } = useLeftPanelStore();
const { emoji, titleWithoutEmoji } = getEmojiAndTitle(doc.title || '');
const displayTitle = titleWithoutEmoji || untitledDocument;
const afterCreate = (createdDoc: Doc) => { const afterCreate = (createdDoc: Doc) => {
const actualChildren = node.data.children ?? []; const actualChildren = node.data.children ?? [];
@@ -122,7 +130,7 @@ export const DocSubPageItem = (props: TreeViewNodeProps<Doc>) => {
$minHeight="24px" $minHeight="24px"
> >
<Box $width="16px" $height="16px"> <Box $width="16px" $height="16px">
<SubPageIcon /> <DocIcon emoji={emoji} defaultIcon={<SubPageIcon />} $size="sm" />
</Box> </Box>
<Box <Box
@@ -137,7 +145,7 @@ export const DocSubPageItem = (props: TreeViewNodeProps<Doc>) => {
`} `}
> >
<Text $css={ItemTextCss} $size="sm" $variation="1000"> <Text $css={ItemTextCss} $size="sm" $variation="1000">
{doc.title || untitledDocument} {displayTitle}
</Text> </Text>
{doc.nb_accesses_direct >= 1 && ( {doc.nb_accesses_direct >= 1 && (
<Icon <Icon

View File

@@ -42,6 +42,7 @@
"Docs Logo": "Logo Docs", "Docs Logo": "Logo Docs",
"Document accessible to any connected person": "Restr a c'hall bezañ tizhet gant ne vern piv a vefe kevreet", "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 duplicated successfully!": "Restr eilet gant berzh!",
"Document emoji icon": "Ikon emojis ar restr",
"Document owner": "Perc'henn ar restr", "Document owner": "Perc'henn ar restr",
"Document sections": "Rannoù an teul", "Document sections": "Rannoù an teul",
"Docx": "Docx", "Docx": "Docx",
@@ -218,6 +219,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.", "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 accessible to any connected person": "Dokument für jeden angemeldeten Benutzer zugänglich",
"Document duplicated successfully!": "Dokument erfolgreich dupliziert!", "Document duplicated successfully!": "Dokument erfolgreich dupliziert!",
"Document emoji icon": "Emojisymbol für das Dokument",
"Document owner": "Besitzer des Dokuments", "Document owner": "Besitzer des Dokuments",
"Document sections": "Dokumentabschnitte", "Document sections": "Dokumentabschnitte",
"Docx": "Docx", "Docx": "Docx",
@@ -443,6 +445,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 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.", "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 accessible to any connected person": "Documento accesible a cualquier persona conectada",
"Document emoji icon": "Emoji para el documento",
"Document owner": "Propietario del documento", "Document owner": "Propietario del documento",
"Document sections": "Secciones del documento", "Document sections": "Secciones del documento",
"Docx": "Docx", "Docx": "Docx",
@@ -643,6 +646,7 @@
"Docs: Your new companion to collaborate on documents efficiently, intuitively, and securely.": "Docs : Votre nouveau compagnon pour collaborer sur des documents efficacement, intuitivement et en toute sécurité.", "Docs: Your new companion to collaborate on documents efficiently, intuitively, and securely.": "Docs : Votre nouveau compagnon pour collaborer sur des documents efficacement, intuitivement et en toute sécurité.",
"Document accessible to any connected person": "Document accessible à toute personne connectée", "Document accessible to any connected person": "Document accessible à toute personne connectée",
"Document duplicated successfully!": "Document dupliqué avec succès !", "Document duplicated successfully!": "Document dupliqué avec succès !",
"Document emoji icon": "Emoji pour le document",
"Document owner": "Propriétaire du document", "Document owner": "Propriétaire du document",
"Document sections": "Sections des documents", "Document sections": "Sections des documents",
"Docx": "Docx", "Docx": "Docx",
@@ -860,6 +864,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 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.", "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 accessible to any connected person": "Documento accessibile a qualsiasi persona collegata",
"Document emoji icon": "Emoji per il documento",
"Document owner": "Proprietario del documento", "Document owner": "Proprietario del documento",
"Docx": "Docx", "Docx": "Docx",
"Download": "Scarica", "Download": "Scarica",
@@ -1019,6 +1024,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 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.", "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 accessible to any connected person": "Document is toegankelijk voor ieder verbonden persoon",
"Document emoji icon": "Emoji voor het document",
"Document owner": "Document eigenaar", "Document owner": "Document eigenaar",
"Document sections": "Document secties", "Document sections": "Document secties",
"Docx": "Docx", "Docx": "Docx",
@@ -1314,6 +1320,7 @@
"Docs transforms your documents into knowledge bases thanks to subpages, powerful search and the ability to pin your important documents.": "Docs 通过子页面、强大的搜索功能以及固定重要文档的能力,将您的文档转化为知识库。", "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 为您提供高效、直观且安全的文档协作解决方案。", "Docs: Your new companion to collaborate on documents efficiently, intuitively, and securely.": "Docs 为您提供高效、直观且安全的文档协作解决方案。",
"Document accessible to any connected person": "任何来访的人都可以访问文档", "Document accessible to any connected person": "任何来访的人都可以访问文档",
"Document emoji icon": "文档表情符号",
"Document owner": "文档所有者", "Document owner": "文档所有者",
"Docx": "Doc", "Docx": "Doc",
"Download": "下载", "Download": "下载",

View File

@@ -7624,7 +7624,7 @@ emoji-mart@5.6.0, emoji-mart@^5.6.0:
resolved "https://registry.yarnpkg.com/emoji-mart/-/emoji-mart-5.6.0.tgz#71b3ed0091d3e8c68487b240d9d6d9a73c27f023" resolved "https://registry.yarnpkg.com/emoji-mart/-/emoji-mart-5.6.0.tgz#71b3ed0091d3e8c68487b240d9d6d9a73c27f023"
integrity sha512-eJp3QRe79pjwa+duv+n7+5YsNhRcMl812EcFVwrnRvYKoNPoQb5qxU8DG6Bgwji0akHdp6D4Ln6tYLG58MFSow== integrity sha512-eJp3QRe79pjwa+duv+n7+5YsNhRcMl812EcFVwrnRvYKoNPoQb5qxU8DG6Bgwji0akHdp6D4Ln6tYLG58MFSow==
emoji-regex@^10.3.0: emoji-regex@10.4.0, emoji-regex@^10.3.0:
version "10.4.0" version "10.4.0"
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-10.4.0.tgz#03553afea80b3975749cfcb36f776ca268e413d4" resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-10.4.0.tgz#03553afea80b3975749cfcb36f776ca268e413d4"
integrity sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw== integrity sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==