From 1944f6177e7fa602fc6e50ff3deeb1bed7ff6b4e Mon Sep 17 00:00:00 2001 From: Anthony LC Date: Wed, 4 Mar 2026 16:00:23 +0100 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F(frontend)=20Add=20default=20?= =?UTF-8?q?data=20to=20move=20modal?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit By default the move modal has the last updated doc in the list, when typing it starts filtering. In order to have this behaviour possible, we had to refactorise DocSearchContent and the QuickSearch components to be able to set a default value in the input and a default list of items. --- .../app-impress/doc-grid-move.spec.ts | 4 +- .../__tests__/app-impress/doc-header.spec.ts | 11 +- .../components/quick-search/QuickSearch.tsx | 28 +-- .../quick-search/QuickSearchGroup.tsx | 2 +- .../quick-search/QuickSearchInput.tsx | 13 +- .../quick-search/QuickSearchStyle.tsx | 1 - .../components/DocSearchContent.tsx | 54 ++++-- .../docs-grid/components/DocMoveModal.tsx | 169 ++++++++---------- .../components/LeftPanelCollapseButton.tsx | 24 ++- 9 files changed, 148 insertions(+), 158 deletions(-) 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 ae8c6fe41..01e9a86c9 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 @@ -302,7 +302,9 @@ test.describe('Doc grid move', () => { await input.click(); await input.fill(titleDoc2); - await expect(page.getByRole('option').getByText(titleDoc2)).toBeVisible(); + await expect( + page.getByRole('option').first().getByText(titleDoc2), + ).toBeVisible(); // Select the first result await page.keyboard.press('Enter'); diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-header.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-header.spec.ts index 068559420..4d5a57bd8 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/doc-header.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-header.spec.ts @@ -32,7 +32,10 @@ test.describe('Doc Header', () => { 1, ); - const collapseButton = page.getByTestId('floating-bar-toggle-left-panel'); + const cardCollapse = page.locator('.--docs--left-panel-collapse-button'); + const collapseButton = cardCollapse.getByTestId( + 'floating-bar-toggle-left-panel', + ); await expect(collapseButton).toBeVisible(); // Panel open @@ -42,7 +45,7 @@ test.describe('Doc Header', () => { // Collapse panel await collapseButton.click(); await expect(collapseButton).toHaveAttribute('aria-expanded', 'false'); - await expect(collapseButton.getByText(docTitle)).toBeHidden(); + await expect(cardCollapse.getByText(docTitle)).toBeHidden(); // When the title is not visible in the viewport, the button should show the title const editor = await writeInEditor({ page, text: 'Lorem ipsum' }); @@ -50,12 +53,12 @@ test.describe('Doc Header', () => { await editor.press('Enter'); } await writeInEditor({ page, text: 'Lorem ipsum 2' }); - await expect(collapseButton.getByText(docTitle)).toBeVisible(); + await expect(cardCollapse.getByText(docTitle)).toBeVisible(); // Expand panel and check the title is hidden again await collapseButton.click(); await expect(collapseButton).toHaveAttribute('aria-expanded', 'true'); - await expect(collapseButton.getByText(docTitle)).toBeHidden(); + await expect(cardCollapse.getByText(docTitle)).toBeHidden(); }); test('it checks the element are correctly displayed', async ({ diff --git a/src/frontend/apps/impress/src/components/quick-search/QuickSearch.tsx b/src/frontend/apps/impress/src/components/quick-search/QuickSearch.tsx index 441e3e44a..30c09d9a8 100644 --- a/src/frontend/apps/impress/src/components/quick-search/QuickSearch.tsx +++ b/src/frontend/apps/impress/src/components/quick-search/QuickSearch.tsx @@ -45,22 +45,13 @@ export const QuickSearch = ({ }: PropsWithChildren) => { const ref = useRef(null); const listId = useId(); - const NO_SELECTION_VALUE = '__none__'; - const [userInteracted, setUserInteracted] = useState(false); - const [selectedValue, setSelectedValue] = useState(NO_SELECTION_VALUE); - const isExpanded = userInteracted; - - const handleValueChange = (val: string) => { - if (userInteracted) { - setSelectedValue(val); - } - }; - - const handleUserInteract = () => { - if (!userInteracted) { - setUserInteracted(true); - } - }; + /** + * Hack to prevent cmdk from auto-selecting the first element on open + * + * TODO: Find a clean solution to prevent cmdk from auto-selecting + * the first element on open + */ + const [selectedValue, _] = useState('__none__'); return ( <> @@ -71,9 +62,8 @@ export const QuickSearch = ({ shouldFilter={false} ref={ref} tabIndex={-1} - value={selectedValue} - onValueChange={handleValueChange} disablePointerSelection + value={selectedValue} > {showInput && ( {inputContent} diff --git a/src/frontend/apps/impress/src/components/quick-search/QuickSearchGroup.tsx b/src/frontend/apps/impress/src/components/quick-search/QuickSearchGroup.tsx index c8eada9e7..61be75c99 100644 --- a/src/frontend/apps/impress/src/components/quick-search/QuickSearchGroup.tsx +++ b/src/frontend/apps/impress/src/components/quick-search/QuickSearchGroup.tsx @@ -18,7 +18,7 @@ export const QuickSearchGroup = ({ renderElement, }: Props) => { return ( - + void; isExpanded?: boolean; }; export const QuickSearchInput = ({ @@ -24,8 +23,6 @@ export const QuickSearchInput = ({ children, withSeparator: separator = true, listId, - onUserInteract, - isExpanded, }: PropsWithChildren) => { const { t } = useTranslation(); const { spacingsTokens } = useCunninghamTheme(); @@ -46,19 +43,13 @@ export const QuickSearchInput = ({ $align="center" className="quick-search-input" $gap={spacingsTokens['2xs']} - $padding={{ horizontal: 'base', vertical: 'sm' }} + $padding={{ horizontal: 'base', vertical: 'xxs' }} > - {separator && } + {separator && } ); }; diff --git a/src/frontend/apps/impress/src/components/quick-search/QuickSearchStyle.tsx b/src/frontend/apps/impress/src/components/quick-search/QuickSearchStyle.tsx index 5b583ccfc..1aba150b7 100644 --- a/src/frontend/apps/impress/src/components/quick-search/QuickSearchStyle.tsx +++ b/src/frontend/apps/impress/src/components/quick-search/QuickSearchStyle.tsx @@ -19,7 +19,6 @@ export const QuickSearchStyle = createGlobalStyle` border: none; width: 100%; font-size: 17px; - padding: var(--c--globals--spacings--xs); background: white; outline: none; color: var(--c--contextuals--content--semantic--neutral--primary); diff --git a/src/frontend/apps/impress/src/features/docs/doc-search/components/DocSearchContent.tsx b/src/frontend/apps/impress/src/features/docs/doc-search/components/DocSearchContent.tsx index 0536714e2..1fde46d64 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-search/components/DocSearchContent.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-search/components/DocSearchContent.tsx @@ -1,30 +1,31 @@ import { t } from 'i18next'; -import { useEffect, useMemo } from 'react'; +import { useEffect, useState } from 'react'; import { InView } from 'react-intersection-observer'; +import { Box } from '@/components/'; import { QuickSearchData, QuickSearchGroup } from '@/components/quick-search'; +import { Doc, useInfiniteDocs } from '@/docs/doc-management'; -import { Doc, useInfiniteDocs } from '../../doc-management'; - -import { DocSearchFiltersValues } from './DocSearchFilters'; import { DocSearchItem } from './DocSearchItem'; type DocSearchContentProps = { + groupName: string; search: string; - filters: DocSearchFiltersValues; filterResults?: (doc: Doc) => boolean; + isSearchNotMandatory?: boolean; onSelect: (doc: Doc) => void; onLoadingChange?: (loading: boolean) => void; renderSearchElement?: (doc: Doc) => React.ReactNode; }; export const DocSearchContent = ({ + groupName, search, - filters, filterResults, onSelect, onLoadingChange, renderSearchElement, + isSearchNotMandatory, }: DocSearchContentProps) => { const { data, @@ -35,33 +36,56 @@ export const DocSearchContent = ({ hasNextPage, } = useInfiniteDocs({ page: 1, - title: search, - ...filters, + ...(search ? { title: search } : {}), }); const loading = isFetching || isRefetching || isLoading; + const [docsData, setDocsData] = useState>({ + groupName: '', + groupKey: 'docs', + elements: [], + emptyString: t('Loading documents...'), + endActions: [], + }); + + useEffect(() => { + if (loading) { + return; + } - const docsData: QuickSearchData = useMemo(() => { let docs = data?.pages.flatMap((page) => page.results) || []; if (filterResults) { docs = docs.filter(filterResults); } - return { - groupName: docs.length > 0 ? t('Select a document') : '', + setDocsData({ + groupName: docs.length > 0 ? groupName : '', groupKey: 'docs', - elements: search ? docs : [], + elements: search || isSearchNotMandatory ? docs : [], emptyString: t('No document found'), endActions: hasNextPage ? [ { - content: void fetchNextPage()} />, + content: ( + + void fetchNextPage()} /> + + ), }, ] : [], - }; - }, [search, data?.pages, fetchNextPage, hasNextPage, filterResults]); + }); + }, [ + search, + data?.pages, + fetchNextPage, + hasNextPage, + filterResults, + groupName, + isSearchNotMandatory, + loading, + ]); useEffect(() => { onLoadingChange?.(loading); diff --git a/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocMoveModal.tsx b/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocMoveModal.tsx index 37564668e..25aca5aaf 100644 --- a/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocMoveModal.tsx +++ b/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocMoveModal.tsx @@ -5,17 +5,15 @@ import { useModal, } 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 { Box, ButtonCloseModal, HorizontalSeparator, 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 { DocSearchContent } from '@/docs/doc-search'; import { AlertModalRequestAccess } from '@/docs/doc-share'; import { useResponsiveStore } from '@/stores'; @@ -35,20 +33,17 @@ export const DocMoveModalStyle = createGlobalStyle` 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); + div.c__modal__title { + padding: 0; + margin-bottom: 0; } .c__modal__footer { margin-top: 0rem; + border-top: 1px solid var(--c--contextuals--border--surface--primary); } .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; } @@ -79,7 +74,8 @@ export const DocMoveModal = ({ const modalRequest = useModal(); const { mutate: moveDoc } = useMoveDoc(true); const [search, setSearch] = useState(''); - const { isDesktop } = useResponsiveStore(); + const { isDesktop, isTablet, isMobile } = useResponsiveStore(); + const isModal = (isDesktop || isTablet) && !isMobile; const handleInputSearch = useDebouncedCallback(setSearch, 700); const handleSelect = (docSelected: Doc) => { @@ -106,11 +102,15 @@ export const DocMoveModal = ({ isOpen={isOpen} onClose={onClose} closeOnClickOutside - size={isDesktop ? ModalSize.LARGE : ModalSize.FULL} + size={isModal ? ModalSize.MEDIUM : ModalSize.FULL} hideCloseButton aria-label={t('Move Modal')} rightActions={ - + - + > + {shouldShowButtonTitle && ( + + {buttonTitle} + + )} + ); };