diff --git a/CHANGELOG.md b/CHANGELOG.md index 8dbffb0f..b6232c41 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to - 🔧(backend) add option to configure list of essential OIDC claims #525 & #531 - 🔧(helm) add option to disable default tls setting by @dominikkaminski #519 - 💄(frontend) Add left panel #420 +- 💄(frontend) add filtering to left panel #475 ## Changed @@ -44,6 +45,9 @@ and this project adheres to + + + ## [1.9.0] - 2024-12-11 ## Added 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 533a97f6..c1bce7a0 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 @@ -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('/'); diff --git a/src/frontend/apps/impress/src/cunningham/cunningham-style.css b/src/frontend/apps/impress/src/cunningham/cunningham-style.css index c51a16d0..2a44b72d 100644 --- a/src/frontend/apps/impress/src/cunningham/cunningham-style.css +++ b/src/frontend/apps/impress/src/cunningham/cunningham-style.css @@ -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); } diff --git a/src/frontend/apps/impress/src/features/docs/doc-management/api/useDocs.tsx b/src/frontend/apps/impress/src/features/docs/doc-management/api/useDocs.tsx index f3339aa1..3df96b98 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-management/api/useDocs.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-management/api/useDocs.tsx @@ -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; +export const getDocs = async (params: DocsParams): Promise => { + const searchParams = new URLSearchParams(); + if (params.page) { + searchParams.set('page', params.page.toString()); + } -export const getDocs = async ({ - ordering, - page, -}: DocsParams): Promise => { - 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)); diff --git a/src/frontend/apps/impress/src/features/docs/doc-management/types.tsx b/src/frontend/apps/impress/src/features/docs/doc-management/types.tsx index 6c645240..2b38fe31 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-management/types.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-management/types.tsx @@ -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', +} 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 3f5d0a0e..10fb85ac 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 @@ -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; diff --git a/src/frontend/apps/impress/src/features/left-panel/components/LefPanelTargetFilters.tsx b/src/frontend/apps/impress/src/features/left-panel/components/LefPanelTargetFilters.tsx new file mode 100644 index 00000000..56999edc --- /dev/null +++ b/src/frontend/apps/impress/src/features/left-panel/components/LefPanelTargetFilters.tsx @@ -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 ( + + {defaultQueries.map((query) => { + const isActive = target === query.targetQuery; + + return ( + 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; + } + `} + > + + {query.label} + + ); + })} + + ); +}; diff --git a/src/frontend/apps/impress/src/features/left-panel/components/LeftPanel.tsx b/src/frontend/apps/impress/src/features/left-panel/components/LeftPanel.tsx index a32a600b..05b0a4cb 100644 --- a/src/frontend/apps/impress/src/features/left-panel/components/LeftPanel.tsx +++ b/src/frontend/apps/impress/src/features/left-panel/components/LeftPanel.tsx @@ -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']}; `} > - {children} + + )} @@ -65,7 +66,8 @@ export const LeftPanel = ({ children }: PropsWithChildren) => { gap: ${spacings['base']}; `} > - {children} + + diff --git a/src/frontend/apps/impress/src/features/left-panel/components/LeftPanelContent.tsx b/src/frontend/apps/impress/src/features/left-panel/components/LeftPanelContent.tsx new file mode 100644 index 00000000..8b0ab24b --- /dev/null +++ b/src/frontend/apps/impress/src/features/left-panel/components/LeftPanelContent.tsx @@ -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 ( + + {isHome && ( + + + + )} + + ); +}; diff --git a/src/frontend/apps/impress/src/pages/docs/index.tsx b/src/frontend/apps/impress/src/pages/docs/index.tsx index 1af902fa..ce4256a3 100644 --- a/src/frontend/apps/impress/src/pages/docs/index.tsx +++ b/src/frontend/apps/impress/src/pages/docs/index.tsx @@ -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 ( - + ); };