Skip to content

Commit 3ec8300

Browse files
committed
✨(frontend) doc page when deleted
Whe the doc is deleted, the doc page is a bit different, we have to adapt the doc header to add some information and actions that are relevant for a deleted doc.
1 parent 4a5375c commit 3ec8300

File tree

12 files changed

+359
-86
lines changed

12 files changed

+359
-86
lines changed

src/frontend/apps/e2e/__tests__/app-impress/doc-trashbin.spec.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import { expect, test } from '@playwright/test';
22

33
import {
4+
clickInEditorMenu,
45
clickInGridMenu,
56
createDoc,
67
getGridRow,
78
verifyDocName,
89
} from './utils-common';
910
import { addNewMember } from './utils-share';
11+
import { createRootSubPage } from './utils-sub-pages';
1012

1113
test.beforeEach(async ({ page }) => {
1214
await page.goto('/');
@@ -67,4 +69,60 @@ test.describe('Doc Trashbin', () => {
6769
await page.getByRole('link', { name: 'Trashbin' }).click();
6870
await expect(row2.getByText(title2)).toBeHidden();
6971
});
72+
73+
test('it controls UI and interaction from the doc page', async ({
74+
page,
75+
browserName,
76+
}) => {
77+
const [title] = await createDoc(
78+
page,
79+
'my-trash-editor-doc',
80+
browserName,
81+
1,
82+
);
83+
await verifyDocName(page, title);
84+
const { name: subDocName } = await createRootSubPage(
85+
page,
86+
browserName,
87+
'my-trash-editor-subdoc',
88+
);
89+
90+
await page.getByRole('link', { name: /Open root document/ }).click();
91+
await verifyDocName(page, title);
92+
93+
await clickInEditorMenu(page, 'Delete document');
94+
await page.getByRole('button', { name: 'Delete document' }).click();
95+
96+
await page.getByRole('link', { name: 'Trashbin' }).click();
97+
const row = await getGridRow(page, title);
98+
await row.getByText(title).click();
99+
await verifyDocName(page, title);
100+
101+
await expect(page.getByLabel('Alert deleted document')).toBeVisible();
102+
await expect(page.getByRole('button', { name: 'Share' })).toBeDisabled();
103+
await expect(page.locator('.bn-editor')).toHaveAttribute(
104+
'contenteditable',
105+
'false',
106+
);
107+
const docTree = page.getByTestId('doc-tree');
108+
await expect(docTree).toBeVisible();
109+
// expect Cannot Receive Events
110+
await expect(
111+
page.getByTestId('doc-tree').getByText(subDocName),
112+
).toBeVisible();
113+
await expect(
114+
page
115+
.getByTestId('doc-tree')
116+
.locator(".--docs-sub-page-item[aria-disabled='true']")
117+
.getByText(subDocName),
118+
).toBeVisible();
119+
120+
await page.getByRole('button', { name: 'Restore' }).click();
121+
await expect(page.getByLabel('Alert deleted document')).toBeHidden();
122+
await expect(page.locator('.bn-editor')).toHaveAttribute(
123+
'contenteditable',
124+
'true',
125+
);
126+
await expect(page.getByRole('button', { name: 'Share' })).toBeEnabled();
127+
});
70128
});

src/frontend/apps/e2e/__tests__/app-impress/utils-common.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,11 @@ export async function waitForLanguageSwitch(
327327
await page.getByRole('menuitem', { name: lang.label }).click();
328328
}
329329

330+
export const clickInEditorMenu = async (page: Page, textButton: string) => {
331+
await page.getByRole('button', { name: 'Open the document options' }).click();
332+
await page.getByRole('menuitem', { name: textButton }).click();
333+
};
334+
330335
export const clickInGridMenu = async (
331336
page: Page,
332337
row: Locator,

src/frontend/apps/impress/src/components/Card.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,15 @@ import { Box, BoxType } from '.';
77

88
export const Card = ({
99
children,
10+
className,
1011
$css,
1112
...props
1213
}: PropsWithChildren<BoxType>) => {
1314
const { colorsTokens } = useCunninghamTheme();
1415

1516
return (
1617
<Box
17-
className={`--docs--card ${props.className || ''}`}
18+
className={`--docs--card ${className || ''}`}
1819
$background="white"
1920
$radius="4px"
2021
$css={css`
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { PropsWithChildren } from 'react';
2+
import { css } from 'styled-components';
3+
4+
import { Box, BoxType } from '.';
5+
6+
type OverlayerProps = PropsWithChildren<{
7+
isOverlay: boolean;
8+
}> &
9+
Partial<BoxType>;
10+
11+
export const Overlayer = ({
12+
children,
13+
className,
14+
$css,
15+
isOverlay,
16+
...props
17+
}: OverlayerProps) => {
18+
if (!isOverlay) {
19+
return children;
20+
}
21+
22+
return (
23+
<Box
24+
className={`--docs--overlayer ${className || ''}`}
25+
$opacity="0.4"
26+
$zIndex="10"
27+
$css={css`
28+
${$css}
29+
pointer-events: none;
30+
`}
31+
{...props}
32+
>
33+
{children}
34+
</Box>
35+
);
36+
};

src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteEditor.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
8383
const { isEditable, isLoading } = useIsCollaborativeEditable(doc);
8484
const isConnectedToCollabServer = provider.isSynced;
8585
const readOnly = !doc.abilities.partial_update || !isEditable || isLoading;
86+
const isDeletedDoc = !!doc.deleted_at;
8687

8788
useSaveDoc(doc.id, provider.document, !readOnly, isConnectedToCollabServer);
8889
const { i18n } = useTranslation();
@@ -180,7 +181,7 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
180181
<Box
181182
$padding={{ top: 'md' }}
182183
$background="white"
183-
$css={cssEditor(readOnly)}
184+
$css={cssEditor(readOnly, isDeletedDoc)}
184185
className="--docs--editor-container"
185186
>
186187
{errorAttachment && (
@@ -231,7 +232,7 @@ export const BlockNoteEditorVersion = ({
231232
);
232233

233234
return (
234-
<Box $css={cssEditor(readOnly)} className="--docs--editor-container">
235+
<Box $css={cssEditor(readOnly, true)} className="--docs--editor-container">
235236
<BlockNoteView editor={editor} editable={!readOnly} theme="light" />
236237
</Box>
237238
);

src/frontend/apps/impress/src/features/docs/doc-editor/styles.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { css } from 'styled-components';
22

3-
export const cssEditor = (readonly: boolean) => css`
3+
export const cssEditor = (readonly: boolean, isDeletedDoc: boolean) => css`
44
&,
55
& > .bn-container,
66
& .ProseMirror {
@@ -127,6 +127,13 @@ export const cssEditor = (readonly: boolean) => css`
127127
.bn-block-outer:not([data-prev-depth-changed]):before {
128128
border-left: none;
129129
}
130+
131+
${isDeletedDoc &&
132+
`
133+
a {
134+
pointer-events: none;
135+
}
136+
`}
130137
}
131138
132139
& .bn-editor {
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import { VariantType, useToastProvider } from '@openfun/cunningham-react';
2+
import { useTranslation } from 'react-i18next';
3+
import { css } from 'styled-components';
4+
5+
import { Box, BoxButton, Icon, Text } from '@/components';
6+
import { useCunninghamTheme } from '@/cunningham';
7+
import {
8+
Doc,
9+
KEY_DOC,
10+
KEY_LIST_DOC,
11+
useRestoreDoc,
12+
} from '@/docs/doc-management';
13+
import { KEY_LIST_DOC_TRASHBIN } from '@/docs/docs-grid';
14+
15+
export const AlertRestore = ({ doc }: { doc: Doc }) => {
16+
const { t } = useTranslation();
17+
const { toast } = useToastProvider();
18+
const { colorsTokens, spacingsTokens } = useCunninghamTheme();
19+
const { mutate: restoreDoc, error } = useRestoreDoc({
20+
listInvalidQueries: [KEY_LIST_DOC, KEY_LIST_DOC_TRASHBIN, KEY_DOC],
21+
options: {
22+
onSuccess: (_data) => {
23+
toast(t('The document has been restored.'), VariantType.SUCCESS, {
24+
duration: 4000,
25+
});
26+
},
27+
onError: () => {
28+
toast(
29+
t('An error occurred while restoring the document: {{error}}', {
30+
error: error?.message,
31+
}),
32+
VariantType.ERROR,
33+
{
34+
duration: 4000,
35+
},
36+
);
37+
},
38+
},
39+
});
40+
41+
return (
42+
<Box
43+
className="--docs--alert-restore"
44+
aria-label={t('Alert deleted document')}
45+
$color={colorsTokens['danger-800']}
46+
$background={colorsTokens['danger-100']}
47+
$radius={spacingsTokens['3xs']}
48+
$direction="row"
49+
$padding="xs"
50+
$flex={1}
51+
$align="center"
52+
$gap={spacingsTokens['3xs']}
53+
$css={css`
54+
border: 1px solid var(--c--theme--colors--danger-300, #e3e3fd);
55+
`}
56+
$justify="space-between"
57+
>
58+
<Box $direction="row" $align="center" $gap="0.2rem">
59+
<Icon
60+
$theme="danger"
61+
$variation="700"
62+
data-testid="public-icon"
63+
iconName="delete"
64+
variant="symbols-outlined"
65+
/>
66+
<Text $theme="danger" $variation="700" $weight="500">
67+
{t('Document deleted')}
68+
</Text>
69+
</Box>
70+
<BoxButton
71+
onClick={() =>
72+
restoreDoc({
73+
docId: doc.id,
74+
})
75+
}
76+
$direction="row"
77+
$gap="0.2rem"
78+
>
79+
<Icon
80+
iconName="undo"
81+
$theme="danger"
82+
$variation="600"
83+
$css="font-size: 18px !important;"
84+
variant="symbols-outlined"
85+
/>
86+
<Text $theme="danger" $variation="600" $size="s">
87+
{t('Restore')}
88+
</Text>
89+
</BoxButton>
90+
</Box>
91+
);
92+
};
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { useTreeContext } from '@gouvfr-lasuite/ui-kit';
2+
import { Button } from '@openfun/cunningham-react';
3+
import { useMemo } from 'react';
4+
import { useTranslation } from 'react-i18next';
5+
import { css } from 'styled-components';
6+
7+
import { Box, Icon } from '@/components';
8+
import { Doc } from '@/docs/doc-management';
9+
10+
interface BoutonShareProps {
11+
displayNbAccess: boolean;
12+
doc: Doc;
13+
isDisabled?: boolean;
14+
isHidden?: boolean;
15+
open: () => void;
16+
}
17+
18+
export const BoutonShare = ({
19+
displayNbAccess,
20+
doc,
21+
isDisabled,
22+
isHidden,
23+
open,
24+
}: BoutonShareProps) => {
25+
const { t } = useTranslation();
26+
const treeContext = useTreeContext<Doc>();
27+
28+
/**
29+
* Following the change where there is no default owner when adding a sub-page,
30+
* we need to handle both the case where the doc is the root and the case of sub-pages.
31+
*/
32+
const hasAccesses = useMemo(() => {
33+
if (treeContext?.root?.id === doc.id) {
34+
return doc.nb_accesses_direct > 1 && displayNbAccess;
35+
}
36+
37+
return doc.nb_accesses_direct >= 1 && displayNbAccess;
38+
}, [doc.id, treeContext?.root, doc.nb_accesses_direct, displayNbAccess]);
39+
40+
if (isHidden) {
41+
return null;
42+
}
43+
44+
if (hasAccesses) {
45+
return (
46+
<Box
47+
$css={css`
48+
.c__button--medium {
49+
height: 32px;
50+
padding: 10px var(--c--theme--spacings--xs);
51+
gap: 7px;
52+
}
53+
`}
54+
>
55+
<Button
56+
color="tertiary"
57+
aria-label={t('Share button')}
58+
icon={
59+
<Icon
60+
iconName="group"
61+
$theme="primary"
62+
$variation="800"
63+
variant="filled"
64+
disabled={isDisabled}
65+
/>
66+
}
67+
onClick={open}
68+
size="medium"
69+
disabled={isDisabled}
70+
>
71+
{doc.nb_accesses_direct}
72+
</Button>
73+
</Box>
74+
);
75+
}
76+
77+
return (
78+
<Button
79+
color="tertiary-text"
80+
onClick={open}
81+
size="medium"
82+
disabled={isDisabled}
83+
>
84+
{t('Share')}
85+
</Button>
86+
);
87+
};

0 commit comments

Comments
 (0)