♻️(frontend) Add default data to move modal
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.
This commit is contained in:
@@ -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');
|
||||
|
||||
@@ -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 ({
|
||||
|
||||
@@ -45,22 +45,13 @@ export const QuickSearch = ({
|
||||
}: PropsWithChildren<QuickSearchProps>) => {
|
||||
const ref = useRef<HTMLDivElement | null>(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 && (
|
||||
<QuickSearchInput
|
||||
@@ -82,8 +72,6 @@ export const QuickSearch = ({
|
||||
onFilter={onFilter}
|
||||
placeholder={placeholder}
|
||||
listId={listId}
|
||||
isExpanded={isExpanded}
|
||||
onUserInteract={handleUserInteract}
|
||||
>
|
||||
{inputContent}
|
||||
</QuickSearchInput>
|
||||
|
||||
@@ -18,7 +18,7 @@ export const QuickSearchGroup = <T,>({
|
||||
renderElement,
|
||||
}: Props<T>) => {
|
||||
return (
|
||||
<Box $margin={{ top: 'sm' }}>
|
||||
<Box>
|
||||
<Command.Group
|
||||
key={group.groupName}
|
||||
heading={group.groupName}
|
||||
|
||||
@@ -14,7 +14,6 @@ type QuickSearchInputProps = {
|
||||
placeholder?: string;
|
||||
withSeparator?: boolean;
|
||||
listId?: string;
|
||||
onUserInteract?: () => void;
|
||||
isExpanded?: boolean;
|
||||
};
|
||||
export const QuickSearchInput = ({
|
||||
@@ -24,8 +23,6 @@ export const QuickSearchInput = ({
|
||||
children,
|
||||
withSeparator: separator = true,
|
||||
listId,
|
||||
onUserInteract,
|
||||
isExpanded,
|
||||
}: PropsWithChildren<QuickSearchInputProps>) => {
|
||||
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' }}
|
||||
>
|
||||
<Icon iconName="search" $variation="secondary" aria-hidden="true" />
|
||||
<Command.Input
|
||||
autoFocus={true}
|
||||
aria-label={t('Quick search input')}
|
||||
aria-expanded={isExpanded}
|
||||
aria-controls={listId}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onUserInteract?.();
|
||||
}}
|
||||
onKeyDown={() => onUserInteract?.()}
|
||||
value={inputValue}
|
||||
role="combobox"
|
||||
placeholder={placeholder ?? t('Search')}
|
||||
@@ -67,7 +58,7 @@ export const QuickSearchInput = ({
|
||||
data-testid="quick-search-input"
|
||||
/>
|
||||
</Box>
|
||||
{separator && <HorizontalSeparator $withPadding={false} />}
|
||||
{separator && <HorizontalSeparator $margin={{ top: 'base' }} />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
+39
-15
@@ -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<QuickSearchData<Doc>>({
|
||||
groupName: '',
|
||||
groupKey: 'docs',
|
||||
elements: [],
|
||||
emptyString: t('Loading documents...'),
|
||||
endActions: [],
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (loading) {
|
||||
return;
|
||||
}
|
||||
|
||||
const docsData: QuickSearchData<Doc> = 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: <InView onChange={() => void fetchNextPage()} />,
|
||||
content: (
|
||||
<Box $minHeight="1px">
|
||||
<InView onChange={() => void fetchNextPage()} />
|
||||
</Box>
|
||||
),
|
||||
},
|
||||
]
|
||||
: [],
|
||||
};
|
||||
}, [search, data?.pages, fetchNextPage, hasNextPage, filterResults]);
|
||||
});
|
||||
}, [
|
||||
search,
|
||||
data?.pages,
|
||||
fetchNextPage,
|
||||
hasNextPage,
|
||||
filterResults,
|
||||
groupName,
|
||||
isSearchNotMandatory,
|
||||
loading,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
onLoadingChange?.(loading);
|
||||
|
||||
@@ -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={
|
||||
<Box $direction="row-reverse" $padding="md" $gap="small">
|
||||
<Box
|
||||
$direction="row-reverse"
|
||||
$padding={{ vertical: 'base', horizontal: 'md' }}
|
||||
$gap="small"
|
||||
>
|
||||
<Button
|
||||
aria-label={t('Move the document to the selected location')}
|
||||
variant="primary"
|
||||
@@ -144,21 +144,22 @@ export const DocMoveModal = ({
|
||||
}
|
||||
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>
|
||||
<Box $position="absolute" $css="top: 4px; right: 4px;">
|
||||
<ButtonCloseModal
|
||||
aria-label={t('Close the move modal')}
|
||||
onClick={() => onClose()}
|
||||
/>
|
||||
</Box>
|
||||
<Box $margin={{ top: 'sm' }}>
|
||||
<Text
|
||||
as="h2"
|
||||
$margin="0"
|
||||
$size="h6"
|
||||
$align="flex-start"
|
||||
$padding={{ horizontal: 'md', top: 'md' }}
|
||||
>
|
||||
{t('Move')}
|
||||
</Text>
|
||||
<Box $margin={{ top: 'sm' }} $padding={{ horizontal: 'md' }}>
|
||||
<Text
|
||||
$size="sm"
|
||||
$variation="secondary"
|
||||
@@ -171,6 +172,7 @@ export const DocMoveModal = ({
|
||||
</Trans>
|
||||
</Text>
|
||||
</Box>
|
||||
<HorizontalSeparator />
|
||||
</Box>
|
||||
}
|
||||
>
|
||||
@@ -195,81 +197,64 @@ export const DocMoveModal = ({
|
||||
onFilter={handleInputSearch}
|
||||
>
|
||||
<Box
|
||||
$padding={{ horizontal: 'md' }}
|
||||
$height={isDesktop ? 'min(60vh, 500px)' : 'calc(100vh - 260px)'}
|
||||
$padding={{ horizontal: 'md', top: 'base' }}
|
||||
$height={isModal ? 'min(60vh, 350px)' : '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}
|
||||
onSelect={handleSelect}
|
||||
onLoadingChange={setLoading}
|
||||
renderSearchElement={(docSearch) => {
|
||||
const isSelected = docSelected?.id === docSearch.id;
|
||||
<Box>
|
||||
<DocSearchContent
|
||||
groupName={search ? t('Search results') : t('All docs')}
|
||||
search={search}
|
||||
filterResults={(docResults) => docResults.id !== doc.id}
|
||||
onSelect={handleSelect}
|
||||
onLoadingChange={setLoading}
|
||||
isSearchNotMandatory
|
||||
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)'
|
||||
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: 1px solid
|
||||
${isSelected
|
||||
? 'var(--c--contextuals--border--semantic--brand--tertiary)'
|
||||
: 'transparent'};
|
||||
border-radius: var(--c--globals--spacings--3xs);
|
||||
border-radius: var(--c--globals--spacings--3xs);
|
||||
|
||||
/* Arrow key navigation highlight */
|
||||
&[data-selected='true'] {
|
||||
${!isSelected &&
|
||||
`
|
||||
/* 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>
|
||||
)}
|
||||
}
|
||||
`}
|
||||
aria-selected={isSelected}
|
||||
>
|
||||
<DocsGridItemTitle
|
||||
doc={docSearch}
|
||||
withTooltip={false}
|
||||
/>
|
||||
<DocsGridItemDate
|
||||
doc={docSearch}
|
||||
isDesktop={isModal}
|
||||
isInTrashbin={false}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</QuickSearch>
|
||||
</Box>
|
||||
|
||||
+11
-13
@@ -3,7 +3,7 @@ import { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { css } from 'styled-components';
|
||||
|
||||
import { Box, Text } from '@/components';
|
||||
import { Card, Text } from '@/components';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
import { CLASS_DOC_TITLE } from '@/docs/doc-header/components/DocTitle';
|
||||
import { getEmojiAndTitle, useDocStore, useTrans } from '@/docs/doc-management';
|
||||
@@ -58,15 +58,14 @@ export const LeftPanelCollapseButton = () => {
|
||||
: t('Show the side panel for {{title}}', { title: docTitle });
|
||||
|
||||
return (
|
||||
<Box
|
||||
<Card
|
||||
className="--docs--left-panel-collapse-button"
|
||||
$direction="row"
|
||||
$css={css`
|
||||
display: inline-flex;
|
||||
padding: var(--c--globals--spacings--xxxs);
|
||||
align-items: center;
|
||||
gap: var(--c--globals--spacings--xxxs);
|
||||
border-radius: var(--c--globals--spacings--xs);
|
||||
border: 1px solid var(--c--contextuals--border--surface--primary);
|
||||
background: var(--c--contextuals--background--surface--primary);
|
||||
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.05);
|
||||
`}
|
||||
>
|
||||
@@ -79,13 +78,12 @@ export const LeftPanelCollapseButton = () => {
|
||||
variant="tertiary"
|
||||
icon={<LeftPanelIcon width={24} height={24} aria-hidden="true" />}
|
||||
data-testid="floating-bar-toggle-left-panel"
|
||||
>
|
||||
{shouldShowButtonTitle ? (
|
||||
<Text $size="sm" $weight={700} $color={colorsTokens['gray-1000']}>
|
||||
{buttonTitle}
|
||||
</Text>
|
||||
) : undefined}
|
||||
</Button>
|
||||
</Box>
|
||||
></Button>
|
||||
{shouldShowButtonTitle && (
|
||||
<Text $size="sm" $weight={700} $color={colorsTokens['gray-1000']}>
|
||||
{buttonTitle}
|
||||
</Text>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user