diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-create.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-create.spec.ts index 59096517b..3f91787fa 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/doc-create.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-create.spec.ts @@ -18,10 +18,9 @@ test.describe('Doc Create', () => { test('it creates a doc', async ({ page, browserName }) => { const [docTitle] = await createDoc(page, 'my-new-doc', browserName, 1); - await page.waitForFunction( - () => document.title.match(/my-new-doc - Docs/), - { timeout: 5000 }, - ); + await page.waitForFunction(() => document.title.match(/my-new-doc/), { + timeout: 5000, + }); await page.getByRole('button', { name: 'Back to homepage' }).click(); diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-grid-move.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-grid-move.spec.ts index 85ffe6e6e..c57fe0743 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/doc-grid-move.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-grid-move.spec.ts @@ -22,31 +22,32 @@ test.describe('Doc grid move', () => { browserName, }) => { await page.goto('/'); - await createDoc(page, 'Draggable doc', browserName, 1); - await page.getByRole('button', { name: 'Back to homepage' }).click(); - await createDoc(page, 'Droppable doc', browserName, 1); - await page.getByRole('button', { name: 'Back to homepage' }).click(); - - const response = await page.waitForResponse( - (response) => - response.url().endsWith('documents/?page=1') && - response.status() === 200, + const [draggableTitle] = await createDoc( + page, + 'Draggable doc', + browserName, + 1, ); - const responseJson = await response.json(); + const [droppableTitle] = await createDoc( + page, + 'Droppable doc', + browserName, + 1, + ); + await page.getByRole('button', { name: 'Back to homepage' }).click(); - const items = responseJson.results; + const draggableRow = await getGridRow(page, draggableTitle); + const droppableRow = await getGridRow(page, droppableTitle); const docsGrid = page.getByTestId('docs-grid'); await expect(docsGrid).toBeVisible(); await expect(page.getByTestId('grid-loader')).toBeHidden(); - const draggableElement = page.getByTestId(`draggable-doc-${items[1].id}`); - const dropZone = page.getByTestId(`droppable-doc-${items[0].id}`); - await expect(draggableElement).toBeVisible(); - await expect(dropZone).toBeVisible(); + await expect(draggableRow).toBeVisible(); + await expect(droppableRow).toBeVisible(); // Get the position of the elements - const draggableBoundingBox = await draggableElement.boundingBox(); - const dropZoneBoundingBox = await dropZone.boundingBox(); + const draggableBoundingBox = await draggableRow.boundingBox(); + const dropZoneBoundingBox = await droppableRow.boundingBox(); expect(draggableBoundingBox).toBeDefined(); expect(dropZoneBoundingBox).toBeDefined(); @@ -71,10 +72,11 @@ test.describe('Doc grid move', () => { const dragOverlay = page.getByTestId('drag-doc-overlay'); await expect(dragOverlay).toBeVisible(); - await expect(dragOverlay).toHaveText(items[1].title as string); + await expect(dragOverlay).toHaveText(draggableTitle); await page.mouse.up(); await expect(dragOverlay).toBeHidden(); + await expect(page.getByText(draggableTitle)).toBeHidden(); }); test("it checks can't drop when we have not the minimum role", async ({ diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-grid.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-grid.spec.ts index bf6fdb858..49cb3209a 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/doc-grid.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-grid.spec.ts @@ -298,34 +298,6 @@ test.describe('Documents filters', () => { }); test.describe('Documents Grid', () => { - test('checks all the elements are visible', async ({ page }) => { - void page.goto('/'); - - let docs: SmallDoc[]; - const response = await page.waitForResponse( - (response) => - response.url().endsWith('documents/?page=1') && - response.status() === 200, - ); - const result = await response.json(); - docs = result.results as SmallDoc[]; - - await expect(page.getByTestId('grid-loader')).toBeHidden(); - await expect(page.locator('h2').getByText('All docs')).toBeVisible(); - - const thead = page.getByTestId('docs-grid-header'); - await expect(thead.getByText(/Name/i)).toBeVisible(); - await expect(thead.getByText(/Last modified/i)).toBeVisible(); - - await Promise.all( - docs.map(async (doc) => { - await expect( - page.getByTestId(`docs-grid-name-${doc.id}`), - ).toBeVisible(); - }), - ); - }); - test('opens a document with keyboard (Tab + Enter)', async ({ page, browserName, @@ -353,14 +325,14 @@ test.describe('Documents Grid', () => { let docs: SmallDoc[]; const responsePromisePage1 = page.waitForResponse((response) => { return ( - response.url().endsWith(`/documents/?page=1`) && + response.url().endsWith(`/documents/?page=1&ordering=-updated_at`) && response.status() === 200 ); }); const responsePromisePage2 = page.waitForResponse( (response) => - response.url().endsWith(`/documents/?page=2`) && + response.url().endsWith(`/documents/?page=2&ordering=-updated_at`) && response.status() === 200, ); @@ -390,4 +362,42 @@ test.describe('Documents Grid', () => { }), ); }); + + test('it checks the sorting feature', async ({ page, browserName }) => { + await page.goto('/'); + + const [docA] = await createDoc(page, 'a-sorting-feat-aaa', browserName); + const [docB] = await createDoc(page, 'b-sorting-feat-bbb', browserName); + const [docZ] = await createDoc(page, 'z-sorting-feat-zzz', browserName); + + await page.getByRole('button', { name: 'Back to homepage' }).click(); + + const rowFilter = (text: string) => + page.getByTestId('docs-grid').getByRole('listitem').filter({ + hasText: text, + }); + + const row = rowFilter('sorting-feat'); + + // By default, the documents are sorted by descending order (last modified first) + await expect(row.nth(0).getByTestId('doc-title')).toHaveText(docZ); + await expect(row.nth(1).getByTestId('doc-title')).toHaveText(docB); + await expect(row.nth(2).getByTestId('doc-title')).toHaveText(docA); + + // Sort by ascending order - should be empty + await page.getByRole('button', { name: 'Sorted by Last modified' }).click(); + await expect(row).toHaveCount(0); + + // Sort by title ascending + await page.getByRole('button', { name: 'Sort by Name' }).click(); + await expect(rowFilter(docA)).toHaveCount(1); + await expect(rowFilter(docB)).toHaveCount(1); + await expect(rowFilter(docZ)).toHaveCount(0); + + // Sort by title descending + await page.getByRole('button', { name: 'Sorted by Name' }).click(); + await expect(rowFilter(docZ)).toHaveCount(1); + await expect(rowFilter(docA)).toHaveCount(0); + await expect(rowFilter(docB)).toHaveCount(0); + }); }); diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-member-create.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-member-create.spec.ts index 37817bc12..351f6f393 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/doc-member-create.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-member-create.spec.ts @@ -104,7 +104,7 @@ test.describe('Document create member', () => { ).toBeVisible(); // Select email and verify tag - const email = randomName('test@test.fr', browserName, 1)[0]; + const email = randomName('test@test.fr', browserName, 1, true)[0]; await inputSearch.fill(email); await quickSearchContent.getByText(email).click(); await expect(list.getByText(email)).toBeVisible(); @@ -164,7 +164,7 @@ test.describe('Document create member', () => { const inputSearch = page.getByTestId('quick-search-input'); - const [email] = randomName('test@test.fr', browserName, 1); + const [email] = randomName('test@test.fr', browserName, 1, true); await inputSearch.fill(email); await page.getByTestId(`search-user-row-${email}`).click(); diff --git a/src/frontend/apps/e2e/__tests__/app-impress/utils-common.ts b/src/frontend/apps/e2e/__tests__/app-impress/utils-common.ts index b42785719..2e9bf818e 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/utils-common.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/utils-common.ts @@ -88,9 +88,16 @@ export const getOtherBrowserName = (browserName: BrowserName) => { return otherBrowserName; }; -export const randomName = (name: string, browserName: string, length: number) => +export const randomName = ( + name: string, + browserName: string, + length: number, + reverseName = false, +) => Array.from({ length }, (_el, index) => { - return `${browserName}-${Math.floor(Math.random() * 10000)}-${index}-${name}`; + return reverseName + ? `${browserName}-${Math.floor(Math.random() * 10000)}-${index}-${name}` + : `${name}-${browserName}-${Math.floor(Math.random() * 10000)}-${index}`; }); export const openHeaderMenu = async (page: Page) => { @@ -203,9 +210,8 @@ export const getGridRow = async (page: Page, title: string) => { await expect(docsGrid).toBeVisible(); await expect(page.getByTestId('grid-loader')).toBeHidden(); - const rows = docsGrid.getByRole('listitem'); - - const row = rows + const row = docsGrid + .getByRole('listitem') .filter({ hasText: title, }) diff --git a/src/frontend/apps/e2e/__tests__/app-impress/utils-signin.ts b/src/frontend/apps/e2e/__tests__/app-impress/utils-signin.ts index 1c7efeadf..9dd7c28fa 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/utils-signin.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/utils-signin.ts @@ -24,15 +24,7 @@ export const customSignIn = async ( fromHome = true, ) => { // Check if already signed in (Silent login or session still valid) - if ( - await page - .locator('header') - .first() - .getByRole('button', { - name: 'Logout', - }) - .isVisible() - ) { + if (await page.getByLabel('User menu').isVisible()) { return; } diff --git a/src/frontend/apps/impress/src/assets/icons/ui-kit/arrow-up-down.svg b/src/frontend/apps/impress/src/assets/icons/ui-kit/arrow-up-down.svg index 5567837d9..e451b510c 100644 --- a/src/frontend/apps/impress/src/assets/icons/ui-kit/arrow-up-down.svg +++ b/src/frontend/apps/impress/src/assets/icons/ui-kit/arrow-up-down.svg @@ -1,4 +1,10 @@ - - - + + + diff --git a/src/frontend/apps/impress/src/assets/icons/ui-kit/clock.svg b/src/frontend/apps/impress/src/assets/icons/ui-kit/clock.svg index 1f9490559..06bc5be0d 100644 --- a/src/frontend/apps/impress/src/assets/icons/ui-kit/clock.svg +++ b/src/frontend/apps/impress/src/assets/icons/ui-kit/clock.svg @@ -1,3 +1,12 @@ - - + + + diff --git a/src/frontend/apps/impress/src/features/docs/docs-grid/api/useDocsGridQuery.tsx b/src/frontend/apps/impress/src/features/docs/docs-grid/api/useDocsGridQuery.tsx new file mode 100644 index 000000000..7a3c94e1f --- /dev/null +++ b/src/frontend/apps/impress/src/features/docs/docs-grid/api/useDocsGridQuery.tsx @@ -0,0 +1,37 @@ +import { + DocDefaultFilter, + DocsOrdering, + useInfiniteDocs, +} from '../../doc-management'; + +import { useInfiniteDocsTrashbin } from './useDocsTrashbin'; + +export const useDocsGridQuery = ( + target: DocDefaultFilter, + ordering: DocsOrdering, +) => { + const trashbinQuery = useInfiniteDocsTrashbin( + { + page: 1, + }, + { + enabled: target === DocDefaultFilter.TRASHBIN, + }, + ); + + const docsQuery = useInfiniteDocs( + { + page: 1, + ordering, + ...(target && + target !== DocDefaultFilter.ALL_DOCS && { + is_creator_me: target === DocDefaultFilter.MY_DOCS, + }), + }, + { + enabled: target !== DocDefaultFilter.TRASHBIN, + }, + ); + + return target === DocDefaultFilter.TRASHBIN ? trashbinQuery : docsQuery; +}; diff --git a/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocsGrid.tsx b/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocsGrid.tsx index 5bc15e9ef..679ecee0f 100644 --- a/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocsGrid.tsx +++ b/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocsGrid.tsx @@ -6,16 +6,17 @@ import { css } from 'styled-components'; import AllDocs from '@/assets/icons/doc-all.svg'; import { Box, Card, Icon, Loading, Text } from '@/components'; -import { useInfiniteDocs } from '@/docs/doc-management/api/useDocs'; +import { FadeComponent } from '@/components/Effect'; import { useImport } from '@/docs/doc-management/hooks/useImport'; -import { DocDefaultFilter } from '@/docs/doc-management/types'; +import { DocDefaultFilter, DocsOrdering } from '@/docs/doc-management/types'; import DocsIcon from '@/icons/Docs.svg'; import BinIcon from '@/icons/bin.svg'; import { useResponsiveStore } from '@/stores'; -import { useInfiniteDocsTrashbin } from '../api'; +import { useDocsGridQuery } from '../api/useDocsGridQuery'; import { DocGridContentList } from './DocGridContentList'; +import { DocsGridColumnName } from './DocsGridColumnName'; type DocsGridProps = { target?: DocDefaultFilter; @@ -44,8 +45,11 @@ export const DocsGrid = ({ const { isDesktop, isSmallMobile } = useResponsiveStore(); + const [ordering, setOrdering] = useState('-updated_at'); + const canSort = target !== DocDefaultFilter.TRASHBIN; + const { data, isFetching, isLoading, fetchNextPage, hasNextPage } = - useDocsQuery(target); + useDocsGridQuery(target, canSort ? ordering : undefined); const docs = useMemo(() => { const allDocs = data?.pages.flatMap((page) => page.results) ?? []; @@ -107,11 +111,11 @@ export const DocsGrid = ({ > {!hasDocs && !loading && } - {hasDocs && ( - + + - + - {loading && ( - - )} - {hasNextPage && !loading && ( - + {loading && ( + + )} + {hasNextPage && !loading && ( + + - )} - - )} + {t('More docs')} + + + )} + ); @@ -270,29 +251,3 @@ const DocGridNoDocs = ({ target }: { target: DocDefaultFilter }) => { ); }; - -const useDocsQuery = (target: DocDefaultFilter) => { - const trashbinQuery = useInfiniteDocsTrashbin( - { - page: 1, - }, - { - enabled: target === DocDefaultFilter.TRASHBIN, - }, - ); - - const docsQuery = useInfiniteDocs( - { - page: 1, - ...(target && - target !== DocDefaultFilter.ALL_DOCS && { - is_creator_me: target === DocDefaultFilter.MY_DOCS, - }), - }, - { - enabled: target !== DocDefaultFilter.TRASHBIN, - }, - ); - - return target === DocDefaultFilter.TRASHBIN ? trashbinQuery : docsQuery; -}; diff --git a/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocsGridActions.tsx b/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocsGridActions.tsx index b6413b65c..e07fd3bf1 100644 --- a/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocsGridActions.tsx +++ b/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocsGridActions.tsx @@ -291,7 +291,7 @@ const DocsGridDropdown = ({ doc, options }: DocsGridDropdownProps) => { aria-label={t('Open the menu of actions for the document: {{title}}', { title: doc.title || untitledDocument, })} - size="small" + size="nano" icon={} color="neutral" variant="tertiary" diff --git a/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocsGridColumnName.tsx b/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocsGridColumnName.tsx new file mode 100644 index 000000000..467d911d9 --- /dev/null +++ b/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocsGridColumnName.tsx @@ -0,0 +1,132 @@ +import { Button } from '@gouvfr-lasuite/cunningham-react'; +import { ReactNode } from 'react'; +import { useTranslation } from 'react-i18next'; +import { css } from 'styled-components'; + +import { Box, Text } from '@/components'; +import { DocDefaultFilter, DocsOrdering } from '@/docs/doc-management/types'; +import ArrowUpDownIcon from '@/icons/arrow-up-down.svg'; +import ClockIcon from '@/icons/clock.svg'; +import { useResponsiveStore } from '@/stores'; + +type DocsGridColumnNameProps = { + ordering: DocsOrdering; + setOrdering: React.Dispatch>; + target?: DocDefaultFilter; +}; + +export const DocsGridColumnName = ({ + target = DocDefaultFilter.ALL_DOCS, + ordering, + setOrdering, +}: DocsGridColumnNameProps) => { + const { t } = useTranslation(); + const { isSmallMobile } = useResponsiveStore(); + + const canSort = target !== DocDefaultFilter.TRASHBIN; + + const toggleOrdering = (field: 'title' | 'updated_at') => { + setOrdering((prevOrdering) => + prevOrdering === field ? (`-${field}` as DocsOrdering) : field, + ); + }; + + return ( + + + {canSort ? ( + toggleOrdering('title')} + /> + ) : ( + + {t('Name')} + + )} + + {!isSmallMobile && ( + + {canSort ? ( + + {' '} + {t('Last modified')} + + } + ariaLabel={t('Last modified')} + ordering={ordering} + field="updated_at" + onClick={() => toggleOrdering('updated_at')} + /> + ) : ( + + {t('Days remaining')} + + )} + + )} + + ); +}; + +const DocGridSortButton = ({ + label, + ariaLabel, + field, + ordering, + onClick, +}: { + label: ReactNode; + ariaLabel: string; + field: 'title' | 'updated_at'; + ordering: DocsOrdering; + onClick: () => void; +}) => { + const { t } = useTranslation(); + const isDesc = ordering === `-${field}`; + const isActive = ordering === field || isDesc; + + return ( + + + {label} + + } + variant="tertiary" + color={isActive ? 'brand' : 'neutral'} + /> + + ); +};