Skip to content

Commit 33928f0

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 1b8ff42 commit 33928f0

File tree

13 files changed

+420
-87
lines changed

13 files changed

+420
-87
lines changed

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

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
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 {
12+
addChild,
13+
createRootSubPage,
14+
navigateToPageFromTree,
15+
} from './utils-sub-pages';
1016

1117
test.beforeEach(async ({ page }) => {
1218
await page.goto('/');
@@ -67,4 +73,69 @@ test.describe('Doc Trashbin', () => {
6773
await page.getByRole('link', { name: 'Trashbin' }).click();
6874
await expect(row2.getByText(title2)).toBeHidden();
6975
});
76+
77+
test('it controls UI and interaction from the doc page', async ({
78+
page,
79+
browserName,
80+
}) => {
81+
const [topParent] = await createDoc(
82+
page,
83+
'my-trash-editor-doc',
84+
browserName,
85+
1,
86+
);
87+
await verifyDocName(page, topParent);
88+
const { name: subDocName } = await createRootSubPage(
89+
page,
90+
browserName,
91+
'my-trash-editor-subdoc',
92+
);
93+
94+
const subsubDocName = await addChild({
95+
page,
96+
browserName,
97+
docParent: subDocName,
98+
});
99+
await verifyDocName(page, subsubDocName);
100+
101+
await navigateToPageFromTree({ page, title: subDocName });
102+
await verifyDocName(page, subDocName);
103+
104+
await clickInEditorMenu(page, 'Delete document');
105+
await page.getByRole('button', { name: 'Delete document' }).click();
106+
107+
await page.getByRole('link', { name: 'Trashbin' }).click();
108+
const row = await getGridRow(page, subDocName);
109+
await row.getByText(subDocName).click();
110+
await verifyDocName(page, subDocName);
111+
112+
await expect(page.getByLabel('Alert deleted document')).toBeVisible();
113+
await expect(page.getByRole('button', { name: 'Share' })).toBeDisabled();
114+
await expect(page.locator('.bn-editor')).toHaveAttribute(
115+
'contenteditable',
116+
'false',
117+
);
118+
const docTree = page.getByTestId('doc-tree');
119+
await expect(docTree.getByText(topParent)).toBeHidden();
120+
await expect(
121+
docTree.getByText(subDocName, {
122+
exact: true,
123+
}),
124+
).toBeVisible();
125+
await expect(docTree.getByText(subsubDocName)).toBeVisible();
126+
await expect(
127+
docTree
128+
.locator(".--docs-sub-page-item[aria-disabled='true']")
129+
.getByText(subsubDocName),
130+
).toBeVisible();
131+
132+
await page.getByRole('button', { name: 'Restore' }).click();
133+
await expect(page.getByLabel('Alert deleted document')).toBeHidden();
134+
await expect(page.locator('.bn-editor')).toHaveAttribute(
135+
'contenteditable',
136+
'true',
137+
);
138+
await expect(page.getByRole('button', { name: 'Share' })).toBeEnabled();
139+
await expect(docTree.getByText(topParent)).toBeVisible();
140+
});
70141
});

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/e2e/__tests__/app-impress/utils-sub-pages.ts

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Page, expect } from '@playwright/test';
22

33
import {
4+
BrowserName,
45
randomName,
56
updateDocTitle,
67
verifyDocName,
@@ -9,7 +10,7 @@ import {
910

1011
export const createRootSubPage = async (
1112
page: Page,
12-
browserName: string,
13+
browserName: BrowserName,
1314
docName: string,
1415
isMobile: boolean = false,
1516
) => {
@@ -67,6 +68,47 @@ export const clickOnAddRootSubPage = async (page: Page) => {
6768
await rootItem.getByTestId('doc-tree-item-actions-add-child').click();
6869
};
6970

71+
export const addChild = async ({
72+
page,
73+
browserName,
74+
docParent,
75+
}: {
76+
page: Page;
77+
browserName: BrowserName;
78+
docParent: string;
79+
}) => {
80+
let item = page.getByTestId('doc-tree-root-item');
81+
82+
const isParent = await item
83+
.filter({
84+
hasText: docParent,
85+
})
86+
.first()
87+
.count();
88+
89+
if (!isParent) {
90+
const items = page.getByRole('treeitem');
91+
92+
item = items
93+
.filter({
94+
hasText: docParent,
95+
})
96+
.first();
97+
}
98+
99+
await item.hover();
100+
await item.getByTestId('doc-tree-item-actions-add-child').click();
101+
102+
const [name] = randomName(docParent, browserName, 1);
103+
await updateDocTitle(page, name);
104+
105+
return name;
106+
};
107+
108+
export const navigateToTopParentFromTree = async ({ page }: { page: Page }) => {
109+
await page.getByRole('link', { name: /Open root document/ }).click();
110+
};
111+
70112
export const navigateToPageFromTree = async ({
71113
page,
72114
title,

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: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import { useTreeContext } from '@gouvfr-lasuite/ui-kit';
2+
import { VariantType, useToastProvider } from '@openfun/cunningham-react';
3+
import { useTranslation } from 'react-i18next';
4+
import { css } from 'styled-components';
5+
6+
import { Box, BoxButton, Icon, Text } from '@/components';
7+
import { useCunninghamTheme } from '@/cunningham';
8+
import {
9+
Doc,
10+
KEY_DOC,
11+
KEY_LIST_DOC,
12+
useRestoreDoc,
13+
} from '@/docs/doc-management';
14+
import { KEY_LIST_DOC_TRASHBIN } from '@/docs/docs-grid';
15+
16+
export const AlertRestore = ({ doc }: { doc: Doc }) => {
17+
const { t } = useTranslation();
18+
const { toast } = useToastProvider();
19+
const treeContext = useTreeContext<Doc>();
20+
const { colorsTokens, spacingsTokens } = useCunninghamTheme();
21+
const { mutate: restoreDoc, error } = useRestoreDoc({
22+
listInvalidQueries: [KEY_LIST_DOC, KEY_LIST_DOC_TRASHBIN, KEY_DOC],
23+
options: {
24+
onSuccess: (_data) => {
25+
// It will force the tree to be reloaded
26+
treeContext?.setRoot(undefined as unknown as Doc);
27+
28+
toast(t('The document has been restored.'), VariantType.SUCCESS, {
29+
duration: 4000,
30+
});
31+
},
32+
onError: () => {
33+
toast(
34+
t('An error occurred while restoring the document: {{error}}', {
35+
error: error?.message,
36+
}),
37+
VariantType.ERROR,
38+
{
39+
duration: 4000,
40+
},
41+
);
42+
},
43+
},
44+
});
45+
46+
return (
47+
<Box
48+
className="--docs--alert-restore"
49+
aria-label={t('Alert deleted document')}
50+
$color={colorsTokens['danger-800']}
51+
$background={colorsTokens['danger-100']}
52+
$radius={spacingsTokens['3xs']}
53+
$direction="row"
54+
$padding="xs"
55+
$flex={1}
56+
$align="center"
57+
$gap={spacingsTokens['3xs']}
58+
$css={css`
59+
border: 1px solid var(--c--theme--colors--danger-300, #e3e3fd);
60+
`}
61+
$justify="space-between"
62+
>
63+
<Box $direction="row" $align="center" $gap="0.2rem">
64+
<Icon
65+
$theme="danger"
66+
$variation="700"
67+
data-testid="public-icon"
68+
iconName="delete"
69+
variant="symbols-outlined"
70+
/>
71+
<Text $theme="danger" $variation="700" $weight="500">
72+
{t('Document deleted')}
73+
</Text>
74+
</Box>
75+
<BoxButton
76+
onClick={() =>
77+
restoreDoc({
78+
docId: doc.id,
79+
})
80+
}
81+
$direction="row"
82+
$gap="0.2rem"
83+
>
84+
<Icon
85+
iconName="undo"
86+
$theme="danger"
87+
$variation="600"
88+
$css="font-size: 18px !important;"
89+
variant="symbols-outlined"
90+
/>
91+
<Text $theme="danger" $variation="600" $size="s">
92+
{t('Restore')}
93+
</Text>
94+
</BoxButton>
95+
</Box>
96+
);
97+
};

0 commit comments

Comments
 (0)