🔥(frontend) remove docs-panel feature

Remove docs-panel feature, we will replace it
with a datagrid.
This commit is contained in:
Anthony LC
2024-07-05 14:13:00 +02:00
committed by Anthony LC
parent c27e6c1b06
commit 6f4b4bb7d3
19 changed files with 5 additions and 788 deletions

View File

@@ -1,159 +0,0 @@
import { expect, test } from '@playwright/test';
import { waitForElementCount } from '../helpers';
import { createDoc } from './common';
test.beforeEach(async ({ page }) => {
await page.goto('/');
});
test.describe('Documents Panel', () => {
test('checks all the elements are visible', async ({ page }) => {
const panel = page.getByLabel('Documents panel').first();
await expect(panel.getByText('Documents')).toBeVisible();
await expect(
panel.getByRole('button', {
name: 'Sort the documents',
}),
).toBeVisible();
await expect(
panel.getByRole('button', {
name: 'Add a document',
}),
).toBeVisible();
});
test('checks the sort button', async ({ page }) => {
const responsePromiseSortDesc = page.waitForResponse(
(response) =>
response.url().includes('/documents/?page=1&ordering=-created_at') &&
response.status() === 200,
);
const responsePromiseSortAsc = page.waitForResponse(
(response) =>
response.url().includes('/documents/?page=1&ordering=created_at') &&
response.status() === 200,
);
const panel = page.getByLabel('Documents panel').first();
await panel
.getByRole('button', {
name: 'Sort the documents by creation date ascendent',
})
.click();
const responseSortAsc = await responsePromiseSortAsc;
expect(responseSortAsc.ok()).toBeTruthy();
await panel
.getByRole('button', {
name: 'Sort the documents by creation date descendent',
})
.click();
const responseSortDesc = await responsePromiseSortDesc;
expect(responseSortDesc.ok()).toBeTruthy();
});
test('checks the infinite scroll', async ({ page }) => {
await page.route(
/.*\/documents\/\?page=.*&ordering=-created_at/,
async (route) => {
const request = route.request();
const url = new URL(request.url());
const pageId = url.searchParams.get('page');
const documents = {
count: 40,
next: 'http://localhost:3000/documents/?page=2&ordering=-created_at',
previous: null,
results: Array.from({ length: 20 }, (_, i) => ({
id: `2ff-${pageId}-${i}`,
title: `My document-${pageId}-${i}`,
accesses: [
{
id: 'b644e9b1-0517-4cfb-90ca-f7d6f2f6bb9a',
role: `owner`,
team: '',
user: {
id: 'a4743608-c9d8-4692-bef4-f795e25a3a88',
email: 'user@chromium.e2e',
},
},
],
content: '',
is_public: true,
abilities: {},
})),
};
if (request.method().includes('GET')) {
await route.fulfill({
json: documents,
});
} else {
await route.continue();
}
},
);
await page.route(`**/documents/2ff-1-16/`, async (route) => {
const request = route.request();
if (request.method().includes('GET')) {
await route.fulfill({
json: {
id: '2ff-1-16',
title: 'My document-1-16',
content: '',
abilities: {
partial_update: true,
},
accesses: [
{
id: 'b644e9b1-0517-4cfb-90ca-f7d6f2f6bb9a',
role: `owner`,
team: '',
user: {
id: 'a4743608-c9d8-4692-bef4-f795e25a3a88',
email: '',
},
},
],
},
});
} else {
await route.continue();
}
});
const panel = page.getByLabel('Documents panel').first();
await expect(panel.locator('li')).toHaveCount(20);
await panel.getByText(`My document-1-16`).click();
await waitForElementCount(panel.locator('li'), 21, 10000);
expect(await panel.locator('li').count()).toBeGreaterThan(20);
await expect(panel.getByText(`My document-1-16`)).toBeVisible();
await expect(panel.getByText(`My document-2-15`)).toBeVisible();
});
test('checks the hover and selected state', async ({ page, browserName }) => {
const panel = page.getByLabel('Documents panel').first();
await createDoc(page, 'doc-hover', browserName, 2);
const selectedDoc = panel.locator('li').nth(0);
await expect(selectedDoc).toHaveCSS(
'background-color',
'rgb(202, 202, 251)',
);
const hoverDoc = panel.locator('li').nth(1);
await hoverDoc.hover();
await expect(hoverDoc).toHaveCSS('background-color', 'rgb(227, 227, 253)');
});
});

View File

@@ -1,168 +0,0 @@
import '@testing-library/jest-dom';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import fetchMock from 'fetch-mock';
import { AppWrapper } from '@/tests/utils';
import { DocList } from '../components/DocList';
import { Panel } from '../components/Panel';
window.HTMLElement.prototype.scroll = function () {};
jest.mock('next/router', () => ({
...jest.requireActual('next/router'),
useRouter: () => ({
query: {},
}),
}));
describe('PanelDocs', () => {
afterEach(() => {
fetchMock.restore();
});
it('renders with no doc to display', async () => {
fetchMock.mock(`end:/documents/?page=1&ordering=-created_at`, {
count: 0,
results: [],
});
render(<DocList />, { wrapper: AppWrapper });
expect(screen.getByRole('status')).toBeInTheDocument();
expect(
await screen.findByText(
'Create your first document by clicking on the "Create a new document" button.',
),
).toBeInTheDocument();
});
it('renders an empty doc', async () => {
fetchMock.mock(`end:/documents/?page=1&ordering=-created_at`, {
count: 1,
results: [
{
id: '1',
name: 'Team 1',
accesses: [],
},
],
});
render(<DocList />, { wrapper: AppWrapper });
expect(screen.getByRole('status')).toBeInTheDocument();
expect(await screen.findByLabelText('Empty docs icon')).toBeInTheDocument();
});
it('renders a doc with only 1 member', async () => {
fetchMock.mock(`end:/documents/?page=1&ordering=-created_at`, {
count: 1,
results: [
{
id: '1',
name: 'Team 1',
accesses: [
{
id: '1',
role: 'owner',
},
],
},
],
});
render(<DocList />, { wrapper: AppWrapper });
expect(screen.getByRole('status')).toBeInTheDocument();
expect(await screen.findByLabelText('Empty docs icon')).toBeInTheDocument();
});
it('renders a non-empty doc', async () => {
fetchMock.mock(`end:/documents/?page=1&ordering=-created_at`, {
count: 1,
results: [
{
id: '1',
name: 'Doc 1',
accesses: [
{
id: '1',
role: 'admin',
},
{
id: '2',
role: 'member',
},
],
},
],
});
render(<DocList />, { wrapper: AppWrapper });
expect(screen.getByRole('status')).toBeInTheDocument();
expect(await screen.findByLabelText('Docs icon')).toBeInTheDocument();
});
it('renders the error', async () => {
fetchMock.mock(`end:/documents/?page=1&ordering=-created_at`, {
status: 500,
});
render(<DocList />, { wrapper: AppWrapper });
expect(screen.getByRole('status')).toBeInTheDocument();
expect(
await screen.findByText('Something bad happens, please retry.'),
).toBeInTheDocument();
});
it('renders with doc panel open', async () => {
fetchMock.mock(`end:/documents/?page=1&ordering=-created_at`, {
count: 1,
results: [],
});
render(<Panel />, { wrapper: AppWrapper });
expect(
screen.getByRole('button', { name: 'Close the documents panel' }),
).toBeVisible();
expect(await screen.findByText('Documents')).toBeVisible();
});
it('closes and opens the doc panel', async () => {
fetchMock.mock(`end:/documents/?page=1&ordering=-created_at`, {
count: 1,
results: [],
});
render(<Panel />, { wrapper: AppWrapper });
expect(await screen.findByText('Documents')).toBeVisible();
await userEvent.click(
screen.getByRole('button', {
name: 'Close the documents panel',
}),
);
expect(await screen.findByText('Documents')).not.toBeVisible();
await userEvent.click(
screen.getByRole('button', {
name: 'Open the documents panel',
}),
);
expect(await screen.findByText('Documents')).toBeVisible();
});
});

View File

@@ -1,6 +0,0 @@
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M20.75 0.75H3.25C1.8625 0.75 0.75 1.875 0.75 3.25V20.75C0.75 22.125 1.8625 23.25 3.25 23.25H20.75C22.125 23.25 23.25 22.125 23.25 20.75V3.25C23.25 1.875 22.125 0.75 20.75 0.75ZM17 13.25H13.25V17C13.25 17.6875 12.6875 18.25 12 18.25C11.3125 18.25 10.75 17.6875 10.75 17V13.25H7C6.3125 13.25 5.75 12.6875 5.75 12C5.75 11.3125 6.3125 10.75 7 10.75H10.75V7C10.75 6.3125 11.3125 5.75 12 5.75C12.6875 5.75 13.25 6.3125 13.25 7V10.75H17C17.6875 10.75 18.25 11.3125 18.25 12C18.25 12.6875 17.6875 13.25 17 13.25Z"
fill="currentColor"
/>
</svg>

Before

Width:  |  Height:  |  Size: 630 B

View File

@@ -1,13 +0,0 @@
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_508_5524)">
<path
d="M12 2C6.48 2 2 6.48 2 12C2 17.52 6.48 22 12 22C17.52 22 22 17.52 22 12C22 6.48 17.52 2 12 2ZM4 12C4 7.58 7.58 4 12 4C13.85 4 15.55 4.63 16.9 5.69L5.69 16.9C4.63 15.55 4 13.85 4 12ZM12 20C10.15 20 8.45 19.37 7.1 18.31L18.31 7.1C19.37 8.45 20 10.15 20 12C20 16.42 16.42 20 12 20Z"
fill="currentColor"
/>
</g>
<defs>
<clipPath id="clip0_508_5524">
<rect width="24" height="24" fill="white" />
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 578 B

View File

@@ -1,13 +0,0 @@
<svg viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_178_17837)">
<path
d="M11.25 3.75L6.25 8.7375H10V17.5H12.5V8.7375H16.25L11.25 3.75ZM20 21.2625V12.5H17.5V21.2625H13.75L18.75 26.25L23.75 21.2625H20Z"
fill="currentColor"
/>
</g>
<defs>
<clipPath id="clip0_178_17837">
<rect width="30" height="30" fill="white" />
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 429 B

View File

@@ -1,102 +0,0 @@
import { useRouter } from 'next/router';
import React from 'react';
import { useTranslation } from 'react-i18next';
import IconGroup from '@/assets/icons/icon-group.svg';
import { Box, StyledLink, Text } from '@/components';
import { useCunninghamTheme } from '@/cunningham';
import { Doc } from '@/features/docs/doc-management';
import IconNone from '../assets/icon-none.svg';
interface DocItemProps {
doc: Doc;
}
export const DocItem = ({ doc }: DocItemProps) => {
const { t } = useTranslation();
const { colorsTokens } = useCunninghamTheme();
const {
query: { id },
} = useRouter();
// There is at least 1 owner in the team
const hasMembers = doc.accesses.length > 1;
const isActive = doc.id === id;
const commonProps = {
className: 'p-t',
width: 52,
style: {
borderRadius: '10px',
flexShrink: 0,
background: '#fff',
},
};
const activeStyle = `
border-right: 4px solid ${colorsTokens()['primary-600']};
background: ${colorsTokens()['primary-400']};
span{
color: ${colorsTokens()['primary-text']};
}
`;
const hoverStyle = `
&:hover{
border-right: 4px solid ${colorsTokens()['primary-400']};
background: ${colorsTokens()['primary-300']};
span{
color: ${colorsTokens()['primary-text']};
}
}
`;
return (
<Box
$margin="none"
as="li"
$css={`
transition: all 0.2s ease-in;
border-right: 4px solid transparent;
${isActive ? activeStyle : hoverStyle}
`}
>
<StyledLink className="p-s pt-t pb-t" href={`/docs/${doc.id}`}>
<Box $align="center" $direction="row" $gap="0.5rem">
{hasMembers ? (
<IconGroup
aria-label={t(`Docs icon`)}
color={colorsTokens()['primary-500']}
{...commonProps}
style={{
...commonProps.style,
border: `1px solid ${colorsTokens()['primary-300']}`,
}}
/>
) : (
<IconNone
aria-label={t(`Empty docs icon`)}
color={colorsTokens()['greyscale-500']}
{...commonProps}
style={{
...commonProps.style,
border: `1px solid ${colorsTokens()['greyscale-300']}`,
}}
/>
)}
<Text
$weight="bold"
$color={!hasMembers ? colorsTokens()['greyscale-600'] : undefined}
$css={`
min-width: 14rem;
`}
>
{doc.title}
</Text>
</Box>
</StyledLink>
</Box>
);
};

View File

@@ -1,112 +0,0 @@
import { Loader } from '@openfun/cunningham-react';
import React, { useMemo, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { APIError } from '@/api';
import { Box, Text, TextErrors } from '@/components';
import { InfiniteScroll } from '@/components/InfiniteScroll';
import { Doc, useDocs } from '@/features/docs/doc-management';
import { useDocPanelStore } from '../store';
import { DocItem } from './DocItem';
interface PanelTeamsStateProps {
isLoading: boolean;
error: APIError<unknown> | null;
docs?: Doc[];
}
const DocListState = ({ isLoading, error, docs }: PanelTeamsStateProps) => {
const { t } = useTranslation();
if (isLoading) {
return (
<Box $align="center" $margin="large">
<Loader />
</Box>
);
}
if (!docs?.length && !error) {
return (
<Box $justify="center" $margin="small">
<Text
as="p"
$margin={{ vertical: 'none' }}
$theme="greyscale"
$variation="500"
>
{t('0 group to display.')}
</Text>
<Text as="p" $theme="greyscale" $variation="500">
{t(
'Create your first document by clicking on the "Create a new document" button.',
)}
</Text>
</Box>
);
}
return (
<>
{docs?.map((doc) => <DocItem doc={doc} key={doc.id} />)}
{error && (
<Box
$justify="center"
$margin={{ vertical: 'big', horizontal: 'auto' }}
>
<TextErrors
causes={error.cause}
icon={
error.status === 502 ? (
<Text className="material-icons" $theme="danger">
wifi_off
</Text>
) : undefined
}
/>
</Box>
)}
</>
);
};
export const DocList = () => {
const ordering = useDocPanelStore((state) => state.ordering);
const {
data,
error,
isLoading,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
} = useDocs({
ordering,
});
const containerRef = useRef<HTMLDivElement>(null);
const docs = useMemo(() => {
return data?.pages.reduce((acc, page) => {
return acc.concat(page.results);
}, [] as Doc[]);
}, [data?.pages]);
return (
<Box $css="overflow-y: auto; overflow-x: hidden;" ref={containerRef}>
<InfiniteScroll
hasMore={hasNextPage}
isLoading={isFetchingNextPage}
next={() => {
void fetchNextPage();
}}
scrollContainer={containerRef.current}
as="ul"
$padding="none"
$margin={{ top: 'none' }}
role="listbox"
>
<DocListState isLoading={isLoading} error={error} docs={docs} />
</InfiniteScroll>
</Box>
);
};

View File

@@ -1,87 +0,0 @@
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Box, BoxButton, Text } from '@/components';
import { useCunninghamTheme } from '@/cunningham';
import { DocList } from './DocList';
import { PanelActions } from './PanelActions';
export const Panel = () => {
const { t } = useTranslation();
const { colorsTokens } = useCunninghamTheme();
const [isOpen, setIsOpen] = useState(true);
const closedOverridingStyles = !isOpen && {
$width: '0',
$maxWidth: '0',
$minWidth: '0',
};
const transition = 'all 0.5s ease-in-out';
return (
<Box
$width="100%"
$maxWidth="20rem"
$minWidth="14rem"
$css={`
position: relative;
border-right: 1px solid ${colorsTokens()['primary-300']};
transition: ${transition};
`}
$height="inherit"
aria-label="Documents panel"
{...closedOverridingStyles}
>
<BoxButton
className="material-icons"
aria-label={
isOpen
? t('Close the documents panel')
: t('Open the documents panel')
}
$background="white"
$color={colorsTokens()['primary-600']}
$radius="100%"
$padding="0.3rem"
$position="absolute"
onClick={() => setIsOpen(!isOpen)}
$css={`
right: ${isOpen ? '-1.3' : '-2.8'}rem;
top: ${isOpen ? '0.7' : '0.25'}rem;
transform: rotate(${isOpen ? '0' : '180'}deg);
transition: ${transition};
font-size: 1.8rem;
border: 1px solid #fafafa;
box-shadow: ${isOpen ? '1px 1px' : '-1px -1px'} 3px #dfdfdf;
z-index: 1;
`}
>
menu_open
</BoxButton>
<Box
$css={`
overflow: hidden;
opacity: ${isOpen ? '1' : '0'};
transition: ${transition};
`}
>
<Box
$padding={{ all: 'small', right: 'large' }}
$direction="row"
$align="center"
$justify="space-between"
$css={`border-bottom: 1px solid ${colorsTokens()['primary-300']};`}
>
<Text $weight="bold" $size="1.25rem">
{t('Documents')}
</Text>
<PanelActions />
</Box>
<DocList />
</Box>
</Box>
);
};

View File

@@ -1,60 +0,0 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { Box, BoxButton, StyledLink } from '@/components';
import { useCunninghamTheme } from '@/cunningham';
import { DocsOrdering } from '@/features/docs/doc-management';
import IconAdd from '../assets/icon-add.svg';
import IconSort from '../assets/icon-sort.svg';
import { useDocPanelStore } from '../store';
export const PanelActions = () => {
const { t } = useTranslation();
const { changeOrdering, ordering } = useDocPanelStore();
const { colorsTokens } = useCunninghamTheme();
const isSortAsc = ordering === DocsOrdering.BY_CREATED_ON;
return (
<Box
$direction="row"
$gap="1rem"
$css={`
& button {
padding: 0;
svg {
padding: 0.1rem;
}
}
`}
>
<BoxButton
aria-label={
isSortAsc
? t('Sort the documents by creation date descendent')
: t('Sort the documents by creation date ascendent')
}
onClick={changeOrdering}
$radius="100%"
$background={isSortAsc ? colorsTokens()['primary-200'] : 'transparent'}
$color={colorsTokens()['primary-600']}
>
<IconSort
width={30}
height={30}
aria-label={t('Sort documents icon')}
/>
</BoxButton>
<StyledLink href="/docs/create">
<BoxButton
aria-label={t('Add a document')}
$color={colorsTokens()['primary-600']}
>
<IconAdd width={30} height={30} aria-label={t('Add document icon')} />
</BoxButton>
</StyledLink>
</Box>
);
};

View File

@@ -1 +0,0 @@
export * from './Panel';

View File

@@ -1 +0,0 @@
export * from './components';

View File

@@ -1 +0,0 @@
export * from './useDocPanelStore';

View File

@@ -1,19 +0,0 @@
import { create } from 'zustand';
import { DocsOrdering } from '@/features/docs/doc-management/api';
interface DocPanelStore {
ordering: DocsOrdering;
changeOrdering: () => void;
}
export const useDocPanelStore = create<DocPanelStore>((set) => ({
ordering: DocsOrdering.BY_CREATED_ON_DESC,
changeOrdering: () =>
set(({ ordering }) => ({
ordering:
ordering === DocsOrdering.BY_CREATED_ON
? DocsOrdering.BY_CREATED_ON_DESC
: DocsOrdering.BY_CREATED_ON,
})),
}));

View File

@@ -1,3 +1,2 @@
export * from './doc-editor';
export * from './doc-management';
export * from './docs-panel';

View File

@@ -1,26 +0,0 @@
import { PropsWithChildren } from 'react';
import { Box } from '@/components';
import { useCunninghamTheme } from '@/cunningham';
import { Panel } from '@/features/docs/docs-panel';
import { MainLayout } from './MainLayout';
export function DocLayout({ children }: PropsWithChildren) {
const { colorsTokens } = useCunninghamTheme();
return (
<MainLayout>
<Box $height="inherit" $direction="row">
<Panel />
<Box
$background={colorsTokens()['primary-bg']}
$width="100%"
$height="inherit"
>
{children}
</Box>
</Box>
</MainLayout>
);
}

View File

@@ -1,3 +1,2 @@
export * from './MainLayout';
export * from './DocLayout';
export * from './PageLayout';

View File

@@ -6,7 +6,7 @@ import { ReactElement } from 'react';
import { Box, Text, TextErrors } from '@/components/';
import { DocEditor } from '@/features/docs/doc-editor';
import { useDoc } from '@/features/docs/doc-management';
import { DocLayout } from '@/layouts';
import { MainLayout } from '@/layouts';
import { NextPageWithLayout } from '@/types/next';
const Page: NextPageWithLayout = () => {
@@ -63,7 +63,7 @@ const Doc = ({ id }: DocProps) => {
};
Page.getLayout = function getLayout(page: ReactElement) {
return <DocLayout>{page}</DocLayout>;
return <MainLayout>{page}</MainLayout>;
};
export default Page;

View File

@@ -2,7 +2,7 @@ import { ReactElement } from 'react';
import { Box } from '@/components';
import { CardCreateDoc } from '@/features/docs/doc-management';
import { DocLayout } from '@/layouts';
import { MainLayout } from '@/layouts';
import { NextPageWithLayout } from '@/types/next';
const Page: NextPageWithLayout = () => {
@@ -14,7 +14,7 @@ const Page: NextPageWithLayout = () => {
};
Page.getLayout = function getLayout(page: ReactElement) {
return <DocLayout>{page}</DocLayout>;
return <MainLayout>{page}</MainLayout>;
};
export default Page;

View File

@@ -1,16 +1,3 @@
import type { ReactElement } from 'react';
import { DocLayout } from '@/layouts';
import { NextPageWithLayout } from '@/types/next';
import Docs from './docs';
const Page: NextPageWithLayout = () => {
return <Docs />;
};
Page.getLayout = function getLayout(page: ReactElement) {
return <DocLayout>{page}</DocLayout>;
};
export default Page;
export default Docs;