Skip to content

Commit a668728

Browse files
✨(frontend) enhance document sharing and visibility features
- Added a new component `DocInheritedShareContent` to display inherited access information for documents. - Updated `DocShareModal` to include inherited share content when applicable. - Refactored `DocRoleDropdown` to improve role selection messaging based on inherited roles. - Enhanced `DocVisibility` to manage link reach and role updates more effectively, including handling desynchronization scenarios. - Improved `DocShareMemberItem` to accommodate inherited access logic and ensure proper role management.
1 parent 498331e commit a668728

File tree

13 files changed

+538
-101
lines changed

13 files changed

+538
-101
lines changed

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,9 @@ export const DropdownMenu = ({
9999
$size="xs"
100100
$weight="bold"
101101
$padding={{ vertical: 'xs', horizontal: 'base' }}
102+
$css={css`
103+
white-space: pre-line;
104+
`}
102105
>
103106
{topMessage}
104107
</Text>

src/frontend/apps/impress/src/components/quick-search/QuickSearchStyle.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,7 @@ export const QuickSearchStyle = createGlobalStyle`
6565
6666
[cmdk-list] {
6767
68-
padding: 0 var(--c--theme--spacings--base) var(--c--theme--spacings--base)
69-
var(--c--theme--spacings--base);
70-
68+
7169
flex:1;
7270
overflow-y: auto;
7371
overscroll-behavior: contain;

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
Doc,
99
LinkReach,
1010
currentDocRole,
11+
getDocLinkReach,
1112
useTrans,
1213
} from '@/docs/doc-management';
1314
import { useResponsiveStore } from '@/stores';
@@ -24,8 +25,8 @@ export const DocHeader = ({ doc }: DocHeaderProps) => {
2425
const { isDesktop } = useResponsiveStore();
2526

2627
const { t } = useTranslation();
27-
const docIsPublic = doc.link_reach === LinkReach.PUBLIC;
28-
const docIsAuth = doc.link_reach === LinkReach.AUTHENTICATED;
28+
const docIsPublic = getDocLinkReach(doc) === LinkReach.PUBLIC;
29+
const docIsAuth = getDocLinkReach(doc) === LinkReach.AUTHENTICATED;
2930

3031
const { transRole } = useTrans();
3132

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

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1+
import { useTreeContext } from '@gouvfr-lasuite/ui-kit';
12
import { Button, useModal } from '@openfun/cunningham-react';
23
import { useQueryClient } from '@tanstack/react-query';
34
import dynamic from 'next/dynamic';
4-
import { useEffect } from 'react';
5+
import { useEffect, useMemo } from 'react';
56
import { useTranslation } from 'react-i18next';
67
import { css } from 'styled-components';
78

@@ -23,7 +24,20 @@ const DocToolBoxLicence = dynamic(() =>
2324

2425
export const DocToolBox = ({ doc }: DocToolBoxProps) => {
2526
const { t } = useTranslation();
26-
const hasAccesses = doc.nb_accesses_direct > 1 && doc.abilities.accesses_view;
27+
const treeContext = useTreeContext<Doc>();
28+
29+
/**
30+
* Following the change where there is no default owner when adding a sub-page,
31+
* we need to handle both the case where the doc is the root and the case of sub-pages.
32+
*/
33+
const hasAccesses = useMemo(() => {
34+
if (treeContext?.root?.id === doc.id) {
35+
return doc.nb_accesses_direct > 1 && doc.abilities.accesses_view;
36+
}
37+
38+
return doc.nb_accesses_direct >= 1 && doc.abilities.accesses_view;
39+
}, [doc, treeContext?.root]);
40+
2741
const queryClient = useQueryClient();
2842

2943
const { spacingsTokens } = useCunninghamTheme();
Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
import { Button, Modal, ModalSize, useModal } from '@openfun/cunningham-react';
2+
import { Fragment, useMemo } from 'react';
3+
import { useTranslation } from 'react-i18next';
4+
import { createGlobalStyle } from 'styled-components';
5+
6+
import { Box, StyledLink, Text } from '@/components';
7+
import { useCunninghamTheme } from '@/cunningham';
8+
9+
import {
10+
Access,
11+
RoleImportance,
12+
useDoc,
13+
useDocStore,
14+
} from '../../doc-management';
15+
import SimpleFileIcon from '../../docs-grid/assets/simple-document.svg';
16+
17+
import { DocShareMemberItem } from './DocShareMemberItem';
18+
const ShareModalStyle = createGlobalStyle`
19+
.c__modal__title {
20+
padding-bottom: 0 !important;
21+
}
22+
.c__modal__scroller {
23+
padding: 15px 15px !important;
24+
}
25+
`;
26+
27+
type Props = {
28+
rawAccesses: Access[];
29+
};
30+
31+
const getMaxRoleBetweenAccesses = (access1: Access, access2: Access) => {
32+
const role1 = access1.max_role;
33+
const role2 = access2.max_role;
34+
35+
const roleImportance1 = RoleImportance[role1];
36+
const roleImportance2 = RoleImportance[role2];
37+
38+
return roleImportance1 > roleImportance2 ? role1 : role2;
39+
};
40+
41+
export const DocInheritedShareContent = ({ rawAccesses }: Props) => {
42+
const { t } = useTranslation();
43+
const { spacingsTokens } = useCunninghamTheme();
44+
const { currentDoc } = useDocStore();
45+
46+
const inheritedData = useMemo(() => {
47+
if (!currentDoc || rawAccesses.length === 0) {
48+
return null;
49+
}
50+
51+
let parentId = null;
52+
let parentPathLength = 0;
53+
const members: Access[] = [];
54+
55+
// Find the parent document with the longest path that is different from currentDoc
56+
for (const access of rawAccesses) {
57+
const docPath = access.document.path;
58+
59+
// Skip if it's the current document
60+
if (access.document.id === currentDoc.id) {
61+
continue;
62+
}
63+
64+
const findIndex = members.findIndex(
65+
(member) => member.user.id === access.user.id,
66+
);
67+
if (findIndex === -1) {
68+
members.push(access);
69+
} else {
70+
const accessToUpdate = members[findIndex];
71+
const currentRole = accessToUpdate.max_role;
72+
const maxRole = getMaxRoleBetweenAccesses(accessToUpdate, access);
73+
74+
if (maxRole !== currentRole) {
75+
members[findIndex] = access;
76+
}
77+
}
78+
79+
// Check if this document has a longer path than our current candidate
80+
if (docPath && (!parentId || docPath.length > parentPathLength)) {
81+
parentId = access.document.id;
82+
parentPathLength = docPath.length;
83+
}
84+
}
85+
86+
return { parentId, members };
87+
}, [currentDoc, rawAccesses]);
88+
89+
// Check if accesses map is empty
90+
const hasAccesses = rawAccesses.length > 0;
91+
92+
if (!hasAccesses) {
93+
return null;
94+
}
95+
96+
return (
97+
<Box $gap={spacingsTokens.sm}>
98+
<Box
99+
$gap={spacingsTokens.sm}
100+
$padding={{
101+
horizontal: spacingsTokens.base,
102+
vertical: spacingsTokens.sm,
103+
bottom: '0px',
104+
}}
105+
>
106+
<Text $variation="1000" $weight="bold" $size="sm">
107+
{t('Inherited share')}
108+
</Text>
109+
110+
{inheritedData && (
111+
<DocInheritedShareContentItem
112+
key={inheritedData?.parentId}
113+
accesses={inheritedData?.members ?? []}
114+
document_id={inheritedData?.parentId ?? ''}
115+
/>
116+
)}
117+
</Box>
118+
</Box>
119+
);
120+
};
121+
122+
type DocInheritedShareContentItemProps = {
123+
accesses: Access[];
124+
document_id: string;
125+
};
126+
export const DocInheritedShareContentItem = ({
127+
accesses,
128+
document_id,
129+
}: DocInheritedShareContentItemProps) => {
130+
const { t } = useTranslation();
131+
const { spacingsTokens } = useCunninghamTheme();
132+
const { data: doc, error, isLoading } = useDoc({ id: document_id });
133+
const errorCode = error?.status;
134+
135+
const accessModal = useModal();
136+
if ((!doc && !isLoading && !error) || (error && errorCode !== 403)) {
137+
return null;
138+
}
139+
140+
return (
141+
<>
142+
<Box
143+
$gap={spacingsTokens.sm}
144+
$width="100%"
145+
$direction="row"
146+
$align="center"
147+
$margin={{ bottom: spacingsTokens.sm }}
148+
$justify="space-between"
149+
>
150+
<Box $direction="row" $align="center" $gap={spacingsTokens.sm}>
151+
<SimpleFileIcon />
152+
<Box>
153+
{isLoading ? (
154+
<Box $direction="column" $gap="2px">
155+
<Box className="skeleton" $width="150px" $height="20px" />
156+
<Box className="skeleton" $width="200px" $height="17px" />
157+
</Box>
158+
) : (
159+
<>
160+
<StyledLink href={`/docs/${doc?.id}`}>
161+
<Text $variation="1000" $weight="bold" $size="sm">
162+
{error && errorCode === 403
163+
? t('You do not have permission to view this document')
164+
: (doc?.title ?? t('Untitled document'))}
165+
</Text>
166+
</StyledLink>
167+
<Text $variation="600" $weight="400" $size="xs">
168+
{t('Members of this page have access')}
169+
</Text>
170+
</>
171+
)}
172+
</Box>
173+
</Box>
174+
{!isLoading && (
175+
<Button color="primary-text" size="small" onClick={accessModal.open}>
176+
{t('See access')}
177+
</Button>
178+
)}
179+
</Box>
180+
{accessModal.isOpen && (
181+
<Modal
182+
isOpen
183+
closeOnClickOutside
184+
onClose={accessModal.close}
185+
title={
186+
<Box $align="flex-start">
187+
<Text $variation="1000" $weight="bold" $size="sm">
188+
{t('Access inherited from the parent page')}
189+
</Text>
190+
</Box>
191+
}
192+
size={ModalSize.MEDIUM}
193+
>
194+
<ShareModalStyle />
195+
<Box $padding={{ top: spacingsTokens.sm }}>
196+
{accesses.map((access) => (
197+
<Fragment key={access.id}>
198+
<DocShareMemberItem doc={doc} access={access} isInherited />
199+
</Fragment>
200+
))}
201+
</Box>
202+
</Modal>
203+
)}
204+
</>
205+
);
206+
};

src/frontend/apps/impress/src/features/docs/doc-share/components/DocRoleDropdown.tsx

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { useMemo } from 'react';
2+
import { useTranslation } from 'react-i18next';
13
import { css } from 'styled-components';
24

35
import { DropdownMenu, DropdownMenuOption, Text } from '@/components';
@@ -18,30 +20,48 @@ export const DocRoleDropdown = ({
1820
onSelectRole,
1921
rolesAllowed,
2022
}: DocRoleDropdownProps) => {
23+
const { t } = useTranslation();
2124
const { transRole, translatedRoles } = useTrans();
2225

23-
if (!canUpdate) {
24-
return (
25-
<Text aria-label="doc-role-text" $variation="600">
26-
{transRole(currentRole)}
27-
</Text>
28-
);
29-
}
26+
/**
27+
* When there is a higher role, the rolesAllowed are truncated
28+
* We display a message to indicate that there is a higher role
29+
*/
30+
const topMessage = useMemo(() => {
31+
if (!canUpdate || !rolesAllowed || rolesAllowed.length === 0) {
32+
return message;
33+
}
34+
35+
const allRoles = Object.keys(translatedRoles);
36+
37+
if (rolesAllowed.length < allRoles.length) {
38+
let result = message ? `${message}\n\n` : '';
39+
result += t('This user has access inherited from a parent page.');
40+
return result;
41+
}
42+
43+
return message;
44+
}, [canUpdate, rolesAllowed, translatedRoles, message, t]);
3045

3146
const roles: DropdownMenuOption[] = Object.keys(translatedRoles).map(
3247
(key) => {
3348
return {
3449
label: transRole(key as Role),
3550
callback: () => onSelectRole?.(key as Role),
36-
disabled: rolesAllowed && !rolesAllowed.includes(key as Role),
3751
isSelected: currentRole === (key as Role),
3852
};
3953
},
4054
);
41-
55+
if (!canUpdate) {
56+
return (
57+
<Text aria-label="doc-role-text" $variation="600">
58+
{transRole(currentRole)}
59+
</Text>
60+
);
61+
}
4262
return (
4363
<DropdownMenu
44-
topMessage={message}
64+
topMessage={topMessage}
4565
label="doc-role-dropdown"
4666
showArrow={true}
4767
options={roles}

0 commit comments

Comments
 (0)