✨(frontend) implement document filtering
- Introduced a new enum for default document filters to improve code clarity. - Updated the API call to support filtering documents based on the creator. - Enhanced the DocsGrid component to accept a target filter, allowing dynamic content rendering based on user selection. - Modified the main layout to include a left panel for improved navigation and user experience. - Added a new test suite for document filters, verifying the visibility and selection states of 'All docs', 'My docs', and 'Shared with me'.
This commit is contained in:
committed by
Anthony LC
parent
42d9fa70a2
commit
7696872416
@@ -183,6 +183,80 @@ test.describe('Document grid item options', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Documents filters', () => {
|
||||
test('it checks the prebuild left panel filters', async ({ page }) => {
|
||||
// All Docs
|
||||
await expect(page.getByTestId('docs-grid-loader')).toBeVisible();
|
||||
const response = await page.waitForResponse(
|
||||
(response) =>
|
||||
response.url().includes('documents/?page=1') &&
|
||||
response.status() === 200,
|
||||
);
|
||||
const result = await response.json();
|
||||
const allCount = result.count as number;
|
||||
await expect(page.getByTestId('docs-grid-loader')).toBeHidden();
|
||||
|
||||
const allDocs = page.getByLabel('All docs');
|
||||
const myDocs = page.getByLabel('My docs');
|
||||
const sharedWithMe = page.getByLabel('Shared with me');
|
||||
|
||||
// Initial state
|
||||
await expect(allDocs).toBeVisible();
|
||||
await expect(allDocs).toHaveCSS('background-color', 'rgb(238, 238, 238)');
|
||||
await expect(allDocs).toHaveAttribute('aria-selected', 'true');
|
||||
|
||||
await expect(myDocs).toBeVisible();
|
||||
await expect(myDocs).toHaveCSS('background-color', 'rgba(0, 0, 0, 0)');
|
||||
await expect(myDocs).toHaveAttribute('aria-selected', 'false');
|
||||
|
||||
await expect(sharedWithMe).toBeVisible();
|
||||
await expect(sharedWithMe).toHaveCSS(
|
||||
'background-color',
|
||||
'rgba(0, 0, 0, 0)',
|
||||
);
|
||||
await expect(sharedWithMe).toHaveAttribute('aria-selected', 'false');
|
||||
|
||||
await allDocs.click();
|
||||
|
||||
let url = new URL(page.url());
|
||||
let target = url.searchParams.get('target');
|
||||
expect(target).toBe('all_docs');
|
||||
|
||||
// My docs
|
||||
await myDocs.click();
|
||||
url = new URL(page.url());
|
||||
target = url.searchParams.get('target');
|
||||
expect(target).toBe('my_docs');
|
||||
await expect(page.getByTestId('docs-grid-loader')).toBeVisible();
|
||||
const responseMyDocs = await page.waitForResponse(
|
||||
(response) =>
|
||||
response.url().includes('documents/?page=1&is_creator_me=true') &&
|
||||
response.status() === 200,
|
||||
);
|
||||
const resultMyDocs = await responseMyDocs.json();
|
||||
const countMyDocs = resultMyDocs.count as number;
|
||||
await expect(page.getByTestId('docs-grid-loader')).toBeHidden();
|
||||
expect(countMyDocs).toBeLessThanOrEqual(allCount);
|
||||
|
||||
// Shared with me
|
||||
await sharedWithMe.click();
|
||||
url = new URL(page.url());
|
||||
target = url.searchParams.get('target');
|
||||
expect(target).toBe('shared_with_me');
|
||||
await expect(page.getByTestId('docs-grid-loader')).toBeVisible();
|
||||
const responseSharedWithMe = await page.waitForResponse(
|
||||
(response) =>
|
||||
response.url().includes('documents/?page=1&is_creator_me=false') &&
|
||||
response.status() === 200,
|
||||
);
|
||||
const resultSharedWithMe = await responseSharedWithMe.json();
|
||||
const countSharedWithMe = resultSharedWithMe.count as number;
|
||||
await expect(page.getByTestId('docs-grid-loader')).toBeHidden();
|
||||
expect(countSharedWithMe).toBeLessThanOrEqual(allCount);
|
||||
expect(countSharedWithMe + countMyDocs).toEqual(allCount);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Documents Grid', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/');
|
||||
|
||||
@@ -356,6 +356,10 @@ input:-webkit-autofill:focus {
|
||||
gap: var(--c--theme--spacings--2xs);
|
||||
}
|
||||
|
||||
.c__button--nano.c__button--icon-only {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.c__button--medium {
|
||||
padding: 0.9rem var(--c--theme--spacings--s);
|
||||
}
|
||||
|
||||
@@ -28,16 +28,24 @@ export type DocsOrdering = (typeof docsOrdering)[number];
|
||||
export type DocsParams = {
|
||||
page: number;
|
||||
ordering?: DocsOrdering;
|
||||
is_creator_me?: boolean;
|
||||
};
|
||||
|
||||
export type DocsResponse = APIList<Doc>;
|
||||
export const getDocs = async (params: DocsParams): Promise<DocsResponse> => {
|
||||
const searchParams = new URLSearchParams();
|
||||
if (params.page) {
|
||||
searchParams.set('page', params.page.toString());
|
||||
}
|
||||
|
||||
export const getDocs = async ({
|
||||
ordering,
|
||||
page,
|
||||
}: DocsParams): Promise<DocsResponse> => {
|
||||
const orderingQuery = ordering ? `&ordering=${ordering}` : '';
|
||||
const response = await fetchAPI(`documents/?page=${page}${orderingQuery}`);
|
||||
if (params.ordering) {
|
||||
searchParams.set('ordering', params.ordering);
|
||||
}
|
||||
if (params.is_creator_me !== undefined) {
|
||||
searchParams.set('is_creator_me', params.is_creator_me.toString());
|
||||
}
|
||||
|
||||
const response = await fetchAPI(`documents/?${searchParams.toString()}`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new APIError('Failed to get the docs', await errorCauses(response));
|
||||
|
||||
@@ -59,3 +59,9 @@ export interface Doc {
|
||||
versions_retrieve: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export enum DocDefaultFilter {
|
||||
ALL_DOCS = 'all_docs',
|
||||
MY_DOCS = 'my_docs',
|
||||
SHARED_WITH_ME = 'shared_with_me',
|
||||
}
|
||||
|
||||
@@ -5,12 +5,17 @@ import { InView } from 'react-intersection-observer';
|
||||
import { Box, Card, Text } from '@/components';
|
||||
import { useResponsiveStore } from '@/stores';
|
||||
|
||||
import { useInfiniteDocs } from '../../doc-management';
|
||||
import { DocDefaultFilter, useInfiniteDocs } from '../../doc-management';
|
||||
|
||||
import { DocsGridItem } from './DocsGridItem';
|
||||
import { DocsGridLoader } from './DocsGridLoader';
|
||||
|
||||
export const DocsGrid = () => {
|
||||
type DocsGridProps = {
|
||||
target?: DocDefaultFilter;
|
||||
};
|
||||
export const DocsGrid = ({
|
||||
target = DocDefaultFilter.ALL_DOCS,
|
||||
}: DocsGridProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { isDesktop } = useResponsiveStore();
|
||||
@@ -24,6 +29,10 @@ export const DocsGrid = () => {
|
||||
hasNextPage,
|
||||
} = useInfiniteDocs({
|
||||
page: 1,
|
||||
...(target &&
|
||||
target !== DocDefaultFilter.ALL_DOCS && {
|
||||
is_creator_me: target === DocDefaultFilter.MY_DOCS,
|
||||
}),
|
||||
});
|
||||
const loading = isFetching || isLoading;
|
||||
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
|
||||
import { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { css } from 'styled-components';
|
||||
|
||||
import { Box, BoxButton, Icon, Text } from '@/components';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
import { DocDefaultFilter } from '@/features/docs';
|
||||
import { useLeftPanelStore } from '@/features/left-panel';
|
||||
|
||||
export const LeftPanelTargetFilters = () => {
|
||||
const { t } = useTranslation();
|
||||
const pathname = usePathname();
|
||||
const { togglePanel } = useLeftPanelStore();
|
||||
const { colorsTokens, spacingsTokens } = useCunninghamTheme();
|
||||
const spacing = spacingsTokens();
|
||||
const colors = colorsTokens();
|
||||
|
||||
const searchParams = useSearchParams();
|
||||
const target =
|
||||
(searchParams.get('target') as DocDefaultFilter) ??
|
||||
DocDefaultFilter.ALL_DOCS;
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const defaultQueries = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
icon: 'apps',
|
||||
label: t('All docs'),
|
||||
targetQuery: DocDefaultFilter.ALL_DOCS,
|
||||
},
|
||||
{
|
||||
icon: 'lock',
|
||||
label: t('My docs'),
|
||||
targetQuery: DocDefaultFilter.MY_DOCS,
|
||||
},
|
||||
{
|
||||
icon: 'group',
|
||||
label: t('Shared with me'),
|
||||
targetQuery: DocDefaultFilter.SHARED_WITH_ME,
|
||||
},
|
||||
];
|
||||
}, [t]);
|
||||
|
||||
const onSelectQuery = (query: DocDefaultFilter) => {
|
||||
const params = new URLSearchParams(searchParams);
|
||||
params.set('target', query);
|
||||
router.replace(`${pathname}?${params.toString()}`);
|
||||
togglePanel();
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
$justify="center"
|
||||
$padding={{ horizontal: 'xs' }}
|
||||
$gap={spacing['2xs']}
|
||||
>
|
||||
{defaultQueries.map((query) => {
|
||||
const isActive = target === query.targetQuery;
|
||||
|
||||
return (
|
||||
<BoxButton
|
||||
aria-label={query.label}
|
||||
key={query.label}
|
||||
onClick={() => onSelectQuery(query.targetQuery)}
|
||||
$direction="row"
|
||||
aria-selected={isActive}
|
||||
$align="center"
|
||||
$justify="flex-start"
|
||||
$gap={spacing['xs']}
|
||||
$radius={spacing['2xs']}
|
||||
$padding={{ all: 'xs' }}
|
||||
$css={css`
|
||||
cursor: pointer;
|
||||
background-color: ${isActive
|
||||
? colors['greyscale-100']
|
||||
: undefined};
|
||||
font-weight: ${isActive ? 700 : undefined};
|
||||
&:hover {
|
||||
background-color: ${colors['greyscale-100']};
|
||||
font-weight: 700;
|
||||
}
|
||||
`}
|
||||
>
|
||||
<Icon $variation="1000" iconName={query.icon} />
|
||||
<Text $variation="1000">{query.label}</Text>
|
||||
</BoxButton>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -1,4 +1,3 @@
|
||||
import { PropsWithChildren } from 'react';
|
||||
import { createGlobalStyle, css } from 'styled-components';
|
||||
|
||||
import { Box, SeparatedSection } from '@/components';
|
||||
@@ -10,6 +9,7 @@ import { useResponsiveStore } from '@/stores';
|
||||
|
||||
import { useLeftPanelStore } from '../stores';
|
||||
|
||||
import { LeftPanelContent } from './LeftPanelContent';
|
||||
import { LeftPanelHeader } from './LeftPanelHeader';
|
||||
|
||||
const MobileLeftPanelStyle = createGlobalStyle`
|
||||
@@ -18,7 +18,7 @@ const MobileLeftPanelStyle = createGlobalStyle`
|
||||
}
|
||||
`;
|
||||
|
||||
export const LeftPanel = ({ children }: PropsWithChildren) => {
|
||||
export const LeftPanel = () => {
|
||||
const { isDesktop } = useResponsiveStore();
|
||||
const { isPanelOpen } = useLeftPanelStore();
|
||||
const theme = useCunninghamTheme();
|
||||
@@ -37,7 +37,8 @@ export const LeftPanel = ({ children }: PropsWithChildren) => {
|
||||
border-right: 1px solid ${colors['greyscale-200']};
|
||||
`}
|
||||
>
|
||||
<LeftPanelHeader>{children}</LeftPanelHeader>
|
||||
<LeftPanelHeader />
|
||||
<LeftPanelContent />
|
||||
</Box>
|
||||
)}
|
||||
|
||||
@@ -65,7 +66,8 @@ export const LeftPanel = ({ children }: PropsWithChildren) => {
|
||||
gap: ${spacings['base']};
|
||||
`}
|
||||
>
|
||||
<LeftPanelHeader>{children}</LeftPanelHeader>
|
||||
<LeftPanelHeader />
|
||||
<LeftPanelContent />
|
||||
<SeparatedSection showSeparator={false}>
|
||||
<Box $justify="center" $align="center" $gap={spacings['sm']}>
|
||||
<ButtonLogin />
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
import { Box, SeparatedSection } from '@/components';
|
||||
|
||||
import { LeftPanelTargetFilters } from './LefPanelTargetFilters';
|
||||
|
||||
export const LeftPanelContent = () => {
|
||||
const router = useRouter();
|
||||
const isHome = router.pathname === '/';
|
||||
|
||||
return (
|
||||
<Box $width="100%">
|
||||
{isHome && (
|
||||
<SeparatedSection>
|
||||
<LeftPanelTargetFilters />
|
||||
</SeparatedSection>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -1,14 +1,19 @@
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import type { ReactElement } from 'react';
|
||||
|
||||
import { Box } from '@/components';
|
||||
import { DocDefaultFilter } from '@/features/docs';
|
||||
import { DocsGrid } from '@/features/docs/docs-grid/components/DocsGrid';
|
||||
import { MainLayout } from '@/layouts';
|
||||
import { NextPageWithLayout } from '@/types/next';
|
||||
|
||||
const Page: NextPageWithLayout = () => {
|
||||
const searchParams = useSearchParams();
|
||||
const target = searchParams.get('target');
|
||||
|
||||
return (
|
||||
<Box $width="100%" $align="center">
|
||||
<DocsGrid />
|
||||
<DocsGrid target={target as DocDefaultFilter} />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user