📱(frontend) docs mobile friendly
We adapt the docs component to be mobile friendly.
This commit is contained in:
@@ -113,13 +113,14 @@ export const goToGridDoc = async (
|
||||
const header = page.locator('header').first();
|
||||
await header.locator('h2').getByText('Docs').click();
|
||||
|
||||
const datagrid = page
|
||||
.getByLabel('Datagrid of the documents page 1')
|
||||
.getByRole('table');
|
||||
const datagrid = page.getByLabel('Datagrid of the documents page 1');
|
||||
const datagridTable = datagrid.getByRole('table');
|
||||
|
||||
await expect(datagrid.getByLabel('Loading data')).toBeHidden();
|
||||
await expect(datagrid.getByLabel('Loading data')).toBeHidden({
|
||||
timeout: 10000,
|
||||
});
|
||||
|
||||
const rows = datagrid.getByRole('row');
|
||||
const rows = datagridTable.getByRole('row');
|
||||
const row = title
|
||||
? rows.filter({
|
||||
hasText: title,
|
||||
@@ -132,7 +133,7 @@ export const goToGridDoc = async (
|
||||
|
||||
expect(docTitle).toBeDefined();
|
||||
|
||||
await docTitleCell.click();
|
||||
await row.getByRole('link').first().click();
|
||||
|
||||
return docTitle as string;
|
||||
};
|
||||
|
||||
@@ -18,12 +18,13 @@ test.describe('Doc Create', () => {
|
||||
const header = page.locator('header').first();
|
||||
await header.locator('h2').getByText('Docs').click();
|
||||
|
||||
const datagrid = page
|
||||
.getByLabel('Datagrid of the documents page 1')
|
||||
.getByRole('table');
|
||||
const datagrid = page.getByLabel('Datagrid of the documents page 1');
|
||||
const datagridTable = datagrid.getByRole('table');
|
||||
|
||||
await expect(datagrid.getByLabel('Loading data')).toBeHidden();
|
||||
await expect(datagrid.getByText(docTitle)).toBeVisible({
|
||||
await expect(datagrid.getByLabel('Loading data')).toBeHidden({
|
||||
timeout: 10000,
|
||||
});
|
||||
await expect(datagridTable.getByText(docTitle)).toBeVisible({
|
||||
timeout: 5000,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -117,7 +117,9 @@ test.describe('Documents Grid', () => {
|
||||
.getByRole('cell')
|
||||
.nth(cellNumber);
|
||||
|
||||
await expect(datagrid.getByLabel('Loading data')).toBeHidden();
|
||||
await expect(datagrid.getByLabel('Loading data')).toBeHidden({
|
||||
timeout: 10000,
|
||||
});
|
||||
|
||||
// Initial state
|
||||
await expect(docNameRow1).toHaveText(/.*/);
|
||||
@@ -134,7 +136,9 @@ test.describe('Documents Grid', () => {
|
||||
const responseOrderingAsc = await responsePromiseOrderingAsc;
|
||||
expect(responseOrderingAsc.ok()).toBeTruthy();
|
||||
|
||||
await expect(datagrid.getByLabel('Loading data')).toBeHidden();
|
||||
await expect(datagrid.getByLabel('Loading data')).toBeHidden({
|
||||
timeout: 10000,
|
||||
});
|
||||
|
||||
await expect(docNameRow1).toHaveText(/.*/);
|
||||
await expect(docNameRow2).toHaveText(/.*/);
|
||||
@@ -155,7 +159,9 @@ test.describe('Documents Grid', () => {
|
||||
const responseOrderingDesc = await responsePromiseOrderingDesc;
|
||||
expect(responseOrderingDesc.ok()).toBeTruthy();
|
||||
|
||||
await expect(datagrid.getByLabel('Loading data')).toBeHidden();
|
||||
await expect(datagrid.getByLabel('Loading data')).toBeHidden({
|
||||
timeout: 10000,
|
||||
});
|
||||
|
||||
await expect(docNameRow1).toHaveText(/.*/);
|
||||
await expect(docNameRow2).toHaveText(/.*/);
|
||||
@@ -244,3 +250,87 @@ test.describe('Documents Grid', () => {
|
||||
await expect(datagrid.getByText(docName!)).toBeHidden();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Documents Grid mobile', () => {
|
||||
test.use({ viewport: { width: 500, height: 1200 } });
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/');
|
||||
});
|
||||
|
||||
test('it checks the grid when mobile', async ({ page }) => {
|
||||
await page.route('**/documents/**', async (route) => {
|
||||
const request = route.request();
|
||||
if (request.method().includes('GET') && request.url().includes('page=')) {
|
||||
await route.fulfill({
|
||||
json: {
|
||||
count: 1,
|
||||
next: null,
|
||||
previous: null,
|
||||
results: [
|
||||
{
|
||||
id: 'b7fd9d9b-0642-4b4f-8617-ce50f69519ed',
|
||||
title: 'My mocked document',
|
||||
accesses: [
|
||||
{
|
||||
id: '8c1e047a-24e7-4a80-942b-8e9c7ab43e1f',
|
||||
user: {
|
||||
id: '7380f42f-02eb-4ad5-b8f0-037a0e66066d',
|
||||
email: 'test@test.test',
|
||||
full_name: 'John Doe',
|
||||
short_name: 'John',
|
||||
},
|
||||
team: '',
|
||||
role: 'owner',
|
||||
abilities: {
|
||||
destroy: false,
|
||||
update: false,
|
||||
partial_update: false,
|
||||
retrieve: true,
|
||||
set_role_to: [],
|
||||
},
|
||||
},
|
||||
],
|
||||
abilities: {
|
||||
attachment_upload: true,
|
||||
destroy: true,
|
||||
link_configuration: true,
|
||||
manage_accesses: true,
|
||||
partial_update: true,
|
||||
retrieve: true,
|
||||
update: true,
|
||||
versions_destroy: true,
|
||||
versions_list: true,
|
||||
versions_retrieve: true,
|
||||
},
|
||||
link_role: 'reader',
|
||||
link_reach: 'public',
|
||||
created_at: '2024-10-07T13:02:41.085298Z',
|
||||
updated_at: '2024-10-07T13:30:21.829690Z',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await route.continue();
|
||||
}
|
||||
});
|
||||
|
||||
await page.goto('/');
|
||||
|
||||
const datagrid = page.getByLabel('Datagrid of the documents page 1');
|
||||
const tableDatagrid = datagrid.getByRole('table');
|
||||
|
||||
await expect(datagrid.getByLabel('Loading data')).toBeHidden({
|
||||
timeout: 10000,
|
||||
});
|
||||
|
||||
const rows = tableDatagrid.getByRole('row');
|
||||
const row = rows.filter({
|
||||
hasText: 'My mocked document',
|
||||
});
|
||||
|
||||
await expect(row.getByRole('cell').nth(0)).toHaveText('My mocked document');
|
||||
await expect(row.getByRole('cell').nth(1)).toHaveText('Public');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -385,3 +385,35 @@ test.describe('Doc Header', () => {
|
||||
).toBeHidden();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Documents Header mobile', () => {
|
||||
test.use({ viewport: { width: 500, height: 1200 } });
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/');
|
||||
});
|
||||
|
||||
test('it checks the close button on Share modal', async ({ page }) => {
|
||||
await mockedDocument(page, {
|
||||
abilities: {
|
||||
destroy: true, // Means owner
|
||||
link_configuration: true,
|
||||
versions_destroy: true,
|
||||
versions_list: true,
|
||||
versions_retrieve: true,
|
||||
manage_accesses: true,
|
||||
update: true,
|
||||
partial_update: true,
|
||||
retrieve: true,
|
||||
},
|
||||
});
|
||||
|
||||
await goToGridDoc(page);
|
||||
|
||||
await page.getByRole('button', { name: 'Share' }).click();
|
||||
|
||||
await expect(page.getByLabel('Share modal')).toBeVisible();
|
||||
await page.getByRole('button', { name: 'close' }).click();
|
||||
await expect(page.getByLabel('Share modal')).toBeHidden();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,6 +8,8 @@ test.beforeEach(async ({ page }) => {
|
||||
|
||||
test.describe('Doc Table Content', () => {
|
||||
test('it checks the doc table content', async ({ page, browserName }) => {
|
||||
test.setTimeout(60000);
|
||||
|
||||
const [randomDoc] = await createDoc(
|
||||
page,
|
||||
'doc-table-content',
|
||||
@@ -37,7 +39,7 @@ test.describe('Doc Table Content', () => {
|
||||
await page.locator('.bn-block-outer').last().click();
|
||||
|
||||
// Create space to fill the viewport
|
||||
for (let i = 0; i < 5; i++) {
|
||||
for (let i = 0; i < 10; i++) {
|
||||
await page.keyboard.press('Enter');
|
||||
}
|
||||
|
||||
@@ -48,7 +50,7 @@ test.describe('Doc Table Content', () => {
|
||||
await page.locator('.bn-block-outer').last().click();
|
||||
|
||||
// Create space to fill the viewport
|
||||
for (let i = 0; i < 5; i++) {
|
||||
for (let i = 0; i < 10; i++) {
|
||||
await page.keyboard.press('Enter');
|
||||
}
|
||||
|
||||
@@ -61,11 +63,11 @@ test.describe('Doc Table Content', () => {
|
||||
const another = panel.getByText('Another World');
|
||||
|
||||
await expect(hello).toBeVisible();
|
||||
await expect(hello).toHaveCSS('font-size', /19/);
|
||||
await expect(hello).toHaveCSS('font-size', /17/);
|
||||
await expect(hello).toHaveAttribute('aria-selected', 'true');
|
||||
|
||||
await expect(superW).toBeVisible();
|
||||
await expect(superW).toHaveCSS('font-size', /16/);
|
||||
await expect(superW).toHaveCSS('font-size', /14/);
|
||||
await expect(superW).toHaveAttribute('aria-selected', 'false');
|
||||
|
||||
await expect(another).toBeVisible();
|
||||
|
||||
@@ -23,7 +23,9 @@ test.describe('Doc Visibility', () => {
|
||||
.getByLabel('Datagrid of the documents page 1')
|
||||
.getByRole('table');
|
||||
|
||||
await expect(datagrid.getByLabel('Loading data')).toBeHidden();
|
||||
await expect(datagrid.getByLabel('Loading data')).toBeHidden({
|
||||
timeout: 10000,
|
||||
});
|
||||
|
||||
await expect(datagrid.getByText(docTitle)).toBeVisible();
|
||||
|
||||
|
||||
@@ -507,6 +507,23 @@ input:-webkit-autofill:focus {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
@media screen and (width <= 420px) {
|
||||
.c__modal__scroller {
|
||||
padding: 0.7rem;
|
||||
}
|
||||
|
||||
.c__modal__title h2 {
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width <= 576px) {
|
||||
.c__modal__footer--sided {
|
||||
gap: 0.5rem;
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toast
|
||||
*/
|
||||
|
||||
@@ -18,10 +18,14 @@ import { randomColor } from '../utils';
|
||||
|
||||
import { BlockNoteToolbar } from './BlockNoteToolbar';
|
||||
|
||||
const cssEditor = `
|
||||
const cssEditor = (readonly: boolean) => `
|
||||
&, & > .bn-container, & .ProseMirror {
|
||||
height:100%
|
||||
};
|
||||
& .bn-editor {
|
||||
padding-right: 30px;
|
||||
${readonly && `padding-left: 30px;`}
|
||||
};
|
||||
& .collaboration-cursor__caret.ProseMirror-widget{
|
||||
word-wrap: initial;
|
||||
}
|
||||
@@ -30,6 +34,35 @@ const cssEditor = `
|
||||
padding: 2px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
@media screen and (width <= 560px) {
|
||||
& .bn-editor {
|
||||
padding-left: 40px;
|
||||
padding-right: 10px;
|
||||
${readonly && `padding-left: 10px;`}
|
||||
};
|
||||
.bn-side-menu[data-block-type=heading][data-level="1"] {
|
||||
height: 46px;
|
||||
}
|
||||
.bn-side-menu[data-block-type=heading][data-level="2"] {
|
||||
height: 40px;
|
||||
}
|
||||
.bn-side-menu[data-block-type=heading][data-level="3"] {
|
||||
height: 40px;
|
||||
}
|
||||
& .bn-editor h1 {
|
||||
font-size: 1.6rem;
|
||||
}
|
||||
& .bn-editor h2 {
|
||||
font-size: 1.35rem;
|
||||
}
|
||||
& .bn-editor h3 {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
.bn-block-content[data-is-empty-and-focused][data-content-type="paragraph"]
|
||||
.bn-inline-content:has(> .ProseMirror-trailingBreak:only-child)::before {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
interface BlockNoteEditorProps {
|
||||
@@ -70,8 +103,9 @@ export const BlockNoteContent = ({
|
||||
const isVersion = doc.id !== storeId;
|
||||
const { userData } = useAuthStore();
|
||||
const { setStore, docsStore } = useDocStore();
|
||||
const canSave = doc.abilities.partial_update && !isVersion;
|
||||
useSaveDoc(doc.id, provider.document, canSave);
|
||||
|
||||
const readOnly = !doc.abilities.partial_update || isVersion;
|
||||
useSaveDoc(doc.id, provider.document, !readOnly);
|
||||
const storedEditor = docsStore?.[storeId]?.editor;
|
||||
const {
|
||||
mutateAsync: createDocAttachment,
|
||||
@@ -130,7 +164,7 @@ export const BlockNoteContent = ({
|
||||
}, [editor, resetHeadings, setHeadings]);
|
||||
|
||||
return (
|
||||
<Box $css={cssEditor}>
|
||||
<Box $css={cssEditor(readOnly)}>
|
||||
{isErrorAttachment && (
|
||||
<Box $margin={{ bottom: 'big' }}>
|
||||
<TextErrors
|
||||
@@ -144,7 +178,7 @@ export const BlockNoteContent = ({
|
||||
<BlockNoteView
|
||||
editor={editor}
|
||||
formattingToolbar={false}
|
||||
editable={doc.abilities.partial_update && !isVersion}
|
||||
editable={!readOnly}
|
||||
theme="light"
|
||||
>
|
||||
<BlockNoteToolbar />
|
||||
|
||||
@@ -9,6 +9,7 @@ import { useCunninghamTheme } from '@/cunningham';
|
||||
import { DocHeader } from '@/features/docs/doc-header';
|
||||
import { Doc } from '@/features/docs/doc-management';
|
||||
import { Versions, useDocVersion } from '@/features/docs/doc-versioning/';
|
||||
import { useResponsiveStore } from '@/stores';
|
||||
|
||||
import { useHeadingStore } from '../stores';
|
||||
|
||||
@@ -25,6 +26,7 @@ export const DocEditor = ({ doc }: DocEditorProps) => {
|
||||
} = useRouter();
|
||||
const { t } = useTranslation();
|
||||
const { headings } = useHeadingStore();
|
||||
const { isMobile } = useResponsiveStore();
|
||||
|
||||
const isVersion = versionId && typeof versionId === 'string';
|
||||
|
||||
@@ -51,11 +53,12 @@ export const DocEditor = ({ doc }: DocEditorProps) => {
|
||||
$background={colorsTokens()['primary-bg']}
|
||||
$height="100%"
|
||||
$direction="row"
|
||||
$margin={{ all: 'small', top: 'none' }}
|
||||
$margin={{ all: isMobile ? 'tiny' : 'small', top: 'none' }}
|
||||
$css="overflow-x: clip;"
|
||||
$position="relative"
|
||||
>
|
||||
<Card
|
||||
$padding="big"
|
||||
$padding={isMobile ? 'small' : 'big'}
|
||||
$css="flex:1;"
|
||||
$overflow="auto"
|
||||
$position="relative"
|
||||
@@ -65,7 +68,7 @@ export const DocEditor = ({ doc }: DocEditorProps) => {
|
||||
) : (
|
||||
<BlockNoteEditor doc={doc} />
|
||||
)}
|
||||
<IconOpenPanelEditor headings={headings} />
|
||||
{!isMobile && <IconOpenPanelEditor headings={headings} />}
|
||||
</Card>
|
||||
<PanelEditor doc={doc} headings={headings} />
|
||||
</Box>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useCunninghamTheme } from '@/cunningham';
|
||||
import { Doc } from '@/features/docs/doc-management';
|
||||
import { TableContent } from '@/features/docs/doc-table-content';
|
||||
import { VersionList } from '@/features/docs/doc-versioning';
|
||||
import { useResponsiveStore } from '@/stores';
|
||||
|
||||
import { usePanelEditorStore } from '../stores';
|
||||
import { HeadingBlock } from '../types';
|
||||
@@ -21,7 +22,7 @@ export const PanelEditor = ({
|
||||
}: PropsWithChildren<PanelProps>) => {
|
||||
const { t } = useTranslation();
|
||||
const { colorsTokens } = useCunninghamTheme();
|
||||
|
||||
const { isMobile } = useResponsiveStore();
|
||||
const { isPanelTableContentOpen, setIsPanelTableContentOpen, isPanelOpen } =
|
||||
usePanelEditorStore();
|
||||
|
||||
@@ -29,12 +30,12 @@ export const PanelEditor = ({
|
||||
<Card
|
||||
$width="100%"
|
||||
$maxWidth="20rem"
|
||||
$position="sticky"
|
||||
$maxHeight="99vh"
|
||||
$position={isMobile ? 'absolute' : 'sticky'}
|
||||
$height="100%"
|
||||
$hasTransition="slow"
|
||||
$css={`
|
||||
top: 0vh;
|
||||
right: 0;
|
||||
transform: translateX(0%);
|
||||
flex: 1;
|
||||
margin-left: 1rem;
|
||||
@@ -60,8 +61,9 @@ export const PanelEditor = ({
|
||||
top: 0;
|
||||
opacity: ${isPanelOpen ? '1' : '0'};
|
||||
`}
|
||||
$maxHeight="100%"
|
||||
$maxHeight="99vh"
|
||||
>
|
||||
{isMobile && <IconOpenPanelEditor headings={headings} />}
|
||||
<Box
|
||||
$direction="row"
|
||||
$justify="space-between"
|
||||
@@ -69,6 +71,7 @@ export const PanelEditor = ({
|
||||
$position="relative"
|
||||
$background={colorsTokens()['primary-400']}
|
||||
$margin={{ bottom: 'tiny' }}
|
||||
$radius="4px 4px 0 0"
|
||||
>
|
||||
<Box
|
||||
$background="white"
|
||||
@@ -78,7 +81,15 @@ export const PanelEditor = ({
|
||||
$hasTransition="slow"
|
||||
$css={`
|
||||
border-top: 2px solid ${colorsTokens()['primary-600']};
|
||||
${isPanelTableContentOpen ? 'transform: translateX(0);' : 'transform: translateX(100%);'}
|
||||
border-radius: 0 4px 0 0;
|
||||
${
|
||||
isPanelTableContentOpen
|
||||
? `
|
||||
transform: translateX(0);
|
||||
border-radius: 4px 0 0 0;
|
||||
`
|
||||
: `transform: translateX(100%);`
|
||||
}
|
||||
`}
|
||||
/>
|
||||
<BoxButton
|
||||
@@ -134,6 +145,7 @@ export const IconOpenPanelEditor = ({ headings }: IconOpenPanelEditorProps) => {
|
||||
const { setIsPanelOpen, isPanelOpen, setIsPanelTableContentOpen } =
|
||||
usePanelEditorStore();
|
||||
const [hasBeenOpen, setHasBeenOpen] = useState(isPanelOpen);
|
||||
const { isMobile } = useResponsiveStore();
|
||||
|
||||
const setClosePanel = () => {
|
||||
setHasBeenOpen(true);
|
||||
@@ -142,12 +154,18 @@ export const IconOpenPanelEditor = ({ headings }: IconOpenPanelEditorProps) => {
|
||||
|
||||
// Open the panel if there are more than 1 heading
|
||||
useEffect(() => {
|
||||
if (headings?.length && headings.length > 1 && !hasBeenOpen) {
|
||||
if (headings?.length && headings.length > 1 && !hasBeenOpen && !isMobile) {
|
||||
setIsPanelTableContentOpen(true);
|
||||
setIsPanelOpen(true);
|
||||
setHasBeenOpen(true);
|
||||
}
|
||||
}, [headings, setIsPanelTableContentOpen, setIsPanelOpen, hasBeenOpen]);
|
||||
}, [
|
||||
headings,
|
||||
setIsPanelTableContentOpen,
|
||||
setIsPanelOpen,
|
||||
hasBeenOpen,
|
||||
isMobile,
|
||||
]);
|
||||
|
||||
// If open from the doc header we set the state as well
|
||||
useEffect(() => {
|
||||
@@ -169,7 +187,7 @@ export const IconOpenPanelEditor = ({ headings }: IconOpenPanelEditorProps) => {
|
||||
aria-label={isPanelOpen ? t('Close the panel') : t('Open the panel')}
|
||||
$background="transparent"
|
||||
$size="h2"
|
||||
$zIndex={1}
|
||||
$zIndex={10}
|
||||
$hasTransition="slow"
|
||||
$css={`
|
||||
cursor: pointer;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Button } from '@openfun/cunningham-react';
|
||||
import React, { Fragment, useState } from 'react';
|
||||
import React, { Fragment } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Box, Card, StyledLink, Text } from '@/components';
|
||||
@@ -10,8 +9,9 @@ import {
|
||||
currentDocRole,
|
||||
useTrans,
|
||||
} from '@/features/docs/doc-management';
|
||||
import { ModalVersion, Versions } from '@/features/docs/doc-versioning';
|
||||
import { Versions } from '@/features/docs/doc-versioning';
|
||||
import { useDate } from '@/hook';
|
||||
import { useResponsiveStore } from '@/stores';
|
||||
|
||||
import { DocTagPublic } from './DocTagPublic';
|
||||
import { DocTitle } from './DocTitle';
|
||||
@@ -27,15 +27,19 @@ export const DocHeader = ({ doc, versionId }: DocHeaderProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { formatDate } = useDate();
|
||||
const { transRole } = useTrans();
|
||||
const [isModalVersionOpen, setIsModalVersionOpen] = useState(false);
|
||||
const { isMobile, isSmallMobile } = useResponsiveStore();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card
|
||||
$margin="small"
|
||||
$margin={isMobile ? 'tiny' : 'small'}
|
||||
aria-label={t('It is the card information about the document.')}
|
||||
>
|
||||
<Box $padding="small" $direction="row" $align="center">
|
||||
<Box
|
||||
$padding={isMobile ? 'tiny' : 'small'}
|
||||
$direction="row"
|
||||
$align="center"
|
||||
>
|
||||
<StyledLink href="/">
|
||||
<Text
|
||||
$isMaterialIcon
|
||||
@@ -53,33 +57,39 @@ export const DocHeader = ({ doc, versionId }: DocHeaderProps) => {
|
||||
$width="1px"
|
||||
$height="70%"
|
||||
$background={colorsTokens()['greyscale-100']}
|
||||
$margin={{ horizontal: 'small' }}
|
||||
$margin={{ horizontal: 'tiny' }}
|
||||
/>
|
||||
<Box $gap="1rem" $direction="row" $align="center">
|
||||
<Box
|
||||
$direction="row"
|
||||
$justify="space-between"
|
||||
$css="flex:1;"
|
||||
$gap="0.5rem 1rem"
|
||||
$wrap="wrap"
|
||||
$align="center"
|
||||
>
|
||||
<DocTitle doc={doc} />
|
||||
{versionId && (
|
||||
<Button
|
||||
onClick={() => {
|
||||
setIsModalVersionOpen(true);
|
||||
}}
|
||||
size="small"
|
||||
>
|
||||
{t('Restore this version')}
|
||||
</Button>
|
||||
)}
|
||||
<DocToolBox doc={doc} versionId={versionId} />
|
||||
</Box>
|
||||
<DocToolBox doc={doc} />
|
||||
</Box>
|
||||
<Box
|
||||
$direction="row"
|
||||
$align="center"
|
||||
$direction={isSmallMobile ? 'column' : 'row'}
|
||||
$align={isSmallMobile ? 'start' : 'center'}
|
||||
$css="border-top:1px solid #eee"
|
||||
$padding={{ horizontal: 'big', vertical: 'tiny' }}
|
||||
$padding={{
|
||||
horizontal: isMobile ? 'tiny' : 'big',
|
||||
vertical: 'tiny',
|
||||
}}
|
||||
$gap="0.5rem 2rem"
|
||||
$justify="space-between"
|
||||
$wrap="wrap"
|
||||
$position="relative"
|
||||
>
|
||||
<Box $direction="row" $align="center" $gap="0.5rem 2rem" $wrap="wrap">
|
||||
<Box
|
||||
$direction={isSmallMobile ? 'column' : 'row'}
|
||||
$align={isSmallMobile ? 'start' : 'center'}
|
||||
$gap="0.5rem 2rem"
|
||||
$wrap="wrap"
|
||||
>
|
||||
<DocTagPublic doc={doc} />
|
||||
<Text $size="s" $display="inline">
|
||||
{t('Created at')} <strong>{formatDate(doc.created_at)}</strong>
|
||||
@@ -106,13 +116,6 @@ export const DocHeader = ({ doc, versionId }: DocHeaderProps) => {
|
||||
</Text>
|
||||
</Box>
|
||||
</Card>
|
||||
{isModalVersionOpen && versionId && (
|
||||
<ModalVersion
|
||||
onClose={() => setIsModalVersionOpen(false)}
|
||||
docId={doc.id}
|
||||
versionId={versionId}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import { Text } from '@/components';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
import { Doc, LinkReach } from '@/features/docs/doc-management';
|
||||
import { useResponsiveStore } from '@/stores';
|
||||
|
||||
interface DocTagPublicProps {
|
||||
doc: Doc;
|
||||
@@ -11,6 +12,7 @@ interface DocTagPublicProps {
|
||||
export const DocTagPublic = ({ doc }: DocTagPublicProps) => {
|
||||
const { colorsTokens } = useCunninghamTheme();
|
||||
const { t } = useTranslation();
|
||||
const { isSmallMobile } = useResponsiveStore();
|
||||
|
||||
if (doc?.link_reach !== LinkReach.PUBLIC) {
|
||||
return null;
|
||||
@@ -24,6 +26,8 @@ export const DocTagPublic = ({ doc }: DocTagPublicProps) => {
|
||||
$padding="xtiny"
|
||||
$radius="3px"
|
||||
$size="s"
|
||||
$position={isSmallMobile ? 'absolute' : 'initial'}
|
||||
$css={isSmallMobile ? 'right: 10px;' : ''}
|
||||
>
|
||||
{t('Public')}
|
||||
</Text>
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
useTrans,
|
||||
useUpdateDoc,
|
||||
} from '@/features/docs/doc-management';
|
||||
import { useResponsiveStore } from '@/stores';
|
||||
import { isFirefox } from '@/utils/userAgent';
|
||||
|
||||
const DocTitleStyle = createGlobalStyle`
|
||||
@@ -32,9 +33,15 @@ interface DocTitleProps {
|
||||
}
|
||||
|
||||
export const DocTitle = ({ doc }: DocTitleProps) => {
|
||||
const { isMobile } = useResponsiveStore();
|
||||
|
||||
if (!doc.abilities.partial_update) {
|
||||
return (
|
||||
<Text as="h2" $align="center" $margin={{ all: 'none', left: 'tiny' }}>
|
||||
<Text
|
||||
as="h2"
|
||||
$margin={{ all: 'none', left: 'tiny' }}
|
||||
$size={isMobile ? 'h4' : 'h2'}
|
||||
>
|
||||
{doc.title}
|
||||
</Text>
|
||||
);
|
||||
@@ -53,6 +60,7 @@ const DocTitleInput = ({ doc }: DocTitleProps) => {
|
||||
const { headings } = useHeadingStore();
|
||||
const headingText = headings?.[0]?.contentText;
|
||||
const debounceRef = useRef<NodeJS.Timeout>();
|
||||
const { isMobile } = useResponsiveStore();
|
||||
|
||||
const { mutate: updateDoc } = useUpdateDoc({
|
||||
listInvalideQueries: [KEY_DOC, KEY_LIST_DOC],
|
||||
@@ -124,7 +132,6 @@ const DocTitleInput = ({ doc }: DocTitleProps) => {
|
||||
as="h2"
|
||||
$radius="4px"
|
||||
$padding={{ horizontal: 'tiny', vertical: '4px' }}
|
||||
$align="center"
|
||||
$margin="none"
|
||||
contentEditable={isFirefox() ? 'true' : 'plaintext-only'}
|
||||
onClick={handleOnClick}
|
||||
@@ -141,7 +148,7 @@ const DocTitleInput = ({ doc }: DocTitleProps) => {
|
||||
$css={`
|
||||
${isUntitled && 'font-style: italic;'}
|
||||
cursor: text;
|
||||
font-size: 1.5rem;
|
||||
font-size: ${isMobile ? '1.2rem' : '1.5rem'};
|
||||
transition: box-shadow 0.5s, border-color 0.5s;
|
||||
border: 1px dashed transparent;
|
||||
|
||||
|
||||
@@ -9,98 +9,121 @@ import {
|
||||
ModalRemoveDoc,
|
||||
ModalShare,
|
||||
} from '@/features/docs/doc-management';
|
||||
import { useResponsiveStore } from '@/stores';
|
||||
|
||||
import { ModalVersion, Versions } from '../../doc-versioning';
|
||||
|
||||
import { ModalPDF } from './ModalExport';
|
||||
|
||||
interface DocToolBoxProps {
|
||||
doc: Doc;
|
||||
versionId?: Versions['version_id'];
|
||||
}
|
||||
|
||||
export const DocToolBox = ({ doc }: DocToolBoxProps) => {
|
||||
export const DocToolBox = ({ doc, versionId }: DocToolBoxProps) => {
|
||||
const { t } = useTranslation();
|
||||
const [isModalShareOpen, setIsModalShareOpen] = useState(false);
|
||||
const [isModalRemoveOpen, setIsModalRemoveOpen] = useState(false);
|
||||
const [isModalPDFOpen, setIsModalPDFOpen] = useState(false);
|
||||
const [isDropOpen, setIsDropOpen] = useState(false);
|
||||
const { setIsPanelOpen, setIsPanelTableContentOpen } = usePanelEditorStore();
|
||||
const [isModalVersionOpen, setIsModalVersionOpen] = useState(false);
|
||||
const { isSmallMobile } = useResponsiveStore();
|
||||
|
||||
return (
|
||||
<Box
|
||||
$margin={{ left: 'auto' }}
|
||||
$direction="row"
|
||||
$align="center"
|
||||
$gap="1rem"
|
||||
$gap="0.5rem 1.5rem"
|
||||
$wrap={isSmallMobile ? 'wrap' : 'nowrap'}
|
||||
>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setIsModalShareOpen(true);
|
||||
}}
|
||||
>
|
||||
{t('Share')}
|
||||
</Button>
|
||||
<DropButton
|
||||
button={
|
||||
<IconOptions
|
||||
isOpen={isDropOpen}
|
||||
aria-label={t('Open the document options')}
|
||||
/>
|
||||
}
|
||||
onOpenChange={(isOpen) => setIsDropOpen(isOpen)}
|
||||
isOpen={isDropOpen}
|
||||
>
|
||||
<Box>
|
||||
{doc.abilities.versions_list && (
|
||||
{versionId && (
|
||||
<Box $margin={{ left: 'auto' }}>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setIsModalVersionOpen(true);
|
||||
}}
|
||||
color="secondary"
|
||||
size={isSmallMobile ? 'small' : 'medium'}
|
||||
>
|
||||
{t('Restore this version')}
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
<Box $direction="row" $margin={{ left: 'auto' }} $gap="1rem">
|
||||
<Button
|
||||
onClick={() => {
|
||||
setIsModalShareOpen(true);
|
||||
}}
|
||||
size={isSmallMobile ? 'small' : 'medium'}
|
||||
>
|
||||
{t('Share')}
|
||||
</Button>
|
||||
<DropButton
|
||||
button={
|
||||
<IconOptions
|
||||
isOpen={isDropOpen}
|
||||
aria-label={t('Open the document options')}
|
||||
/>
|
||||
}
|
||||
onOpenChange={(isOpen) => setIsDropOpen(isOpen)}
|
||||
isOpen={isDropOpen}
|
||||
>
|
||||
<Box>
|
||||
{doc.abilities.versions_list && (
|
||||
<Button
|
||||
onClick={() => {
|
||||
setIsPanelOpen(true);
|
||||
setIsPanelTableContentOpen(false);
|
||||
setIsDropOpen(false);
|
||||
}}
|
||||
color="primary-text"
|
||||
icon={<span className="material-icons">history</span>}
|
||||
size="small"
|
||||
>
|
||||
<Text $theme="primary">{t('Version history')}</Text>
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
onClick={() => {
|
||||
setIsPanelOpen(true);
|
||||
setIsPanelTableContentOpen(false);
|
||||
setIsPanelTableContentOpen(true);
|
||||
setIsDropOpen(false);
|
||||
}}
|
||||
color="primary-text"
|
||||
icon={<span className="material-icons">history</span>}
|
||||
icon={<span className="material-icons">summarize</span>}
|
||||
size="small"
|
||||
>
|
||||
<Text $theme="primary">{t('Version history')}</Text>
|
||||
<Text $theme="primary">{t('Table of contents')}</Text>
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
onClick={() => {
|
||||
setIsPanelOpen(true);
|
||||
setIsPanelTableContentOpen(true);
|
||||
setIsDropOpen(false);
|
||||
}}
|
||||
color="primary-text"
|
||||
icon={<span className="material-icons">summarize</span>}
|
||||
size="small"
|
||||
>
|
||||
<Text $theme="primary">{t('Table of contents')}</Text>
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setIsModalPDFOpen(true);
|
||||
setIsDropOpen(false);
|
||||
}}
|
||||
color="primary-text"
|
||||
icon={<span className="material-icons">file_download</span>}
|
||||
size="small"
|
||||
>
|
||||
<Text $theme="primary">{t('Export')}</Text>
|
||||
</Button>
|
||||
{doc.abilities.destroy && (
|
||||
<Button
|
||||
onClick={() => {
|
||||
setIsModalRemoveOpen(true);
|
||||
setIsModalPDFOpen(true);
|
||||
setIsDropOpen(false);
|
||||
}}
|
||||
color="primary-text"
|
||||
icon={<span className="material-icons">delete</span>}
|
||||
icon={<span className="material-icons">file_download</span>}
|
||||
size="small"
|
||||
>
|
||||
<Text $theme="primary">{t('Delete document')}</Text>
|
||||
<Text $theme="primary">{t('Export')}</Text>
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
</DropButton>
|
||||
{doc.abilities.destroy && (
|
||||
<Button
|
||||
onClick={() => {
|
||||
setIsModalRemoveOpen(true);
|
||||
setIsDropOpen(false);
|
||||
}}
|
||||
color="primary-text"
|
||||
icon={<span className="material-icons">delete</span>}
|
||||
size="small"
|
||||
>
|
||||
<Text $theme="primary">{t('Delete document')}</Text>
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
</DropButton>
|
||||
</Box>
|
||||
{isModalShareOpen && (
|
||||
<ModalShare onClose={() => setIsModalShareOpen(false)} doc={doc} />
|
||||
)}
|
||||
@@ -110,6 +133,13 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
|
||||
{isModalRemoveOpen && (
|
||||
<ModalRemoveDoc onClose={() => setIsModalRemoveOpen(false)} doc={doc} />
|
||||
)}
|
||||
{isModalVersionOpen && versionId && (
|
||||
<ModalVersion
|
||||
onClose={() => setIsModalVersionOpen(false)}
|
||||
docId={doc.id}
|
||||
versionId={versionId}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -43,48 +43,57 @@ export const DocVisibility = ({ doc }: DocVisibilityProps) => {
|
||||
$direction="row"
|
||||
$align="center"
|
||||
$justify="space-between"
|
||||
$gap="1rem"
|
||||
>
|
||||
<Box $direction="row" $gap="1rem" $align="center">
|
||||
<IconBG iconName="public" $margin="none" />
|
||||
<Switch
|
||||
label={t(docPublic ? 'Doc public' : 'Doc private')}
|
||||
defaultChecked={docPublic}
|
||||
onChange={() => {
|
||||
api.mutate({
|
||||
id: doc.id,
|
||||
link_reach: docPublic ? LinkReach.RESTRICTED : LinkReach.PUBLIC,
|
||||
link_role: 'reader',
|
||||
});
|
||||
setDocPublic(!docPublic);
|
||||
}}
|
||||
disabled={!doc.abilities.link_configuration}
|
||||
text={
|
||||
docPublic
|
||||
? t('Anyone on the internet with the link can view')
|
||||
: t('Only for people with access')
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
<Button
|
||||
onClick={() => {
|
||||
navigator.clipboard
|
||||
.writeText(window.location.href)
|
||||
.then(() => {
|
||||
toast(t('Link Copied !'), VariantType.SUCCESS, {
|
||||
duration: 3000,
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
toast(t('Failed to copy link'), VariantType.ERROR, {
|
||||
duration: 3000,
|
||||
});
|
||||
});
|
||||
}}
|
||||
color="primary"
|
||||
icon={<span className="material-icons">copy</span>}
|
||||
<IconBG iconName="public" $margin="none" />
|
||||
<Box
|
||||
$width="100%"
|
||||
$wrap="wrap"
|
||||
$gap="1rem"
|
||||
$justify="space-between"
|
||||
$direction="row"
|
||||
>
|
||||
{t('Copy link')}
|
||||
</Button>
|
||||
<Box $direction="row" $gap="1rem" $align="center">
|
||||
<Switch
|
||||
label={t(docPublic ? 'Doc public' : 'Doc private')}
|
||||
defaultChecked={docPublic}
|
||||
onChange={() => {
|
||||
api.mutate({
|
||||
id: doc.id,
|
||||
link_reach: docPublic ? LinkReach.RESTRICTED : LinkReach.PUBLIC,
|
||||
link_role: 'reader',
|
||||
});
|
||||
setDocPublic(!docPublic);
|
||||
}}
|
||||
disabled={!doc.abilities.link_configuration}
|
||||
text={
|
||||
docPublic
|
||||
? t('Anyone on the internet with the link can view')
|
||||
: t('Only for people with access')
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
<Button
|
||||
onClick={() => {
|
||||
navigator.clipboard
|
||||
.writeText(window.location.href)
|
||||
.then(() => {
|
||||
toast(t('Link Copied !'), VariantType.SUCCESS, {
|
||||
duration: 3000,
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
toast(t('Failed to copy link'), VariantType.ERROR, {
|
||||
duration: 3000,
|
||||
});
|
||||
});
|
||||
}}
|
||||
color="primary"
|
||||
icon={<span className="material-icons">copy</span>}
|
||||
>
|
||||
{t('Copy link')}
|
||||
</Button>
|
||||
</Box>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Box, Card, SideModal, Text } from '@/components';
|
||||
import { InvitationList } from '@/features/docs/members/invitation-list';
|
||||
import { AddMembers } from '@/features/docs/members/members-add';
|
||||
import { MemberList } from '@/features/docs/members/members-list';
|
||||
import { useResponsiveStore } from '@/stores';
|
||||
|
||||
import { Doc } from '../types';
|
||||
import { currentDocRole } from '../utils';
|
||||
@@ -20,6 +21,15 @@ const ModalShareStyle = createGlobalStyle`
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.c__modal__close{
|
||||
margin-right: 1rem;
|
||||
|
||||
button{
|
||||
border-bottom: 1px solid #E0E0E0;
|
||||
border-left: 1px solid #E0E0E0;
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -29,18 +39,21 @@ interface ModalShareProps {
|
||||
}
|
||||
|
||||
export const ModalShare = ({ onClose, doc }: ModalShareProps) => {
|
||||
const { isMobile, isSmallMobile } = useResponsiveStore();
|
||||
const width = isSmallMobile ? '100vw' : isMobile ? '90vw' : '70vw';
|
||||
|
||||
return (
|
||||
<>
|
||||
<ModalShareStyle />
|
||||
<SideModal
|
||||
isOpen
|
||||
closeOnClickOutside
|
||||
hideCloseButton
|
||||
hideCloseButton={!isSmallMobile}
|
||||
onClose={onClose}
|
||||
width="70vw"
|
||||
width={width}
|
||||
$css="min-width: 320px;max-width: 777px;"
|
||||
>
|
||||
<Box aria-label={t('Share modal')}>
|
||||
<Box aria-label={t('Share modal')} $margin={{ bottom: 'small' }}>
|
||||
<Box $shrink="0">
|
||||
<Card
|
||||
$direction="row"
|
||||
|
||||
@@ -3,10 +3,11 @@ import { useState } from 'react';
|
||||
|
||||
import { BoxButton, Text } from '@/components';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
import { useResponsiveStore } from '@/stores';
|
||||
|
||||
const sizeMap: { [key: number]: string } = {
|
||||
1: '1.2rem',
|
||||
2: '1rem',
|
||||
1: '1.1rem',
|
||||
2: '0.9rem',
|
||||
3: '0.8rem',
|
||||
};
|
||||
|
||||
@@ -32,6 +33,7 @@ export const Heading = ({
|
||||
}: HeadingProps) => {
|
||||
const [isHover, setIsHover] = useState(isHighlight);
|
||||
const { colorsTokens } = useCunninghamTheme();
|
||||
const { isMobile } = useResponsiveStore();
|
||||
|
||||
return (
|
||||
<BoxButton
|
||||
@@ -39,7 +41,11 @@ export const Heading = ({
|
||||
onMouseOver={() => setIsHover(true)}
|
||||
onMouseLeave={() => setIsHover(false)}
|
||||
onClick={() => {
|
||||
editor.focus();
|
||||
// With mobile the focus open the keyboard and the scroll is not working
|
||||
if (!isMobile) {
|
||||
editor.focus();
|
||||
}
|
||||
|
||||
editor.setTextCursorPosition(headingId, 'end');
|
||||
document.querySelector(`[data-id="${headingId}"]`)?.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import { Box, BoxButton, Text } from '@/components';
|
||||
import { HeadingBlock, useDocStore } from '@/features/docs/doc-editor';
|
||||
import { Doc } from '@/features/docs/doc-management';
|
||||
import { useResponsiveStore } from '@/stores';
|
||||
|
||||
import { Heading } from './Heading';
|
||||
|
||||
@@ -14,6 +15,7 @@ interface TableContentProps {
|
||||
|
||||
export const TableContent = ({ doc, headings }: TableContentProps) => {
|
||||
const { docsStore } = useDocStore();
|
||||
const { isMobile } = useResponsiveStore();
|
||||
const { t } = useTranslation();
|
||||
const editor = docsStore?.[doc.id]?.editor;
|
||||
const [headingIdHighlight, setHeadingIdHighlight] = useState<string>();
|
||||
@@ -66,17 +68,20 @@ export const TableContent = ({ doc, headings }: TableContentProps) => {
|
||||
|
||||
return (
|
||||
<Box $padding={{ all: 'small', right: 'none' }} $maxHeight="95%">
|
||||
<Box $overflow="auto">
|
||||
{headings?.map((heading) => (
|
||||
<Heading
|
||||
editor={editor}
|
||||
headingId={heading.id}
|
||||
level={heading.props.level}
|
||||
text={heading.contentText}
|
||||
key={heading.id}
|
||||
isHighlight={headingIdHighlight === heading.id}
|
||||
/>
|
||||
))}
|
||||
<Box $overflow="auto" $padding={{ left: '2px' }}>
|
||||
{headings?.map(
|
||||
(heading) =>
|
||||
heading.contentText && (
|
||||
<Heading
|
||||
editor={editor}
|
||||
headingId={heading.id}
|
||||
level={heading.props.level}
|
||||
text={heading.contentText}
|
||||
key={heading.id}
|
||||
isHighlight={headingIdHighlight === heading.id}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
</Box>
|
||||
<Box
|
||||
$height="1px"
|
||||
@@ -87,7 +92,11 @@ export const TableContent = ({ doc, headings }: TableContentProps) => {
|
||||
/>
|
||||
<BoxButton
|
||||
onClick={() => {
|
||||
editor.focus();
|
||||
// With mobile the focus open the keyboard and the scroll is not working
|
||||
if (!isMobile) {
|
||||
editor.focus();
|
||||
}
|
||||
|
||||
document.querySelector(`.bn-editor`)?.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'start',
|
||||
@@ -101,7 +110,11 @@ export const TableContent = ({ doc, headings }: TableContentProps) => {
|
||||
</BoxButton>
|
||||
<BoxButton
|
||||
onClick={() => {
|
||||
editor.focus();
|
||||
// With mobile the focus open the keyboard and the scroll is not working
|
||||
if (!isMobile) {
|
||||
editor.focus();
|
||||
}
|
||||
|
||||
document
|
||||
.querySelector(
|
||||
`.bn-editor > .bn-block-group > .bn-block-outer:last-child`,
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { DataGrid, SortModel, usePagination } from '@openfun/cunningham-react';
|
||||
import {
|
||||
Column,
|
||||
DataGrid,
|
||||
SortModel,
|
||||
usePagination,
|
||||
} from '@openfun/cunningham-react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { createGlobalStyle } from 'styled-components';
|
||||
@@ -15,6 +20,7 @@ import {
|
||||
useTrans,
|
||||
} from '@/features/docs/doc-management';
|
||||
import { useDate } from '@/hook/';
|
||||
import { useResponsiveStore } from '@/stores';
|
||||
|
||||
import { PAGE_SIZE } from '../conf';
|
||||
|
||||
@@ -62,6 +68,7 @@ export const DocsGrid = () => {
|
||||
]);
|
||||
const { page, pageSize, setPagesCount } = pagination;
|
||||
const [docs, setDocs] = useState<Doc[]>([]);
|
||||
const { isMobile } = useResponsiveStore();
|
||||
|
||||
const ordering = sortModel.length ? formatSortModel(sortModel[0]) : undefined;
|
||||
|
||||
@@ -82,19 +89,117 @@ export const DocsGrid = () => {
|
||||
setPagesCount(data?.count ? Math.ceil(data.count / pageSize) : 0);
|
||||
}, [data?.count, pageSize, setPagesCount]);
|
||||
|
||||
const columns: Column<Doc>[] = [
|
||||
{
|
||||
headerName: '',
|
||||
id: 'visibility',
|
||||
size: 95,
|
||||
renderCell: ({ row }) => {
|
||||
return (
|
||||
row.link_reach === LinkReach.PUBLIC && (
|
||||
<StyledLink href={`/docs/${row.id}`}>
|
||||
<Text
|
||||
$weight="bold"
|
||||
$background={colorsTokens()['primary-600']}
|
||||
$color="white"
|
||||
$padding="xtiny"
|
||||
$radius="3px"
|
||||
>
|
||||
{t('Public')}
|
||||
</Text>
|
||||
</StyledLink>
|
||||
)
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
headerName: t('Document name'),
|
||||
field: 'title',
|
||||
renderCell: ({ row }) => {
|
||||
return (
|
||||
<StyledLink href={`/docs/${row.id}`}>
|
||||
<Text $weight="bold" $theme="primary">
|
||||
{row.title}
|
||||
</Text>
|
||||
</StyledLink>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
headerName: t('Created at'),
|
||||
field: 'created_at',
|
||||
renderCell: ({ row }) => {
|
||||
return (
|
||||
<StyledLink href={`/docs/${row.id}`}>
|
||||
<Text $weight="bold">{formatDate(row.created_at)}</Text>
|
||||
</StyledLink>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
headerName: t('Updated at'),
|
||||
field: 'updated_at',
|
||||
renderCell: ({ row }) => {
|
||||
return (
|
||||
<StyledLink href={`/docs/${row.id}`}>
|
||||
<Text $weight="bold">{formatDate(row.updated_at)}</Text>
|
||||
</StyledLink>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
headerName: t('Your role'),
|
||||
id: 'your_role',
|
||||
renderCell: ({ row }) => {
|
||||
return (
|
||||
<StyledLink href={`/docs/${row.id}`}>
|
||||
<Text $weight="bold">
|
||||
{transRole(currentDocRole(row.abilities))}
|
||||
</Text>
|
||||
</StyledLink>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
headerName: t('Members'),
|
||||
id: 'users_number',
|
||||
renderCell: ({ row }) => {
|
||||
return (
|
||||
<StyledLink href={`/docs/${row.id}`}>
|
||||
<Text $weight="bold">{row.accesses.length}</Text>
|
||||
</StyledLink>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'column-actions',
|
||||
renderCell: ({ row }) => {
|
||||
return <DocsGridActions doc={row} />;
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
// Inverse columns for mobile to have the most important information first
|
||||
if (isMobile) {
|
||||
const tmpCol = columns[0];
|
||||
columns[0] = columns[1];
|
||||
columns[1] = tmpCol;
|
||||
}
|
||||
|
||||
return (
|
||||
<Card
|
||||
$padding={{ bottom: 'small', horizontal: 'big' }}
|
||||
$margin={{ all: 'big', top: 'none' }}
|
||||
$margin={{ all: isMobile ? 'small' : 'big', top: 'none' }}
|
||||
$overflow="auto"
|
||||
aria-label={t(`Datagrid of the documents page {{page}}`, { page })}
|
||||
$height="100%"
|
||||
>
|
||||
<DocsGridStyle />
|
||||
<Text
|
||||
$weight="bold"
|
||||
as="h2"
|
||||
$theme="primary"
|
||||
$margin={{ bottom: 'none' }}
|
||||
$margin={{ bottom: 'small' }}
|
||||
>
|
||||
{t('Documents')}
|
||||
</Text>
|
||||
@@ -102,95 +207,7 @@ export const DocsGrid = () => {
|
||||
{error && <TextErrors causes={error.cause} />}
|
||||
|
||||
<DataGrid
|
||||
columns={[
|
||||
{
|
||||
headerName: '',
|
||||
id: 'visibility',
|
||||
size: 95,
|
||||
renderCell: ({ row }) => {
|
||||
return (
|
||||
<StyledLink href={`/docs/${row.id}`}>
|
||||
{row.link_reach === LinkReach.PUBLIC && (
|
||||
<Text
|
||||
$weight="bold"
|
||||
$background={colorsTokens()['primary-600']}
|
||||
$color="white"
|
||||
$padding="xtiny"
|
||||
$radius="3px"
|
||||
>
|
||||
{t('Public')}
|
||||
</Text>
|
||||
)}
|
||||
</StyledLink>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
headerName: t('Document name'),
|
||||
field: 'title',
|
||||
renderCell: ({ row }) => {
|
||||
return (
|
||||
<StyledLink href={`/docs/${row.id}`}>
|
||||
<Text $weight="bold" $theme="primary">
|
||||
{row.title}
|
||||
</Text>
|
||||
</StyledLink>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
headerName: t('Created at'),
|
||||
field: 'created_at',
|
||||
renderCell: ({ row }) => {
|
||||
return (
|
||||
<StyledLink href={`/docs/${row.id}`}>
|
||||
<Text $weight="bold">{formatDate(row.created_at)}</Text>
|
||||
</StyledLink>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
headerName: t('Updated at'),
|
||||
field: 'updated_at',
|
||||
renderCell: ({ row }) => {
|
||||
return (
|
||||
<StyledLink href={`/docs/${row.id}`}>
|
||||
<Text $weight="bold">{formatDate(row.updated_at)}</Text>
|
||||
</StyledLink>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
headerName: t('Your role'),
|
||||
id: 'your_role',
|
||||
renderCell: ({ row }) => {
|
||||
return (
|
||||
<StyledLink href={`/docs/${row.id}`}>
|
||||
<Text $weight="bold">
|
||||
{transRole(currentDocRole(row.abilities))}
|
||||
</Text>
|
||||
</StyledLink>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
headerName: t('Members'),
|
||||
id: 'users_number',
|
||||
renderCell: ({ row }) => {
|
||||
return (
|
||||
<StyledLink href={`/docs/${row.id}`}>
|
||||
<Text $weight="bold">{row.accesses.length}</Text>
|
||||
</StyledLink>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'column-actions',
|
||||
renderCell: ({ row }) => {
|
||||
return <DocsGridActions doc={row} />;
|
||||
},
|
||||
},
|
||||
]}
|
||||
columns={columns}
|
||||
rows={docs}
|
||||
isLoading={isLoading}
|
||||
pagination={pagination}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Box } from '@/components';
|
||||
import { useCreateDoc, useTrans } from '@/features/docs/doc-management/';
|
||||
import { useResponsiveStore } from '@/stores';
|
||||
|
||||
import { DocsGrid } from './DocsGrid';
|
||||
|
||||
@@ -12,6 +13,7 @@ export const DocsGridContainer = () => {
|
||||
const { t } = useTranslation();
|
||||
const { untitledDocument } = useTrans();
|
||||
const router = useRouter();
|
||||
const { isMobile } = useResponsiveStore();
|
||||
|
||||
const { mutate: createDoc } = useCreateDoc({
|
||||
onSuccess: (doc) => {
|
||||
@@ -25,7 +27,11 @@ export const DocsGridContainer = () => {
|
||||
|
||||
return (
|
||||
<Box $overflow="auto">
|
||||
<Box $align="flex-end" $justify="center" $margin="big">
|
||||
<Box
|
||||
$align="flex-end"
|
||||
$justify="center"
|
||||
$margin={isMobile ? 'small' : 'big'}
|
||||
>
|
||||
<Button onClick={handleCreateDoc}>{t('Create a new document')}</Button>
|
||||
</Box>
|
||||
<DocsGrid />
|
||||
|
||||
@@ -11,6 +11,7 @@ import { Box, IconBG, Text, TextErrors } from '@/components';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
import { Doc, Role } from '@/features/docs/doc-management';
|
||||
import { ChooseRole } from '@/features/docs/members/members-add/';
|
||||
import { useResponsiveStore } from '@/stores';
|
||||
|
||||
import { useDeleteDocInvitation, useUpdateDocInvitation } from '../api';
|
||||
import { Invitation } from '../types';
|
||||
@@ -31,6 +32,7 @@ export const InvitationItem = ({
|
||||
const canDelete = invitation.abilities.destroy;
|
||||
const canUpdate = invitation.abilities.partial_update;
|
||||
const { t } = useTranslation();
|
||||
const { isSmallMobile, screenWidth } = useResponsiveStore();
|
||||
const [localRole, setLocalRole] = useState(role);
|
||||
const { colorsTokens } = useCunninghamTheme();
|
||||
const { toast } = useToastProvider();
|
||||
@@ -62,8 +64,7 @@ export const InvitationItem = ({
|
||||
|
||||
return (
|
||||
<Box $width="100%" $gap="0.7rem">
|
||||
<Box $direction="row" $gap="1rem">
|
||||
<IconBG iconName="account_circle" $size="2rem" />
|
||||
<Box $direction="row" $gap="1rem" $wrap="wrap">
|
||||
<Box
|
||||
$align="center"
|
||||
$direction="row"
|
||||
@@ -71,8 +72,10 @@ export const InvitationItem = ({
|
||||
$justify="space-between"
|
||||
$width="100%"
|
||||
$wrap="wrap"
|
||||
$css={`flex: ${isSmallMobile ? '100%' : '70%'};`}
|
||||
>
|
||||
<Box>
|
||||
<IconBG iconName="account_circle" $size="2rem" />
|
||||
<Box $css="flex:1;">
|
||||
<Text
|
||||
$size="t"
|
||||
$background={colorsTokens()['info-600']}
|
||||
@@ -85,8 +88,15 @@ export const InvitationItem = ({
|
||||
</Text>
|
||||
<Text $justify="center">{invitation.email}</Text>
|
||||
</Box>
|
||||
<Box $direction="row" $gap="1rem" $align="center">
|
||||
<Box $minWidth="13rem">
|
||||
<Box
|
||||
$direction="row"
|
||||
$gap="1rem"
|
||||
$align="center"
|
||||
$justify="space-between"
|
||||
$css="flex:1;"
|
||||
$wrap={screenWidth < 400 ? 'wrap' : 'nowrap'}
|
||||
>
|
||||
<Box $minWidth="13rem" $css={isSmallMobile ? 'flex:1;' : ''}>
|
||||
<ChooseRole
|
||||
label={t('Role')}
|
||||
defaultRole={localRole}
|
||||
@@ -103,25 +113,27 @@ export const InvitationItem = ({
|
||||
/>
|
||||
</Box>
|
||||
{doc.abilities.manage_accesses && (
|
||||
<Button
|
||||
color="tertiary-text"
|
||||
icon={
|
||||
<Text
|
||||
$isMaterialIcon
|
||||
$theme={!canDelete ? 'greyscale' : 'primary'}
|
||||
$variation={!canDelete ? '500' : 'text'}
|
||||
>
|
||||
delete
|
||||
</Text>
|
||||
}
|
||||
disabled={!canDelete}
|
||||
onClick={() =>
|
||||
removeDocInvitation({
|
||||
docId: doc.id,
|
||||
invitationId: invitation.id,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Box $margin={isSmallMobile ? 'auto' : ''}>
|
||||
<Button
|
||||
color="tertiary-text"
|
||||
icon={
|
||||
<Text
|
||||
$isMaterialIcon
|
||||
$theme={!canDelete ? 'greyscale' : 'primary'}
|
||||
$variation={!canDelete ? '500' : 'text'}
|
||||
>
|
||||
delete
|
||||
</Text>
|
||||
}
|
||||
disabled={!canDelete}
|
||||
onClick={() =>
|
||||
removeDocInvitation({
|
||||
docId: doc.id,
|
||||
invitationId: invitation.id,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { APIError } from '@/api';
|
||||
import { Box, Card, InfiniteScroll, TextErrors } from '@/components';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
import { Doc, currentDocRole } from '@/features/docs/doc-management';
|
||||
import { useResponsiveStore } from '@/stores';
|
||||
|
||||
import { useDocInvitationsInfinite } from '../api';
|
||||
import { Invitation } from '../types';
|
||||
@@ -26,6 +27,7 @@ const InvitationListState = ({
|
||||
doc,
|
||||
}: InvitationListStateProps) => {
|
||||
const { colorsTokens } = useCunninghamTheme();
|
||||
const { isSmallMobile } = useResponsiveStore();
|
||||
|
||||
if (error) {
|
||||
return <TextErrors causes={error.cause} />;
|
||||
@@ -49,7 +51,7 @@ const InvitationListState = ({
|
||||
key={`${invitation.id}-${index}`}
|
||||
$background={!(index % 2) ? 'white' : colorsTokens()['greyscale-000']}
|
||||
$direction="row"
|
||||
$padding="small"
|
||||
$padding={isSmallMobile ? 'tiny' : 'small'}
|
||||
$align="center"
|
||||
$gap="1rem"
|
||||
$radius="4px"
|
||||
|
||||
@@ -11,6 +11,7 @@ import { Box, Card, IconBG } from '@/components';
|
||||
import { Doc, Role } from '@/features/docs/doc-management';
|
||||
import { useCreateDocInvitation } from '@/features/docs/members/invitation-list/';
|
||||
import { useLanguage } from '@/i18n/hooks/useLanguage';
|
||||
import { useResponsiveStore } from '@/stores';
|
||||
|
||||
import { useCreateDocAccess } from '../api';
|
||||
import {
|
||||
@@ -37,6 +38,7 @@ interface ModalAddMembersProps {
|
||||
export const AddMembers = ({ currentRole, doc }: ModalAddMembersProps) => {
|
||||
const { contentLanguage } = useLanguage();
|
||||
const { t } = useTranslation();
|
||||
const { isSmallMobile } = useResponsiveStore();
|
||||
const [selectedUsers, setSelectedUsers] = useState<OptionsSelect>([]);
|
||||
const [selectedRole, setSelectedRole] = useState<Role>();
|
||||
const { toast } = useToastProvider();
|
||||
@@ -145,7 +147,12 @@ export const AddMembers = ({ currentRole, doc }: ModalAddMembersProps) => {
|
||||
$wrap="wrap"
|
||||
>
|
||||
<IconBG iconName="group_add" />
|
||||
<Box $gap="0.7rem" $direction="row" $wrap="wrap" $css="flex: 70%;">
|
||||
<Box
|
||||
$gap="0.7rem"
|
||||
$direction="row"
|
||||
$wrap={isSmallMobile ? 'wrap' : 'nowrap'}
|
||||
$css="flex: 70%;"
|
||||
>
|
||||
<Box $gap="0.7rem" $direction="row" $wrap="wrap" $css="flex: 80%;">
|
||||
<Box $css="flex: auto;" $width="15rem">
|
||||
<SearchUsers
|
||||
|
||||
@@ -12,6 +12,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import { Box, IconBG, Text, TextErrors } from '@/components';
|
||||
import { Access, Doc, Role } from '@/features/docs/doc-management';
|
||||
import { ChooseRole } from '@/features/docs/members/members-add/';
|
||||
import { useResponsiveStore } from '@/stores';
|
||||
|
||||
import { useDeleteDocAccess, useUpdateDocAccess } from '../api';
|
||||
import { useWhoAmI } from '../hooks/useWhoAmI';
|
||||
@@ -31,6 +32,7 @@ export const MemberItem = ({
|
||||
}: MemberItemProps) => {
|
||||
const { isMyself, isLastOwner, isOtherOwner } = useWhoAmI(access);
|
||||
const { t } = useTranslation();
|
||||
const { isSmallMobile, screenWidth } = useResponsiveStore();
|
||||
const [localRole, setLocalRole] = useState(role);
|
||||
const { toast } = useToastProvider();
|
||||
const router = useRouter();
|
||||
@@ -71,8 +73,7 @@ export const MemberItem = ({
|
||||
|
||||
return (
|
||||
<Box $width="100%">
|
||||
<Box $direction="row" $gap="1rem">
|
||||
<IconBG iconName="account_circle" $size="2rem" />
|
||||
<Box $direction="row" $gap="1rem" $wrap="wrap">
|
||||
<Box
|
||||
$align="center"
|
||||
$direction="row"
|
||||
@@ -80,10 +81,21 @@ export const MemberItem = ({
|
||||
$justify="space-between"
|
||||
$width="100%"
|
||||
$wrap="wrap"
|
||||
$css={`flex: ${isSmallMobile ? '100%' : '70%'};`}
|
||||
>
|
||||
<Text $justify="center">{access.user.email}</Text>
|
||||
<Box $direction="row" $gap="1rem" $align="center">
|
||||
<Box $minWidth="13rem">
|
||||
<IconBG iconName="account_circle" $size="2rem" />
|
||||
<Text $justify="center" $css="flex:1;">
|
||||
{access.user.email}
|
||||
</Text>
|
||||
<Box
|
||||
$direction="row"
|
||||
$gap="1rem"
|
||||
$align="center"
|
||||
$justify="space-between"
|
||||
$css="flex:1;"
|
||||
$wrap={screenWidth < 400 ? 'wrap' : 'nowrap'}
|
||||
>
|
||||
<Box $minWidth="13rem" $css={isSmallMobile ? 'flex:1;' : ''}>
|
||||
<ChooseRole
|
||||
label={t('Role')}
|
||||
defaultRole={localRole}
|
||||
@@ -100,22 +112,24 @@ export const MemberItem = ({
|
||||
/>
|
||||
</Box>
|
||||
{doc.abilities.manage_accesses && (
|
||||
<Button
|
||||
color="tertiary-text"
|
||||
icon={
|
||||
<Text
|
||||
$isMaterialIcon
|
||||
$theme={isNotAllowed ? 'greyscale' : 'primary'}
|
||||
$variation={isNotAllowed ? '500' : 'text'}
|
||||
>
|
||||
delete
|
||||
</Text>
|
||||
}
|
||||
disabled={isNotAllowed}
|
||||
onClick={() =>
|
||||
removeDocAccess({ docId: doc.id, accessId: access.id })
|
||||
}
|
||||
/>
|
||||
<Box $margin={isSmallMobile ? 'auto' : ''}>
|
||||
<Button
|
||||
color="tertiary-text"
|
||||
icon={
|
||||
<Text
|
||||
$isMaterialIcon
|
||||
$theme={isNotAllowed ? 'greyscale' : 'primary'}
|
||||
$variation={isNotAllowed ? '500' : 'text'}
|
||||
>
|
||||
delete
|
||||
</Text>
|
||||
}
|
||||
disabled={isNotAllowed}
|
||||
onClick={() =>
|
||||
removeDocAccess({ docId: doc.id, accessId: access.id })
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { APIError } from '@/api';
|
||||
import { Box, Card, InfiniteScroll, TextErrors } from '@/components';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
import { Access, Doc, currentDocRole } from '@/features/docs/doc-management';
|
||||
import { useResponsiveStore } from '@/stores';
|
||||
|
||||
import { useDocAccessesInfinite } from '../api';
|
||||
|
||||
@@ -25,6 +26,7 @@ const MemberListState = ({
|
||||
doc,
|
||||
}: MemberListStateProps) => {
|
||||
const { colorsTokens } = useCunninghamTheme();
|
||||
const { isSmallMobile } = useResponsiveStore();
|
||||
|
||||
if (error) {
|
||||
return <TextErrors causes={error.cause} />;
|
||||
@@ -48,7 +50,7 @@ const MemberListState = ({
|
||||
key={`${access.id}-${index}`}
|
||||
$background={!(index % 2) ? 'white' : colorsTokens()['greyscale-000']}
|
||||
$direction="row"
|
||||
$padding="small"
|
||||
$padding={isSmallMobile ? 'tiny' : 'small'}
|
||||
$align="center"
|
||||
$gap="1rem"
|
||||
$radius="4px"
|
||||
|
||||
Reference in New Issue
Block a user