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 ae8c6fe4..01e9a86c 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 06855942..4d5a57bd 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 441e3e44..30c09d9a 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 c8eada9e..61be75c9 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 5b583ccf..1aba150b 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 0536714e..1fde46d6 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 37564668..25aca5aa 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} + + )} + ); };