Skip to content

Commit 3c8cacc

Browse files
committed
🛂(frontend) block edition to not connected users
If an editor is working on a shared document but is not connected to the collaborative server we are now blocking the edition. It is to avoid none connected users to overwrite the document with connected users.
1 parent 598fb4f commit 3c8cacc

File tree

11 files changed

+284
-42
lines changed

11 files changed

+284
-42
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ and this project adheres to
1515
- 🚩(frontend) version MIT only #911
1616
- ✨(backend) integrate maleware_detection from django-lasuite #936
1717
- 🩺(CI) add lint spell mistakes #954
18+
- 🛂(frontend) block edition to not connected users #945
1819

1920
## Changed
2021

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ export const verifyDocName = async (page: Page, docName: string) => {
102102
export const addNewMember = async (
103103
page: Page,
104104
index: number,
105-
role: 'Administrator' | 'Owner' | 'Member' | 'Editor' | 'Reader',
105+
role: 'Administrator' | 'Owner' | 'Editor' | 'Reader',
106106
fillText: string = 'user ',
107107
) => {
108108
const responsePromiseSearchUser = page.waitForResponse(

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

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import { expect, test } from '@playwright/test';
44
import cs from 'convert-stream';
55

66
import {
7+
CONFIG,
8+
addNewMember,
79
createDoc,
810
goToGridDoc,
911
mockedDocument,
@@ -363,7 +365,7 @@ test.describe('Doc Editor', () => {
363365
partial_update: true,
364366
retrieve: true,
365367
},
366-
link_reach: 'public',
368+
link_reach: 'restricted',
367369
link_role: 'editor',
368370
created_at: '2021-09-01T09:00:00Z',
369371
title: '',
@@ -453,6 +455,55 @@ test.describe('Doc Editor', () => {
453455
expect(svgBuffer.toString()).toContain('Hello svg');
454456
});
455457

458+
test('it checks block editing when not connected to collab server', async ({
459+
page,
460+
}) => {
461+
await page.route('**/api/v1.0/config/', async (route) => {
462+
const request = route.request();
463+
if (request.method().includes('GET')) {
464+
await route.fulfill({
465+
json: {
466+
...CONFIG,
467+
COLLABORATION_WS_URL: 'ws://localhost:5555/collaboration/ws/',
468+
},
469+
});
470+
} else {
471+
await route.continue();
472+
}
473+
});
474+
475+
await page.goto('/');
476+
477+
void page
478+
.getByRole('button', {
479+
name: 'New doc',
480+
})
481+
.click();
482+
483+
const card = page.getByLabel('It is the card information');
484+
await expect(
485+
card.getByText('Your network do not allow you to edit'),
486+
).toBeHidden();
487+
const editor = page.locator('.ProseMirror');
488+
489+
await expect(editor).toHaveAttribute('contenteditable', 'true');
490+
491+
await page.getByRole('button', { name: 'Share' }).click();
492+
493+
await addNewMember(page, 0, 'Editor', 'impress');
494+
495+
// Close the modal
496+
await page.getByRole('button', { name: 'close' }).first().click();
497+
498+
await expect(
499+
card.getByText('Your network do not allow you to edit'),
500+
).toBeVisible({
501+
timeout: 10000,
502+
});
503+
504+
await expect(editor).toHaveAttribute('contenteditable', 'false');
505+
});
506+
456507
test('it checks if callout custom block', async ({ page, browserName }) => {
457508
await createDoc(page, 'doc-toolbar', browserName, 1);
458509

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import { useTranslation } from 'react-i18next';
1515
import * as Y from 'yjs';
1616

1717
import { Box, TextErrors } from '@/components';
18-
import { Doc } from '@/docs/doc-management';
18+
import { Doc, useIsCollaborativeEditable } from '@/docs/doc-management';
1919
import { useAuth } from '@/features/auth';
2020

2121
import { useUploadFile } from '../hook';
@@ -49,7 +49,9 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
4949
const { setEditor } = useEditorStore();
5050
const { t } = useTranslation();
5151

52-
const readOnly = !doc.abilities.partial_update;
52+
const { isEditable, isLoading } = useIsCollaborativeEditable(doc);
53+
const readOnly = !doc.abilities.partial_update || !isEditable || isLoading;
54+
5355
useSaveDoc(doc.id, provider.document, !readOnly);
5456
const { i18n } = useTranslation();
5557
const lang = i18n.resolvedLanguage;

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ interface DocEditorProps {
2525

2626
export const DocEditor = ({ doc, versionId }: DocEditorProps) => {
2727
const { isDesktop } = useResponsiveStore();
28-
2928
const isVersion = !!versionId && typeof versionId === 'string';
3029

3130
const { colorsTokens } = useCunninghamTheme();
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import { Button, Modal, ModalSize } from '@openfun/cunningham-react';
2+
import { t } from 'i18next';
3+
import { useState } from 'react';
4+
import { useTranslation } from 'react-i18next';
5+
import { css } from 'styled-components';
6+
7+
import { Box, BoxButton, Icon, Text } from '@/components';
8+
import { useCunninghamTheme } from '@/cunningham';
9+
10+
export const AlertNetwork = () => {
11+
const { t } = useTranslation();
12+
const { colorsTokens, spacingsTokens } = useCunninghamTheme();
13+
const [isModalOpen, setIsModalOpen] = useState(false);
14+
15+
return (
16+
<>
17+
<Box>
18+
<Box
19+
$direction="row"
20+
$justify="space-between"
21+
$width="100%"
22+
$background={colorsTokens['warning-100']}
23+
$radius={spacingsTokens['3xs']}
24+
$padding="xs"
25+
$flex={1}
26+
$align="center"
27+
$gap={spacingsTokens['3xs']}
28+
$css={css`
29+
border: 1px solid var(--c--theme--colors--warning-300);
30+
`}
31+
>
32+
<Box $direction="row" $gap={spacingsTokens['2xs']}>
33+
<Icon iconName="mobiledata_off" $theme="warning" $variation="600" />
34+
<Text $theme="warning" $variation="600" $weight={500}>
35+
{t('Your network do not allow you to edit')}
36+
</Text>
37+
</Box>
38+
<BoxButton
39+
$direction="row"
40+
$gap={spacingsTokens['3xs']}
41+
$align="center"
42+
onClick={() => setIsModalOpen(true)}
43+
>
44+
<Icon
45+
iconName="info"
46+
$theme="warning"
47+
$variation="600"
48+
$size="16px"
49+
$weight="500"
50+
$margin={{ top: 'auto' }}
51+
/>
52+
<Text $theme="warning" $variation="600" $weight="500" $size="xs">
53+
{t('Know more')}
54+
</Text>
55+
</BoxButton>
56+
</Box>
57+
</Box>
58+
{isModalOpen && (
59+
<AlertNetworkModal onClose={() => setIsModalOpen(false)} />
60+
)}
61+
</>
62+
);
63+
};
64+
65+
interface AlertNetworkModalProps {
66+
onClose: () => void;
67+
}
68+
69+
export const AlertNetworkModal = ({ onClose }: AlertNetworkModalProps) => {
70+
return (
71+
<Modal
72+
isOpen
73+
closeOnClickOutside
74+
onClose={() => onClose()}
75+
rightActions={
76+
<>
77+
<Button aria-label={t('OK')} onClick={onClose}>
78+
{t('OK')}
79+
</Button>
80+
</>
81+
}
82+
size={ModalSize.MEDIUM}
83+
title={
84+
<Text
85+
$size="h6"
86+
as="h6"
87+
$margin={{ all: '0' }}
88+
$align="flex-start"
89+
$variation="1000"
90+
>
91+
{t("Why can't I edit?")}
92+
</Text>
93+
}
94+
>
95+
<Box
96+
aria-label={t('Content modal to explain why the user cannot edit')}
97+
className="--docs--modal-alert-network"
98+
$margin={{ top: 'xs' }}
99+
>
100+
<Text $size="sm" $variation="600">
101+
{t(
102+
'The network configuration of your workstation or internet connection does not allow editing shared documents.',
103+
)}
104+
</Text>
105+
<Text $size="sm" $variation="600" $margin={{ top: 'xs' }}>
106+
{t(
107+
'Docs use WebSockets to enable real-time editing. These communication channels allow instant and bidirectional exchanges between your browser and our servers. To access collaborative editing, please contact your IT department to enable WebSockets.',
108+
)}
109+
</Text>
110+
</Box>
111+
</Modal>
112+
);
113+
};
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { useTranslation } from 'react-i18next';
2+
import { css } from 'styled-components';
3+
4+
import { Box, Icon, Text } from '@/components';
5+
import { useCunninghamTheme } from '@/cunningham';
6+
7+
export const AlertPublic = ({ isPublicDoc }: { isPublicDoc: boolean }) => {
8+
const { t } = useTranslation();
9+
const { colorsTokens, spacingsTokens } = useCunninghamTheme();
10+
11+
return (
12+
<Box
13+
aria-label={t('Public document')}
14+
$color={colorsTokens['primary-800']}
15+
$background={colorsTokens['primary-050']}
16+
$radius={spacingsTokens['3xs']}
17+
$direction="row"
18+
$padding="xs"
19+
$flex={1}
20+
$align="center"
21+
$gap={spacingsTokens['3xs']}
22+
$css={css`
23+
border: 1px solid var(--c--theme--colors--primary-300, #e3e3fd);
24+
`}
25+
>
26+
<Icon
27+
$theme="primary"
28+
$variation="800"
29+
data-testid="public-icon"
30+
iconName={isPublicDoc ? 'public' : 'vpn_lock'}
31+
/>
32+
<Text $theme="primary" $variation="800" $weight="500">
33+
{isPublicDoc
34+
? t('Public document')
35+
: t('Document accessible to any connected person')}
36+
</Text>
37+
</Box>
38+
);
39+
};

src/frontend/apps/impress/src/features/docs/doc-header/components/DocHeader.tsx

Lines changed: 23 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,20 @@
11
import { DateTime } from 'luxon';
22
import { useTranslation } from 'react-i18next';
3-
import { css } from 'styled-components';
43

5-
import { Box, HorizontalSeparator, Icon, Text } from '@/components';
4+
import { Box, HorizontalSeparator, Text } from '@/components';
65
import { useCunninghamTheme } from '@/cunningham';
76
import {
87
Doc,
98
LinkReach,
9+
Role,
1010
currentDocRole,
11+
useIsCollaborativeEditable,
1112
useTrans,
1213
} from '@/docs/doc-management';
1314
import { useResponsiveStore } from '@/stores';
1415

16+
import { AlertNetwork } from './AlertNetwork';
17+
import { AlertPublic } from './AlertPublic';
1518
import { DocTitle } from './DocTitle';
1619
import { DocToolBox } from './DocToolBox';
1720

@@ -20,51 +23,26 @@ interface DocHeaderProps {
2023
}
2124

2225
export const DocHeader = ({ doc }: DocHeaderProps) => {
23-
const { colorsTokens, spacingsTokens } = useCunninghamTheme();
26+
const { spacingsTokens } = useCunninghamTheme();
2427
const { isDesktop } = useResponsiveStore();
25-
2628
const { t } = useTranslation();
29+
const { transRole } = useTrans();
30+
const { isEditable } = useIsCollaborativeEditable(doc);
2731
const docIsPublic = doc.link_reach === LinkReach.PUBLIC;
2832
const docIsAuth = doc.link_reach === LinkReach.AUTHENTICATED;
2933

30-
const { transRole } = useTrans();
31-
3234
return (
3335
<>
3436
<Box
3537
$width="100%"
36-
$padding={{ top: isDesktop ? '4xl' : 'md' }}
38+
$padding={{ top: isDesktop ? '50px' : 'md' }}
3739
$gap={spacingsTokens['base']}
3840
aria-label={t('It is the card information about the document.')}
3941
className="--docs--doc-header"
4042
>
43+
{!isEditable && <AlertNetwork />}
4144
{(docIsPublic || docIsAuth) && (
42-
<Box
43-
aria-label={t('Public document')}
44-
$color={colorsTokens['primary-800']}
45-
$background={colorsTokens['primary-050']}
46-
$radius={spacingsTokens['3xs']}
47-
$direction="row"
48-
$padding="xs"
49-
$flex={1}
50-
$align="center"
51-
$gap={spacingsTokens['3xs']}
52-
$css={css`
53-
border: 1px solid var(--c--theme--colors--primary-300, #e3e3fd);
54-
`}
55-
>
56-
<Icon
57-
$theme="primary"
58-
$variation="800"
59-
data-testid="public-icon"
60-
iconName={docIsPublic ? 'public' : 'vpn_lock'}
61-
/>
62-
<Text $theme="primary" $variation="800">
63-
{docIsPublic
64-
? t('Public document')
65-
: t('Document accessible to any connected person')}
66-
</Text>
67-
</Box>
45+
<AlertPublic isPublicDoc={docIsPublic} />
6846
)}
6947
<Box
7048
$direction="row"
@@ -86,8 +64,18 @@ export const DocHeader = ({ doc }: DocHeaderProps) => {
8664
<Box $direction="row">
8765
{isDesktop && (
8866
<>
89-
<Text $variation="600" $size="s" $weight="bold">
90-
{transRole(currentDocRole(doc.abilities))}&nbsp;·&nbsp;
67+
<Text
68+
$variation="600"
69+
$size="s"
70+
$weight="bold"
71+
$theme={isEditable ? 'greyscale' : 'warning'}
72+
>
73+
{transRole(
74+
isEditable
75+
? currentDocRole(doc.abilities)
76+
: Role.READER,
77+
)}
78+
&nbsp;·&nbsp;
9179
</Text>
9280
<Text $variation="600" $size="s">
9381
{t('Last update: {{update}}', {
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export * from './useCollaboration';
2-
export * from './useTrans';
32
export * from './useCopyDocLink';
3+
export * from './useIsCollaborativeEditable';
4+
export * from './useTrans';

0 commit comments

Comments
 (0)