✨(frontend) Move doc modal
We can now move a doc to another doc from a search modal. It will make it easier to move a doc without having to scroll through the doc grid to find the destination doc. We kept most of the logic implemented in the doc grid dnd.
This commit is contained in:
@@ -15,6 +15,7 @@ and this project adheres to
|
|||||||
- ✨(frontend) Can print a doc #1832
|
- ✨(frontend) Can print a doc #1832
|
||||||
- ✨(backend) manage reconciliation requests for user accounts #1878
|
- ✨(backend) manage reconciliation requests for user accounts #1878
|
||||||
- 👷(CI) add GHCR workflow for forked repo testing #1851
|
- 👷(CI) add GHCR workflow for forked repo testing #1851
|
||||||
|
- ✨(frontend) Move doc modal #1886
|
||||||
- ⚡️(backend) remove content from Document serializer when asked #1910
|
- ⚡️(backend) remove content from Document serializer when asked #1910
|
||||||
- ✨(backend) allow the duplication of subpages #1893
|
- ✨(backend) allow the duplication of subpages #1893
|
||||||
- ✨(backend) Onboarding docs for new users #1891
|
- ✨(backend) Onboarding docs for new users #1891
|
||||||
|
|||||||
@@ -1,10 +1,19 @@
|
|||||||
import { expect, test } from '@playwright/test';
|
import { expect, test } from '@playwright/test';
|
||||||
|
|
||||||
import { createDoc, mockedListDocs, toggleHeaderMenu } from './utils-common';
|
import {
|
||||||
|
createDoc,
|
||||||
|
getGridRow,
|
||||||
|
mockedListDocs,
|
||||||
|
toggleHeaderMenu,
|
||||||
|
verifyDocName,
|
||||||
|
} from './utils-common';
|
||||||
import { createRootSubPage } from './utils-sub-pages';
|
import { createRootSubPage } from './utils-sub-pages';
|
||||||
|
|
||||||
test.describe('Doc grid dnd', () => {
|
test.describe('Doc grid move', () => {
|
||||||
test('it creates a doc', async ({ page, browserName }) => {
|
test('it checks drag and drop functionality', async ({
|
||||||
|
page,
|
||||||
|
browserName,
|
||||||
|
}) => {
|
||||||
await page.goto('/');
|
await page.goto('/');
|
||||||
const header = page.locator('header').first();
|
const header = page.locator('header').first();
|
||||||
await createDoc(page, 'Draggable doc', browserName, 1);
|
await createDoc(page, 'Draggable doc', browserName, 1);
|
||||||
@@ -29,7 +38,7 @@ test.describe('Doc grid dnd', () => {
|
|||||||
await expect(draggableElement).toBeVisible();
|
await expect(draggableElement).toBeVisible();
|
||||||
await expect(dropZone).toBeVisible();
|
await expect(dropZone).toBeVisible();
|
||||||
|
|
||||||
// Obtenir les positions des éléments
|
// Get the position of the elements
|
||||||
const draggableBoundingBox = await draggableElement.boundingBox();
|
const draggableBoundingBox = await draggableElement.boundingBox();
|
||||||
const dropZoneBoundingBox = await dropZone.boundingBox();
|
const dropZoneBoundingBox = await dropZone.boundingBox();
|
||||||
|
|
||||||
@@ -46,7 +55,7 @@ test.describe('Doc grid dnd', () => {
|
|||||||
);
|
);
|
||||||
await page.mouse.down();
|
await page.mouse.down();
|
||||||
|
|
||||||
// Déplacer vers la zone cible
|
// Move to the target zone
|
||||||
await page.mouse.move(
|
await page.mouse.move(
|
||||||
dropZoneBoundingBox.x + dropZoneBoundingBox.width / 2,
|
dropZoneBoundingBox.x + dropZoneBoundingBox.width / 2,
|
||||||
dropZoneBoundingBox.y + dropZoneBoundingBox.height / 2,
|
dropZoneBoundingBox.y + dropZoneBoundingBox.height / 2,
|
||||||
@@ -161,6 +170,55 @@ test.describe('Doc grid dnd', () => {
|
|||||||
|
|
||||||
await page.mouse.up();
|
await page.mouse.up();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('it moves a doc from the doc search modal', async ({
|
||||||
|
page,
|
||||||
|
browserName,
|
||||||
|
}) => {
|
||||||
|
await page.goto('/');
|
||||||
|
|
||||||
|
const [titleDoc1] = await createDoc(page, 'Draggable doc', browserName, 1);
|
||||||
|
await page.getByRole('button', { name: 'Back to homepage' }).click();
|
||||||
|
|
||||||
|
const [titleDoc2] = await createDoc(page, 'Droppable doc', browserName, 1);
|
||||||
|
await page.getByRole('button', { name: 'Back to homepage' }).click();
|
||||||
|
|
||||||
|
const docsGrid = page.getByTestId('docs-grid');
|
||||||
|
await expect(docsGrid.getByText(titleDoc1)).toBeVisible();
|
||||||
|
await expect(docsGrid.getByText(titleDoc2)).toBeVisible();
|
||||||
|
|
||||||
|
const row = await getGridRow(page, titleDoc1);
|
||||||
|
await row.getByText(`more_horiz`).click();
|
||||||
|
|
||||||
|
await page.getByRole('menuitem', { name: 'Move into a doc' }).click();
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
page.getByRole('dialog').getByRole('heading', { name: 'Move' }),
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
|
const input = page.getByRole('combobox', { name: 'Quick search input' });
|
||||||
|
await input.click();
|
||||||
|
await input.fill(titleDoc2);
|
||||||
|
|
||||||
|
await expect(page.getByRole('option').getByText(titleDoc2)).toBeVisible();
|
||||||
|
|
||||||
|
// Select the first result
|
||||||
|
await page.keyboard.press('Enter');
|
||||||
|
// The CTA should get the focus
|
||||||
|
await page.keyboard.press('Tab');
|
||||||
|
// Validate the move action
|
||||||
|
await page.keyboard.press('Enter');
|
||||||
|
|
||||||
|
await expect(docsGrid.getByText(titleDoc1)).toBeHidden();
|
||||||
|
await docsGrid
|
||||||
|
.getByRole('link', { name: `Open document ${titleDoc2}` })
|
||||||
|
.click();
|
||||||
|
|
||||||
|
await verifyDocName(page, titleDoc2);
|
||||||
|
|
||||||
|
const docTree = page.getByTestId('doc-tree');
|
||||||
|
await expect(docTree.getByText(titleDoc1)).toBeVisible();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test.describe('Doc grid dnd mobile', () => {
|
test.describe('Doc grid dnd mobile', () => {
|
||||||
@@ -15,6 +15,7 @@ export type QuickSearchAction = {
|
|||||||
|
|
||||||
export type QuickSearchData<T> = {
|
export type QuickSearchData<T> = {
|
||||||
groupName: string;
|
groupName: string;
|
||||||
|
groupKey?: string;
|
||||||
elements: T[];
|
elements: T[];
|
||||||
emptyString?: string;
|
emptyString?: string;
|
||||||
startActions?: QuickSearchAction[];
|
startActions?: QuickSearchAction[];
|
||||||
@@ -30,13 +31,13 @@ export type QuickSearchProps = {
|
|||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
label?: string;
|
label?: string;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
|
groupKey?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const QuickSearch = ({
|
export const QuickSearch = ({
|
||||||
onFilter,
|
onFilter,
|
||||||
inputContent,
|
inputContent,
|
||||||
inputValue,
|
inputValue,
|
||||||
loading,
|
|
||||||
showInput = true,
|
showInput = true,
|
||||||
label,
|
label,
|
||||||
placeholder,
|
placeholder,
|
||||||
@@ -72,10 +73,10 @@ export const QuickSearch = ({
|
|||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
value={selectedValue}
|
value={selectedValue}
|
||||||
onValueChange={handleValueChange}
|
onValueChange={handleValueChange}
|
||||||
|
disablePointerSelection
|
||||||
>
|
>
|
||||||
{showInput && (
|
{showInput && (
|
||||||
<QuickSearchInput
|
<QuickSearchInput
|
||||||
loading={loading}
|
|
||||||
withSeparator={hasChildrens(children)}
|
withSeparator={hasChildrens(children)}
|
||||||
inputValue={inputValue}
|
inputValue={inputValue}
|
||||||
onFilter={onFilter}
|
onFilter={onFilter}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export const QuickSearchGroup = <T,>({
|
|||||||
{group.startActions?.map((action, index) => {
|
{group.startActions?.map((action, index) => {
|
||||||
return (
|
return (
|
||||||
<QuickSearchItem
|
<QuickSearchItem
|
||||||
key={`${group.groupName}-action-${index}`}
|
key={`${group.groupKey ?? group.groupName}-start-actions-${index}`}
|
||||||
onSelect={action.onSelect}
|
onSelect={action.onSelect}
|
||||||
>
|
>
|
||||||
{action.content}
|
{action.content}
|
||||||
@@ -38,8 +38,8 @@ export const QuickSearchGroup = <T,>({
|
|||||||
{group.elements.map((groupElement, index) => {
|
{group.elements.map((groupElement, index) => {
|
||||||
return (
|
return (
|
||||||
<QuickSearchItem
|
<QuickSearchItem
|
||||||
id={`${group.groupName}-element-${index}`}
|
id={`${group.groupKey ?? group.groupName}-element-${index}`}
|
||||||
key={`${group.groupName}-element-${index}`}
|
key={`${group.groupKey ?? group.groupName}-element-${index}`}
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
onSelect?.(groupElement);
|
onSelect?.(groupElement);
|
||||||
}}
|
}}
|
||||||
@@ -51,7 +51,7 @@ export const QuickSearchGroup = <T,>({
|
|||||||
{group.endActions?.map((action, index) => {
|
{group.endActions?.map((action, index) => {
|
||||||
return (
|
return (
|
||||||
<QuickSearchItem
|
<QuickSearchItem
|
||||||
key={`${group.groupName}-action-${index}`}
|
key={`${group.groupKey ?? group.groupName}-end-actions-${index}`}
|
||||||
onSelect={action.onSelect}
|
onSelect={action.onSelect}
|
||||||
>
|
>
|
||||||
{action.content}
|
{action.content}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { Loader } from '@gouvfr-lasuite/cunningham-react';
|
|
||||||
import { Command } from 'cmdk';
|
import { Command } from 'cmdk';
|
||||||
import { ReactNode } from 'react';
|
import { PropsWithChildren } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { HorizontalSeparator } from '@/components';
|
import { HorizontalSeparator } from '@/components';
|
||||||
@@ -9,19 +8,16 @@ import { useCunninghamTheme } from '@/cunningham';
|
|||||||
import { Box } from '../Box';
|
import { Box } from '../Box';
|
||||||
import { Icon } from '../Icon';
|
import { Icon } from '../Icon';
|
||||||
|
|
||||||
type Props = {
|
type QuickSearchInputProps = {
|
||||||
loading?: boolean;
|
|
||||||
inputValue?: string;
|
inputValue?: string;
|
||||||
onFilter?: (str: string) => void;
|
onFilter?: (str: string) => void;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
children?: ReactNode;
|
|
||||||
withSeparator?: boolean;
|
withSeparator?: boolean;
|
||||||
listId?: string;
|
listId?: string;
|
||||||
onUserInteract?: () => void;
|
onUserInteract?: () => void;
|
||||||
isExpanded?: boolean;
|
isExpanded?: boolean;
|
||||||
};
|
};
|
||||||
export const QuickSearchInput = ({
|
export const QuickSearchInput = ({
|
||||||
loading,
|
|
||||||
inputValue,
|
inputValue,
|
||||||
onFilter,
|
onFilter,
|
||||||
placeholder,
|
placeholder,
|
||||||
@@ -30,7 +26,7 @@ export const QuickSearchInput = ({
|
|||||||
listId,
|
listId,
|
||||||
onUserInteract,
|
onUserInteract,
|
||||||
isExpanded,
|
isExpanded,
|
||||||
}: Props) => {
|
}: PropsWithChildren<QuickSearchInputProps>) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { spacingsTokens } = useCunninghamTheme();
|
const { spacingsTokens } = useCunninghamTheme();
|
||||||
|
|
||||||
@@ -52,14 +48,7 @@ export const QuickSearchInput = ({
|
|||||||
$gap={spacingsTokens['2xs']}
|
$gap={spacingsTokens['2xs']}
|
||||||
$padding={{ horizontal: 'base', vertical: 'sm' }}
|
$padding={{ horizontal: 'base', vertical: 'sm' }}
|
||||||
>
|
>
|
||||||
{!loading && (
|
<Icon iconName="search" $variation="secondary" aria-hidden="true" />
|
||||||
<Icon iconName="search" $variation="secondary" aria-hidden="true" />
|
|
||||||
)}
|
|
||||||
{loading && (
|
|
||||||
<div>
|
|
||||||
<Loader size="small" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<Command.Input
|
<Command.Input
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
aria-label={t('Quick search input')}
|
aria-label={t('Quick search input')}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export const QuickSearchStyle = createGlobalStyle`
|
|||||||
padding: var(--c--globals--spacings--xs);
|
padding: var(--c--globals--spacings--xs);
|
||||||
background: white;
|
background: white;
|
||||||
outline: none;
|
outline: none;
|
||||||
color: var(--c--globals--colors--gray-1000);
|
color: var(--c--contextuals--content--semantic--neutral--primary);
|
||||||
border-radius: var(--c--globals--spacings--0);
|
border-radius: var(--c--globals--spacings--0);
|
||||||
|
|
||||||
&::placeholder {
|
&::placeholder {
|
||||||
|
|||||||
@@ -181,6 +181,7 @@ export const SearchPage = ({
|
|||||||
setSearch(value);
|
setSearch(value);
|
||||||
}}
|
}}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
|
autoComplete="off"
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
<Box
|
<Box
|
||||||
|
|||||||
@@ -12,15 +12,19 @@ import { DocSearchItem } from './DocSearchItem';
|
|||||||
type DocSearchContentProps = {
|
type DocSearchContentProps = {
|
||||||
search: string;
|
search: string;
|
||||||
filters: DocSearchFiltersValues;
|
filters: DocSearchFiltersValues;
|
||||||
|
filterResults?: (doc: Doc) => boolean;
|
||||||
onSelect: (doc: Doc) => void;
|
onSelect: (doc: Doc) => void;
|
||||||
onLoadingChange?: (loading: boolean) => void;
|
onLoadingChange?: (loading: boolean) => void;
|
||||||
|
renderSearchElement?: (doc: Doc) => React.ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DocSearchContent = ({
|
export const DocSearchContent = ({
|
||||||
search,
|
search,
|
||||||
filters,
|
filters,
|
||||||
|
filterResults,
|
||||||
onSelect,
|
onSelect,
|
||||||
onLoadingChange,
|
onLoadingChange,
|
||||||
|
renderSearchElement,
|
||||||
}: DocSearchContentProps) => {
|
}: DocSearchContentProps) => {
|
||||||
const {
|
const {
|
||||||
data,
|
data,
|
||||||
@@ -38,10 +42,15 @@ export const DocSearchContent = ({
|
|||||||
const loading = isFetching || isRefetching || isLoading;
|
const loading = isFetching || isRefetching || isLoading;
|
||||||
|
|
||||||
const docsData: QuickSearchData<Doc> = useMemo(() => {
|
const docsData: QuickSearchData<Doc> = useMemo(() => {
|
||||||
const docs = data?.pages.flatMap((page) => page.results) || [];
|
let docs = data?.pages.flatMap((page) => page.results) || [];
|
||||||
|
|
||||||
|
if (filterResults) {
|
||||||
|
docs = docs.filter(filterResults);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
groupName: docs.length > 0 ? t('Select a document') : '',
|
groupName: docs.length > 0 ? t('Select a document') : '',
|
||||||
|
groupKey: 'docs',
|
||||||
elements: search ? docs : [],
|
elements: search ? docs : [],
|
||||||
emptyString: t('No document found'),
|
emptyString: t('No document found'),
|
||||||
endActions: hasNextPage
|
endActions: hasNextPage
|
||||||
@@ -52,7 +61,7 @@ export const DocSearchContent = ({
|
|||||||
]
|
]
|
||||||
: [],
|
: [],
|
||||||
};
|
};
|
||||||
}, [search, data?.pages, fetchNextPage, hasNextPage]);
|
}, [search, data?.pages, fetchNextPage, hasNextPage, filterResults]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
onLoadingChange?.(loading);
|
onLoadingChange?.(loading);
|
||||||
@@ -62,7 +71,9 @@ export const DocSearchContent = ({
|
|||||||
<QuickSearchGroup
|
<QuickSearchGroup
|
||||||
onSelect={onSelect}
|
onSelect={onSelect}
|
||||||
group={docsData}
|
group={docsData}
|
||||||
renderElement={(doc) => <DocSearchItem doc={doc} />}
|
renderElement={
|
||||||
|
renderSearchElement ?? ((doc) => <DocSearchItem doc={doc} />)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
export * from './DocSearchContent';
|
||||||
export * from './DocSearchModal';
|
export * from './DocSearchModal';
|
||||||
export * from './DocSearchFilters';
|
export * from './DocSearchFilters';
|
||||||
export * from './DocSearchSubPageContent';
|
export * from './DocSearchSubPageContent';
|
||||||
|
|||||||
@@ -0,0 +1,260 @@
|
|||||||
|
import { Button, Modal, ModalSize } from '@gouvfr-lasuite/cunningham-react';
|
||||||
|
import { TreeViewMoveModeEnum } from '@gouvfr-lasuite/ui-kit';
|
||||||
|
import Image from 'next/image';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { Trans, useTranslation } from 'react-i18next';
|
||||||
|
import { createGlobalStyle, css } from 'styled-components';
|
||||||
|
import { useDebouncedCallback } from 'use-debounce';
|
||||||
|
|
||||||
|
import { Box, ButtonCloseModal, Text } from '@/components';
|
||||||
|
import { QuickSearch } from '@/components/quick-search';
|
||||||
|
import { Doc, useMoveDoc, useTrans } from '@/docs/doc-management';
|
||||||
|
import { DocSearchContent, DocSearchTarget } from '@/docs/doc-search';
|
||||||
|
import EmptySearchIcon from '@/docs/doc-search/assets/illustration-docs-empty.png';
|
||||||
|
import { useResponsiveStore } from '@/stores';
|
||||||
|
|
||||||
|
import { DocsGridItemDate, DocsGridItemTitle } from './DocsGridItem';
|
||||||
|
|
||||||
|
export const DocMoveModalStyle = createGlobalStyle`
|
||||||
|
.c__modal--full .c__modal__scroller {
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.c__modal__scroller:has(.quick-search-container){
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.quick-search-container [cmdk-list] {
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.c__modal__title {
|
||||||
|
padding-inline: var(--c--globals--spacings--md);
|
||||||
|
padding-block: var(--c--globals--spacings--base);
|
||||||
|
border-bottom: 1px solid var(--c--contextuals--border--surface--primary);
|
||||||
|
}
|
||||||
|
.c__modal__footer {
|
||||||
|
margin-top: 0rem;
|
||||||
|
}
|
||||||
|
.quick-search-input{
|
||||||
|
padding-inline: var(--c--globals--spacings--md);
|
||||||
|
}
|
||||||
|
.c__modal__footer{
|
||||||
|
border-top: 1px solid var(--c--contextuals--border--surface--primary);
|
||||||
|
}
|
||||||
|
.quick-search-container [cmdk-item] {
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.quick-search-container [cmdk-item][data-selected='true'] {
|
||||||
|
background: var(--c--contextuals--background--semantic--contextual--primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
type DocMoveModalGlobalProps = {
|
||||||
|
doc: Doc;
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DocMoveModal = ({
|
||||||
|
doc,
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
}: DocMoveModalGlobalProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [docSelected, setDocSelected] = useState<Doc>();
|
||||||
|
const { untitledDocument } = useTrans();
|
||||||
|
const docTitle = doc.title || untitledDocument;
|
||||||
|
const { mutate: moveDoc } = useMoveDoc(true);
|
||||||
|
const [search, setSearch] = useState('');
|
||||||
|
const { isDesktop } = useResponsiveStore();
|
||||||
|
const handleInputSearch = useDebouncedCallback(setSearch, 700);
|
||||||
|
|
||||||
|
const handleSelect = (docSelected: Doc) => {
|
||||||
|
setDocSelected(docSelected);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMoveDoc = () => {
|
||||||
|
if (!docSelected?.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
moveDoc({
|
||||||
|
sourceDocumentId: doc.id,
|
||||||
|
targetDocumentId: docSelected.id,
|
||||||
|
position: TreeViewMoveModeEnum.FIRST_CHILD,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<DocMoveModalStyle />
|
||||||
|
<Modal
|
||||||
|
isOpen={isOpen}
|
||||||
|
onClose={onClose}
|
||||||
|
closeOnClickOutside
|
||||||
|
size={isDesktop ? ModalSize.LARGE : ModalSize.FULL}
|
||||||
|
hideCloseButton
|
||||||
|
aria-label={t('Move Modal')}
|
||||||
|
rightActions={
|
||||||
|
<Box $direction="row-reverse" $padding="md" $gap="small">
|
||||||
|
<Button
|
||||||
|
aria-label={t('Move the document to the selected location')}
|
||||||
|
variant="primary"
|
||||||
|
fullWidth
|
||||||
|
onClick={() => {
|
||||||
|
handleMoveDoc();
|
||||||
|
}}
|
||||||
|
disabled={!docSelected}
|
||||||
|
>
|
||||||
|
{t('Move here')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
aria-label={t('Cancel the move')}
|
||||||
|
variant="secondary"
|
||||||
|
fullWidth
|
||||||
|
onClick={onClose}
|
||||||
|
>
|
||||||
|
{t('Cancel')}
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
|
title={
|
||||||
|
<Box>
|
||||||
|
<Box
|
||||||
|
$direction="row"
|
||||||
|
$justify="space-between"
|
||||||
|
$align="center"
|
||||||
|
$width="100%"
|
||||||
|
>
|
||||||
|
<Text as="h2" $margin="0" $size="h6" $align="flex-start">
|
||||||
|
{t('Move')}
|
||||||
|
</Text>
|
||||||
|
<ButtonCloseModal
|
||||||
|
aria-label={t('Close the move modal')}
|
||||||
|
onClick={() => onClose()}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<Box $margin={{ top: 'sm' }}>
|
||||||
|
<Text
|
||||||
|
$size="sm"
|
||||||
|
$variation="secondary"
|
||||||
|
$display="inline"
|
||||||
|
$weight="normal"
|
||||||
|
$textAlign="left"
|
||||||
|
>
|
||||||
|
<Trans t={t}>
|
||||||
|
Choose the new location for <strong>{docTitle}</strong>.
|
||||||
|
</Trans>
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
aria-label={t('Move modal')}
|
||||||
|
$direction="column"
|
||||||
|
$justify="space-between"
|
||||||
|
className="--docs--doc-move-modal"
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
// Close modal on Escape
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
onClose();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Prevent keyboard events from bubbling to parent components (e.g., drag and drop)
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<QuickSearch
|
||||||
|
placeholder={t('Search for a doc')}
|
||||||
|
loading={loading}
|
||||||
|
onFilter={handleInputSearch}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
$padding={{ horizontal: 'md' }}
|
||||||
|
$height={isDesktop ? 'min(60vh, 500px)' : 'calc(100vh - 260px)'}
|
||||||
|
>
|
||||||
|
{search.length === 0 && (
|
||||||
|
<Box
|
||||||
|
$direction="column"
|
||||||
|
$height="100%"
|
||||||
|
$align="center"
|
||||||
|
$justify="center"
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
width={320}
|
||||||
|
src={EmptySearchIcon}
|
||||||
|
alt={t('No active search')}
|
||||||
|
style={{ maxWidth: '100%', height: 'auto' }}
|
||||||
|
priority
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
{search && (
|
||||||
|
<Box>
|
||||||
|
<DocSearchContent
|
||||||
|
search={search}
|
||||||
|
filters={{ target: DocSearchTarget.ALL }}
|
||||||
|
filterResults={(docResults) =>
|
||||||
|
docResults.id !== doc.id && docResults.abilities.move
|
||||||
|
}
|
||||||
|
onSelect={handleSelect}
|
||||||
|
onLoadingChange={setLoading}
|
||||||
|
renderSearchElement={(docSearch) => {
|
||||||
|
const isSelected = docSelected?.id === docSearch.id;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
className="--docs--doc-move-modal-search-item"
|
||||||
|
$direction="row"
|
||||||
|
$align="center"
|
||||||
|
$justify="space-between"
|
||||||
|
$width="100%"
|
||||||
|
$gap="sm"
|
||||||
|
$padding="3xs"
|
||||||
|
$css={css`
|
||||||
|
background-color: ${isSelected
|
||||||
|
? 'var(--c--contextuals--background--semantic--brand--tertiary)'
|
||||||
|
: 'transparent'};
|
||||||
|
border: 1px solid
|
||||||
|
${isSelected
|
||||||
|
? 'var(--c--contextuals--border--semantic--brand--tertiary)'
|
||||||
|
: 'transparent'};
|
||||||
|
border-radius: var(--c--globals--spacings--3xs);
|
||||||
|
|
||||||
|
/* Arrow key navigation highlight */
|
||||||
|
&[data-selected='true'] {
|
||||||
|
${!isSelected &&
|
||||||
|
`
|
||||||
|
background-color: var(--c--contextuals--background--semantic--contextual--primary);
|
||||||
|
border-color: transparent;
|
||||||
|
`}
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
aria-selected={isSelected}
|
||||||
|
>
|
||||||
|
<DocsGridItemTitle
|
||||||
|
doc={docSearch}
|
||||||
|
withTooltip={false}
|
||||||
|
/>
|
||||||
|
<DocsGridItemDate
|
||||||
|
doc={docSearch}
|
||||||
|
isDesktop={isDesktop}
|
||||||
|
isInTrashbin={false}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</QuickSearch>
|
||||||
|
</Box>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -11,9 +11,12 @@ import {
|
|||||||
useCreateFavoriteDoc,
|
useCreateFavoriteDoc,
|
||||||
useDeleteFavoriteDoc,
|
useDeleteFavoriteDoc,
|
||||||
useDuplicateDoc,
|
useDuplicateDoc,
|
||||||
|
useTrans,
|
||||||
} from '@/docs/doc-management';
|
} from '@/docs/doc-management';
|
||||||
import { DocShareModal } from '@/docs/doc-share';
|
import { DocShareModal } from '@/docs/doc-share';
|
||||||
|
|
||||||
|
import { DocMoveModal } from './DocMoveModal';
|
||||||
|
|
||||||
interface DocsGridActionsProps {
|
interface DocsGridActionsProps {
|
||||||
doc: Doc;
|
doc: Doc;
|
||||||
}
|
}
|
||||||
@@ -23,6 +26,8 @@ export const DocsGridActions = ({ doc }: DocsGridActionsProps) => {
|
|||||||
|
|
||||||
const deleteModal = useModal();
|
const deleteModal = useModal();
|
||||||
const shareModal = useModal();
|
const shareModal = useModal();
|
||||||
|
const importModal = useModal();
|
||||||
|
const { untitledDocument } = useTrans();
|
||||||
|
|
||||||
const { mutate: duplicateDoc } = useDuplicateDoc();
|
const { mutate: duplicateDoc } = useDuplicateDoc();
|
||||||
|
|
||||||
@@ -56,6 +61,15 @@ export const DocsGridActions = ({ doc }: DocsGridActionsProps) => {
|
|||||||
|
|
||||||
testId: `docs-grid-actions-share-${doc.id}`,
|
testId: `docs-grid-actions-share-${doc.id}`,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: t('Move into a doc'),
|
||||||
|
icon: 'copy_all',
|
||||||
|
callback: () => {
|
||||||
|
importModal.open();
|
||||||
|
},
|
||||||
|
testId: `docs-grid-actions-import-${doc.id}`,
|
||||||
|
show: doc.abilities.move,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: t('Duplicate'),
|
label: t('Duplicate'),
|
||||||
icon: 'content_copy',
|
icon: 'content_copy',
|
||||||
@@ -78,7 +92,7 @@ export const DocsGridActions = ({ doc }: DocsGridActionsProps) => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const documentTitle = doc.title || t('Untitled document');
|
const documentTitle = doc.title || untitledDocument;
|
||||||
const menuLabel = t('Open the menu of actions for the document: {{title}}', {
|
const menuLabel = t('Open the menu of actions for the document: {{title}}', {
|
||||||
title: documentTitle,
|
title: documentTitle,
|
||||||
});
|
});
|
||||||
@@ -116,6 +130,13 @@ export const DocsGridActions = ({ doc }: DocsGridActionsProps) => {
|
|||||||
{shareModal.isOpen && (
|
{shareModal.isOpen && (
|
||||||
<DocShareModal doc={doc} onClose={shareModal.close} />
|
<DocShareModal doc={doc} onClose={shareModal.close} />
|
||||||
)}
|
)}
|
||||||
|
{importModal.isOpen && (
|
||||||
|
<DocMoveModal
|
||||||
|
doc={doc}
|
||||||
|
onClose={importModal.close}
|
||||||
|
isOpen={importModal.isOpen}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { css } from 'styled-components';
|
|||||||
import { Box, Icon, StyledLink, Text } from '@/components';
|
import { Box, Icon, StyledLink, Text } from '@/components';
|
||||||
import { useConfig } from '@/core';
|
import { useConfig } from '@/core';
|
||||||
import { useCunninghamTheme } from '@/cunningham';
|
import { useCunninghamTheme } from '@/cunningham';
|
||||||
import { Doc, LinkReach, SimpleDocItem } from '@/docs/doc-management';
|
import { Doc, LinkReach, SimpleDocItem, useTrans } from '@/docs/doc-management';
|
||||||
import { useDate } from '@/hooks';
|
import { useDate } from '@/hooks';
|
||||||
import { useResponsiveStore } from '@/stores';
|
import { useResponsiveStore } from '@/stores';
|
||||||
|
|
||||||
@@ -26,14 +26,12 @@ export const DocsGridItem = ({ doc, dragMode = false }: DocsGridItemProps) => {
|
|||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const target = searchParams.get('target');
|
const target = searchParams.get('target');
|
||||||
const isInTrashbin = target === 'trashbin';
|
const isInTrashbin = target === 'trashbin';
|
||||||
|
const { untitledDocument } = useTrans();
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { isDesktop } = useResponsiveStore();
|
const { isDesktop } = useResponsiveStore();
|
||||||
const { flexLeft, flexRight } = useResponsiveDocGrid();
|
const { flexLeft, flexRight } = useResponsiveDocGrid();
|
||||||
const { spacingsTokens } = useCunninghamTheme();
|
const { spacingsTokens } = useCunninghamTheme();
|
||||||
const isPublic = doc.link_reach === LinkReach.PUBLIC;
|
|
||||||
const isAuthenticated = doc.link_reach === LinkReach.AUTHENTICATED;
|
|
||||||
const isShared = isPublic || isAuthenticated;
|
|
||||||
|
|
||||||
const handleKeyDown = (e: KeyboardEvent) => {
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
if (e.key === 'Enter' || e.key === ' ') {
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
@@ -62,7 +60,7 @@ export const DocsGridItem = ({ doc, dragMode = false }: DocsGridItemProps) => {
|
|||||||
`}
|
`}
|
||||||
className="--docs--doc-grid-item"
|
className="--docs--doc-grid-item"
|
||||||
aria-label={t('Open document: {{title}}', {
|
aria-label={t('Open document: {{title}}', {
|
||||||
title: doc.title || t('Untitled document'),
|
title: doc.title || untitledDocument,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<Box
|
<Box
|
||||||
@@ -82,72 +80,7 @@ export const DocsGridItem = ({ doc, dragMode = false }: DocsGridItemProps) => {
|
|||||||
href={`/docs/${doc.id}`}
|
href={`/docs/${doc.id}`}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
>
|
>
|
||||||
<Box
|
<DocsGridItemTitle doc={doc} withTooltip={!dragMode} />
|
||||||
data-testid={`docs-grid-name-${doc.id}`}
|
|
||||||
$direction="row"
|
|
||||||
$align="center"
|
|
||||||
$gap={spacingsTokens.xs}
|
|
||||||
$padding={{ right: isDesktop ? 'md' : '3xs' }}
|
|
||||||
$maxWidth="100%"
|
|
||||||
>
|
|
||||||
<SimpleDocItem isPinned={doc.is_favorite} doc={doc} />
|
|
||||||
{isShared && (
|
|
||||||
<Box
|
|
||||||
$padding={{ top: !isDesktop ? '4xs' : undefined }}
|
|
||||||
$css={
|
|
||||||
!isDesktop
|
|
||||||
? css`
|
|
||||||
align-self: flex-start;
|
|
||||||
`
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{dragMode && (
|
|
||||||
<>
|
|
||||||
<Icon
|
|
||||||
$layer="background"
|
|
||||||
$theme="neutral"
|
|
||||||
$variation="primary"
|
|
||||||
$size="14px"
|
|
||||||
iconName={isPublic ? 'public' : 'vpn_lock'}
|
|
||||||
/>
|
|
||||||
<span className="sr-only">
|
|
||||||
{isPublic
|
|
||||||
? t('Accessible to anyone')
|
|
||||||
: t('Accessible to authenticated users')}
|
|
||||||
</span>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{!dragMode && (
|
|
||||||
<Tooltip
|
|
||||||
content={
|
|
||||||
<Text $textAlign="center">
|
|
||||||
{isPublic
|
|
||||||
? t('Accessible to anyone')
|
|
||||||
: t('Accessible to authenticated users')}
|
|
||||||
</Text>
|
|
||||||
}
|
|
||||||
placement="top"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<Icon
|
|
||||||
$layer="background"
|
|
||||||
$theme="neutral"
|
|
||||||
$variation="primary"
|
|
||||||
$size="sm"
|
|
||||||
iconName={isPublic ? 'public' : 'vpn_lock'}
|
|
||||||
/>
|
|
||||||
<span className="sr-only">
|
|
||||||
{isPublic
|
|
||||||
? t('Accessible to anyone')
|
|
||||||
: t('Accessible to authenticated users')}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
</StyledLink>
|
</StyledLink>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
@@ -159,11 +92,13 @@ export const DocsGridItem = ({ doc, dragMode = false }: DocsGridItemProps) => {
|
|||||||
$gap="32px"
|
$gap="32px"
|
||||||
role="gridcell"
|
role="gridcell"
|
||||||
>
|
>
|
||||||
<DocsGridItemDate
|
<StyledLink href={`/docs/${doc.id}`} tabIndex={-1}>
|
||||||
doc={doc}
|
<DocsGridItemDate
|
||||||
isDesktop={isDesktop}
|
doc={doc}
|
||||||
isInTrashbin={isInTrashbin}
|
isDesktop={isDesktop}
|
||||||
/>
|
isInTrashbin={isInTrashbin}
|
||||||
|
/>
|
||||||
|
</StyledLink>
|
||||||
|
|
||||||
<Box $direction="row" $align="center" $gap={spacingsTokens.lg}>
|
<Box $direction="row" $align="center" $gap={spacingsTokens.lg}>
|
||||||
{isDesktop && (
|
{isDesktop && (
|
||||||
@@ -181,6 +116,86 @@ export const DocsGridItem = ({ doc, dragMode = false }: DocsGridItemProps) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const DocsGridItemTitle = ({
|
||||||
|
doc,
|
||||||
|
withTooltip,
|
||||||
|
}: {
|
||||||
|
doc: Doc;
|
||||||
|
withTooltip: boolean;
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { isDesktop } = useResponsiveStore();
|
||||||
|
const { spacingsTokens } = useCunninghamTheme();
|
||||||
|
const isPublic = doc.link_reach === LinkReach.PUBLIC;
|
||||||
|
const isAuthenticated = doc.link_reach === LinkReach.AUTHENTICATED;
|
||||||
|
const isShared = isPublic || isAuthenticated;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
data-testid={`docs-grid-name-${doc.id}`}
|
||||||
|
$direction="row"
|
||||||
|
$align="center"
|
||||||
|
$gap={spacingsTokens.xs}
|
||||||
|
$padding={{ right: isDesktop ? 'md' : '3xs' }}
|
||||||
|
$maxWidth="100%"
|
||||||
|
>
|
||||||
|
<SimpleDocItem isPinned={doc.is_favorite} doc={doc} />
|
||||||
|
{isShared && (
|
||||||
|
<Box
|
||||||
|
$padding={{ top: !isDesktop ? '4xs' : undefined }}
|
||||||
|
$css={
|
||||||
|
!isDesktop
|
||||||
|
? css`
|
||||||
|
align-self: flex-start;
|
||||||
|
`
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{withTooltip ? (
|
||||||
|
<Tooltip
|
||||||
|
content={
|
||||||
|
<Text $textAlign="center">
|
||||||
|
{isPublic
|
||||||
|
? t('Accessible to anyone')
|
||||||
|
: t('Accessible to authenticated users')}
|
||||||
|
</Text>
|
||||||
|
}
|
||||||
|
placement="top"
|
||||||
|
>
|
||||||
|
<Box>
|
||||||
|
<IconPublic isPublic={isPublic} />
|
||||||
|
</Box>
|
||||||
|
</Tooltip>
|
||||||
|
) : (
|
||||||
|
<IconPublic isPublic={isPublic} />
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const IconPublic = ({ isPublic }: { isPublic: boolean }) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Icon
|
||||||
|
$layer="background"
|
||||||
|
$theme="neutral"
|
||||||
|
$variation="primary"
|
||||||
|
$size="sm"
|
||||||
|
iconName={isPublic ? 'public' : 'vpn_lock'}
|
||||||
|
/>
|
||||||
|
<span className="sr-only">
|
||||||
|
{isPublic
|
||||||
|
? t('Accessible to anyone')
|
||||||
|
: t('Accessible to authenticated users')}
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const DocsGridItemDate = ({
|
export const DocsGridItemDate = ({
|
||||||
doc,
|
doc,
|
||||||
isDesktop,
|
isDesktop,
|
||||||
@@ -210,15 +225,8 @@ export const DocsGridItemDate = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledLink href={`/docs/${doc.id}`} tabIndex={-1}>
|
<Text $size="xs" $layer="background" $theme="neutral" $variation="primary">
|
||||||
<Text
|
{dateToDisplay}
|
||||||
$size="xs"
|
</Text>
|
||||||
$layer="background"
|
|
||||||
$theme="neutral"
|
|
||||||
$variation="primary"
|
|
||||||
>
|
|
||||||
{dateToDisplay}
|
|
||||||
</Text>
|
|
||||||
</StyledLink>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,7 +6,12 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import { css } from 'styled-components';
|
import { css } from 'styled-components';
|
||||||
|
|
||||||
import { DropdownMenu, DropdownMenuOption, Icon } from '@/components';
|
import { DropdownMenu, DropdownMenuOption, Icon } from '@/components';
|
||||||
import { Doc, KEY_LIST_DOC, useRestoreDoc } from '@/docs/doc-management';
|
import {
|
||||||
|
Doc,
|
||||||
|
KEY_LIST_DOC,
|
||||||
|
useRestoreDoc,
|
||||||
|
useTrans,
|
||||||
|
} from '@/docs/doc-management';
|
||||||
|
|
||||||
import { KEY_LIST_DOC_TRASHBIN } from '../api';
|
import { KEY_LIST_DOC_TRASHBIN } from '../api';
|
||||||
|
|
||||||
@@ -18,6 +23,7 @@ export const DocsGridTrashbinActions = ({
|
|||||||
doc,
|
doc,
|
||||||
}: DocsGridTrashbinActionsProps) => {
|
}: DocsGridTrashbinActionsProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const { untitledDocument } = useTrans();
|
||||||
const { toast } = useToastProvider();
|
const { toast } = useToastProvider();
|
||||||
const { mutate: restoreDoc, error } = useRestoreDoc({
|
const { mutate: restoreDoc, error } = useRestoreDoc({
|
||||||
listInvalidQueries: [KEY_LIST_DOC, KEY_LIST_DOC_TRASHBIN],
|
listInvalidQueries: [KEY_LIST_DOC, KEY_LIST_DOC_TRASHBIN],
|
||||||
@@ -61,7 +67,7 @@ export const DocsGridTrashbinActions = ({
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const documentTitle = doc.title || t('Untitled document');
|
const documentTitle = doc.title || untitledDocument;
|
||||||
const menuLabel = t('Open the menu of actions for the document: {{title}}', {
|
const menuLabel = t('Open the menu of actions for the document: {{title}}', {
|
||||||
title: documentTitle,
|
title: documentTitle,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -18,7 +18,9 @@ describe('DocsGridItemDate', () => {
|
|||||||
it('should not render date when not on desktop', () => {
|
it('should not render date when not on desktop', () => {
|
||||||
render(
|
render(
|
||||||
<DocsGridItemDate
|
<DocsGridItemDate
|
||||||
doc={{} as Doc}
|
doc={
|
||||||
|
{ updated_at: DateTime.now().minus({ minutes: 1 }).toISO() } as Doc
|
||||||
|
}
|
||||||
isDesktop={false}
|
isDesktop={false}
|
||||||
isInTrashbin={false}
|
isInTrashbin={false}
|
||||||
/>,
|
/>,
|
||||||
@@ -27,7 +29,7 @@ describe('DocsGridItemDate', () => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(screen.queryByRole('link')).not.toBeInTheDocument();
|
expect(screen.queryByText('1 minute ago')).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
[
|
[
|
||||||
@@ -66,7 +68,6 @@ describe('DocsGridItemDate', () => {
|
|||||||
{ wrapper: AppWrapper },
|
{ wrapper: AppWrapper },
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(screen.getByRole('link')).toBeInTheDocument();
|
|
||||||
expect(screen.getByText(rendered)).toBeInTheDocument();
|
expect(screen.getByText(rendered)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -87,7 +88,6 @@ describe('DocsGridItemDate', () => {
|
|||||||
{ wrapper: AppWrapper },
|
{ wrapper: AppWrapper },
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(screen.getByRole('link')).toBeInTheDocument();
|
|
||||||
expect(screen.getByText('il y a 5 jours')).toBeInTheDocument();
|
expect(screen.getByText('il y a 5 jours')).toBeInTheDocument();
|
||||||
|
|
||||||
await i18next.changeLanguage('en');
|
await i18next.changeLanguage('en');
|
||||||
@@ -134,7 +134,6 @@ describe('DocsGridItemDate', () => {
|
|||||||
{ wrapper: AppWrapper },
|
{ wrapper: AppWrapper },
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(screen.getByRole('link')).toBeInTheDocument();
|
|
||||||
await waitFor(
|
await waitFor(
|
||||||
() => {
|
() => {
|
||||||
expect(screen.getByText(rendered)).toBeInTheDocument();
|
expect(screen.getByText(rendered)).toBeInTheDocument();
|
||||||
|
|||||||
Reference in New Issue
Block a user