✨(frontend) use title first emoji as doc icon in tree
Implemented emoji detection system, new DocIcon component.
This commit is contained in:
committed by
Anthony LC
parent
0b64417058
commit
d1cbdfd819
@@ -11,6 +11,7 @@ and this project adheres to
|
||||
### Added
|
||||
|
||||
- 👷(CI) add bundle size check job #1268
|
||||
- ✨(frontend) use title first emoji as doc icon in tree
|
||||
|
||||
### Changed
|
||||
|
||||
|
||||
4
Makefile
4
Makefile
@@ -406,6 +406,10 @@ run-frontend-development: ## Run the frontend in development mode
|
||||
cd $(PATH_FRONT_IMPRESS) && yarn dev
|
||||
.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
|
||||
cd $(PATH_FRONT) && yarn i18n:extract
|
||||
.PHONY: frontend-i18n-extract
|
||||
|
||||
@@ -140,6 +140,12 @@ To start all the services, except the frontend container, you can use the follow
|
||||
$ make run-backend
|
||||
```
|
||||
|
||||
To execute frontend tests & linting only
|
||||
```shellscript
|
||||
$ make frontend-test
|
||||
$ make frontend-lint
|
||||
```
|
||||
|
||||
**Adding content**
|
||||
|
||||
You can create a basic demo site by running this command:
|
||||
|
||||
@@ -61,6 +61,31 @@ test.describe('Doc Header', () => {
|
||||
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 }) => {
|
||||
const [randomDoc] = await createDoc(page, 'doc-delete', browserName, 1);
|
||||
|
||||
|
||||
@@ -136,9 +136,11 @@ export const getGridRow = async (page: Page, title: string) => {
|
||||
|
||||
const rows = docsGrid.getByRole('row');
|
||||
|
||||
const row = rows.filter({
|
||||
hasText: title,
|
||||
});
|
||||
const row = rows
|
||||
.filter({
|
||||
hasText: title,
|
||||
})
|
||||
.first();
|
||||
|
||||
await expect(row).toBeVisible();
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
"crisp-sdk-web": "1.0.25",
|
||||
"docx": "9.5.0",
|
||||
"emoji-mart": "5.6.0",
|
||||
"emoji-regex": "10.4.0",
|
||||
"i18next": "25.3.2",
|
||||
"i18next-browser-languagedetector": "8.2.0",
|
||||
"idb": "8.0.3",
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -4,12 +4,14 @@ import { css } from 'styled-components';
|
||||
|
||||
import { Box, Text } from '@/components';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
import { Doc, useTrans } from '@/docs/doc-management';
|
||||
import { Doc, getEmojiAndTitle, 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;
|
||||
@@ -36,6 +38,10 @@ export const SimpleDocItem = ({
|
||||
const { isDesktop } = useResponsiveStore();
|
||||
const { untitledDocument } = useTrans();
|
||||
|
||||
const { emoji, titleWithoutEmoji: displayTitle } = getEmojiAndTitle(
|
||||
doc.title || untitledDocument,
|
||||
);
|
||||
|
||||
return (
|
||||
<Box
|
||||
$direction="row"
|
||||
@@ -61,23 +67,29 @@ export const SimpleDocItem = ({
|
||||
color={colorsTokens['primary-500']}
|
||||
/>
|
||||
) : (
|
||||
<SimpleFileIcon
|
||||
aria-hidden="true"
|
||||
aria-label={t('Simple document icon')}
|
||||
color={colorsTokens['primary-500']}
|
||||
<DocIcon
|
||||
emoji={emoji}
|
||||
defaultIcon={
|
||||
<SimpleFileIcon
|
||||
aria-hidden="true"
|
||||
aria-label={t('Simple document icon')}
|
||||
color={colorsTokens['primary-500']}
|
||||
/>
|
||||
}
|
||||
$size="25px"
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
<Box $justify="center" $overflow="auto">
|
||||
<Text
|
||||
aria-describedby="doc-title"
|
||||
aria-label={doc.title}
|
||||
aria-label={displayTitle}
|
||||
$size="sm"
|
||||
$variation="1000"
|
||||
$weight="500"
|
||||
$css={ItemTextCss}
|
||||
>
|
||||
{doc.title || untitledDocument}
|
||||
{displayTitle}
|
||||
</Text>
|
||||
{(!isDesktop || showAccesses) && (
|
||||
<Box
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import emojiRegex from 'emoji-regex';
|
||||
import * as Y from 'yjs';
|
||||
|
||||
import { Doc, LinkReach, LinkRole, Role } from './types';
|
||||
@@ -30,3 +31,19 @@ export const getDocLinkReach = (doc: Doc): LinkReach => {
|
||||
export const getDocLinkRole = (doc: Doc): LinkRole => {
|
||||
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 };
|
||||
};
|
||||
|
||||
@@ -9,7 +9,12 @@ import { css } from 'styled-components';
|
||||
|
||||
import { Box, Icon, Text } from '@/components';
|
||||
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 { useResponsiveStore } from '@/stores';
|
||||
|
||||
@@ -38,6 +43,9 @@ export const DocSubPageItem = (props: TreeViewNodeProps<Doc>) => {
|
||||
const router = useRouter();
|
||||
const { togglePanel } = useLeftPanelStore();
|
||||
|
||||
const { emoji, titleWithoutEmoji } = getEmojiAndTitle(doc.title || '');
|
||||
const displayTitle = titleWithoutEmoji || untitledDocument;
|
||||
|
||||
const afterCreate = (createdDoc: Doc) => {
|
||||
const actualChildren = node.data.children ?? [];
|
||||
|
||||
@@ -122,7 +130,7 @@ export const DocSubPageItem = (props: TreeViewNodeProps<Doc>) => {
|
||||
$minHeight="24px"
|
||||
>
|
||||
<Box $width="16px" $height="16px">
|
||||
<SubPageIcon />
|
||||
<DocIcon emoji={emoji} defaultIcon={<SubPageIcon />} $size="sm" />
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
@@ -137,7 +145,7 @@ export const DocSubPageItem = (props: TreeViewNodeProps<Doc>) => {
|
||||
`}
|
||||
>
|
||||
<Text $css={ItemTextCss} $size="sm" $variation="1000">
|
||||
{doc.title || untitledDocument}
|
||||
{displayTitle}
|
||||
</Text>
|
||||
{doc.nb_accesses_direct >= 1 && (
|
||||
<Icon
|
||||
|
||||
@@ -42,6 +42,7 @@
|
||||
"Docs Logo": "Logo Docs",
|
||||
"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 icon": "Ikon emojis ar restr",
|
||||
"Document owner": "Perc'henn ar restr",
|
||||
"Document sections": "Rannoù an teul",
|
||||
"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.",
|
||||
"Document accessible to any connected person": "Dokument für jeden angemeldeten Benutzer zugänglich",
|
||||
"Document duplicated successfully!": "Dokument erfolgreich dupliziert!",
|
||||
"Document emoji icon": "Emojisymbol für das Dokument",
|
||||
"Document owner": "Besitzer des Dokuments",
|
||||
"Document sections": "Dokumentabschnitte",
|
||||
"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: 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 icon": "Emoji para el documento",
|
||||
"Document owner": "Propietario del documento",
|
||||
"Document sections": "Secciones del documento",
|
||||
"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é.",
|
||||
"Document accessible to any connected person": "Document accessible à toute personne connectée",
|
||||
"Document duplicated successfully!": "Document dupliqué avec succès !",
|
||||
"Document emoji icon": "Emoji pour le document",
|
||||
"Document owner": "Propriétaire du document",
|
||||
"Document sections": "Sections des documents",
|
||||
"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: 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 icon": "Emoji per il documento",
|
||||
"Document owner": "Proprietario del documento",
|
||||
"Docx": "Docx",
|
||||
"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: 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 icon": "Emoji voor het document",
|
||||
"Document owner": "Document eigenaar",
|
||||
"Document sections": "Document secties",
|
||||
"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: Your new companion to collaborate on documents efficiently, intuitively, and securely.": "Docs 为您提供高效、直观且安全的文档协作解决方案。",
|
||||
"Document accessible to any connected person": "任何来访的人都可以访问文档",
|
||||
"Document emoji icon": "文档表情符号",
|
||||
"Document owner": "文档所有者",
|
||||
"Docx": "Doc",
|
||||
"Download": "下载",
|
||||
|
||||
@@ -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"
|
||||
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"
|
||||
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-10.4.0.tgz#03553afea80b3975749cfcb36f776ca268e413d4"
|
||||
integrity sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==
|
||||
|
||||
Reference in New Issue
Block a user