Skip to content

Commit 9371910

Browse files
wip inherited access
1 parent 75dc214 commit 9371910

File tree

13 files changed

+224
-48
lines changed

13 files changed

+224
-48
lines changed

src/backend/core/migrations/0021_remove_document_is_public_and_more.py renamed to src/backend/core/migrations/0022_remove_document_is_public_and_more.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
class Migration(migrations.Migration):
77
dependencies = [
8-
("core", "0020_remove_is_public_add_field_attachments_and_duplicated_from"),
8+
("core", "0021_activate_unaccent_extension"),
99
]
1010

1111
operations = [

src/frontend/apps/impress/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
"@dnd-kit/modifiers": "9.0.0",
2626
"@fontsource/material-icons": "5.2.5",
2727
"@gouvfr-lasuite/integration": "1.0.2",
28-
"@gouvfr-lasuite/ui-kit": "0.1.10",
28+
"@gouvfr-lasuite/ui-kit": "0.4.0",
2929
"@hocuspocus/provider": "2.15.2",
3030
"@openfun/cunningham-react": "3.0.0",
3131
"@react-pdf/renderer": "4.1.6",

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +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);
68+
7069
7170
flex:1;
7271
overflow-y: auto;

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
193193
}}
194194
size={isSmallMobile ? 'small' : 'medium'}
195195
style={{
196-
color: colors['primary-800'],
196+
color: colorsTokens['primary-800'],
197197
}}
198198
>
199199
{t('Share')}

src/frontend/apps/impress/src/features/docs/doc-management/types.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export interface Access {
55
role: Role;
66
team: string;
77
user: User;
8+
document_id: string;
89
abilities: {
910
destroy: boolean;
1011
partial_update: boolean;
@@ -73,9 +74,16 @@ export interface Doc {
7374
versions_destroy: boolean;
7475
versions_list: boolean;
7576
versions_retrieve: boolean;
77+
link_select_options: LinkSelectOption;
7678
};
7779
}
7880

81+
export interface LinkSelectOption {
82+
public?: LinkRole[];
83+
authenticated?: LinkRole[];
84+
restricted?: LinkRole[];
85+
}
86+
7987
export enum DocDefaultFilter {
8088
ALL_DOCS = 'all_docs',
8189
MY_DOCS = 'my_docs',

src/frontend/apps/impress/src/features/docs/doc-share/api/useDocAccesses.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export type DocAccessesParam = {
1616
};
1717

1818
export type DocAccessesAPIParams = DocAccessesParam & {
19-
page: number;
19+
page?: number;
2020
};
2121

2222
type AccessesResponse = APIList<Access>;
@@ -25,7 +25,7 @@ export const getDocAccesses = async ({
2525
page,
2626
docId,
2727
ordering,
28-
}: DocAccessesAPIParams): Promise<AccessesResponse> => {
28+
}: DocAccessesAPIParams): Promise<Access[]> => {
2929
let url = `documents/${docId}/accesses/?page=${page}`;
3030

3131
if (ordering) {
@@ -41,16 +41,16 @@ export const getDocAccesses = async ({
4141
);
4242
}
4343

44-
return response.json() as Promise<AccessesResponse>;
44+
return (await response.json()) as Access[];
4545
};
4646

4747
export const KEY_LIST_DOC_ACCESSES = 'docs-accesses';
4848

4949
export function useDocAccesses(
5050
params: DocAccessesAPIParams,
51-
queryConfig?: UseQueryOptions<AccessesResponse, APIError, AccessesResponse>,
51+
queryConfig?: UseQueryOptions<Access[], APIError, Access[]>,
5252
) {
53-
return useQuery<AccessesResponse, APIError, AccessesResponse>({
53+
return useQuery<Access[], APIError, Access[]>({
5454
queryKey: [KEY_LIST_DOC_ACCESSES, params],
5555
queryFn: () => getDocAccesses(params),
5656
...queryConfig,
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import { Button, Modal, ModalSize, useModal } from '@openfun/cunningham-react';
2+
import { useTranslation } from 'react-i18next';
3+
import { createGlobalStyle } from 'styled-components';
4+
5+
import { Box, HorizontalSeparator, Text } from '@/components';
6+
import { useCunninghamTheme } from '@/cunningham';
7+
8+
import { Access, useDoc } from '../../doc-management';
9+
import SimpleFileIcon from '../../docs-grid/assets/simple-document.svg';
10+
11+
import { DocShareMemberItem } from './DocShareMemberItem';
12+
const ShareModalStyle = createGlobalStyle`
13+
.c__modal__title {
14+
padding-bottom: 0 !important;
15+
}
16+
.c__modal__scroller {
17+
padding: 15px 15px !important;
18+
}
19+
`;
20+
21+
type Props = {
22+
accesses: Map<string, Access[]>;
23+
};
24+
25+
export const DocInheritedShareContent = ({ accesses }: Props) => {
26+
const { t } = useTranslation();
27+
const { spacingsTokens } = useCunninghamTheme();
28+
29+
// Check if accesses map is empty
30+
const hasAccesses = accesses.size > 0;
31+
32+
if (!hasAccesses) {
33+
return null;
34+
}
35+
return (
36+
<Box $gap={spacingsTokens.sm}>
37+
<Box
38+
$gap={spacingsTokens.sm}
39+
$padding={{
40+
horizontal: spacingsTokens.base,
41+
vertical: spacingsTokens.sm,
42+
bottom: '0px',
43+
}}
44+
>
45+
<Text $variation="1000" $weight="bold" $size="sm">
46+
{t('Inherited share')}
47+
</Text>
48+
49+
{Array.from(accesses.keys()).map((documentId) => (
50+
<DocInheritedShareContentItem
51+
key={documentId}
52+
accesses={accesses.get(documentId) ?? []}
53+
document_id={documentId}
54+
/>
55+
))}
56+
</Box>
57+
<HorizontalSeparator $withPadding={false} />
58+
</Box>
59+
);
60+
};
61+
62+
type DocInheritedShareContentItemProps = {
63+
accesses: Access[];
64+
document_id: string;
65+
};
66+
export const DocInheritedShareContentItem = ({
67+
accesses,
68+
document_id,
69+
}: DocInheritedShareContentItemProps) => {
70+
const { t } = useTranslation();
71+
const { spacingsTokens } = useCunninghamTheme();
72+
const { data: doc } = useDoc({ id: document_id });
73+
const accessModal = useModal();
74+
console.log('doc', doc);
75+
76+
if (!doc) {
77+
return null;
78+
}
79+
return (
80+
<>
81+
<Box
82+
$gap={spacingsTokens.sm}
83+
$width="100%"
84+
$direction="row"
85+
$align="center"
86+
$justify="space-between"
87+
>
88+
<Box $direction="row" $align="center" $gap={spacingsTokens.sm}>
89+
<SimpleFileIcon />
90+
<Box>
91+
<Text $variation="1000" $weight="bold" $size="sm">
92+
{doc.title ?? t('Untitled document')}
93+
</Text>
94+
<Text $variation="600" $weight="400" $size="xs">
95+
{t('Members of this page have access')}
96+
</Text>
97+
</Box>
98+
</Box>
99+
<Button color="primary-text" size="small" onClick={accessModal.open}>
100+
{t('See access')}
101+
</Button>
102+
</Box>
103+
{accessModal.isOpen && (
104+
<Modal
105+
isOpen
106+
closeOnClickOutside
107+
onClose={accessModal.close}
108+
title={
109+
<Box $align="flex-start">
110+
<Text $variation="1000" $weight="bold" $size="sm">
111+
{t('Access inherited from the parent page')}
112+
</Text>
113+
</Box>
114+
}
115+
size={ModalSize.MEDIUM}
116+
>
117+
<ShareModalStyle />
118+
<Box $padding={{ top: spacingsTokens.sm }}>
119+
{accesses.map((access) => (
120+
<DocShareMemberItem
121+
key={access.id}
122+
doc={doc}
123+
access={access}
124+
isInherited
125+
/>
126+
))}
127+
</Box>
128+
</Modal>
129+
)}
130+
</>
131+
);
132+
};

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

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,13 @@ import { SearchUserRow } from './SearchUserRow';
2121
type Props = {
2222
doc: Doc;
2323
access: Access;
24+
isInherited?: boolean;
2425
};
25-
export const DocShareMemberItem = ({ doc, access }: Props) => {
26+
export const DocShareMemberItem = ({
27+
doc,
28+
access,
29+
isInherited = false,
30+
}: Props) => {
2631
const { t } = useTranslation();
2732
const queryClient = useQueryClient();
2833
const { isLastOwner, isOtherOwner } = useWhoAmI(access);
@@ -80,6 +85,8 @@ export const DocShareMemberItem = ({ doc, access }: Props) => {
8085
},
8186
];
8287

88+
const canUpdate = isInherited ? false : doc.abilities.accesses_manage;
89+
8390
return (
8491
<Box
8592
$width="100%"
@@ -94,12 +101,12 @@ export const DocShareMemberItem = ({ doc, access }: Props) => {
94101
<DocRoleDropdown
95102
currentRole={access.role}
96103
onSelectRole={onUpdate}
97-
canUpdate={doc.abilities.accesses_manage}
104+
canUpdate={canUpdate}
98105
isLastOwner={isLastOwner}
99106
isOtherOwner={!!isOtherOwner}
100107
/>
101108

102-
{isDesktop && doc.abilities.accesses_manage && (
109+
{isDesktop && canUpdate && (
103110
<DropdownMenu options={moreActions}>
104111
<IconOptions
105112
isHorizontal

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

Lines changed: 39 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,13 @@ import { isValidEmail } from '@/utils';
1717

1818
import {
1919
KEY_LIST_USER,
20-
useDocAccessesInfinite,
20+
useDocAccesses,
2121
useDocInvitationsInfinite,
2222
useUsers,
2323
} from '../api';
2424
import { Invitation } from '../types';
2525

26+
import { DocInheritedShareContent } from './DocInheritedShareContent';
2627
import { DocShareAddMemberList } from './DocShareAddMemberList';
2728
import { DocShareInvitationItem } from './DocShareInvitationItem';
2829
import { DocShareMemberItem } from './DocShareMemberItem';
@@ -66,7 +67,7 @@ export const DocShareModal = ({ doc, onClose }: Props) => {
6667
setInputValue('');
6768
};
6869

69-
const membersQuery = useDocAccessesInfinite({
70+
const { data: membersQuery } = useDocAccesses({
7071
docId: doc.id,
7172
});
7273

@@ -82,11 +83,32 @@ export const DocShareModal = ({ doc, onClose }: Props) => {
8283
},
8384
);
8485

86+
const accessesFromTopLevel = useMemo(() => {
87+
const accesses = membersQuery?.filter(
88+
(access) => access.document_id !== doc.id,
89+
);
90+
// Group accesses by document_id
91+
const accessesByDocumentId = new Map<string, Access[]>();
92+
93+
if (accesses) {
94+
for (const access of accesses) {
95+
if (!accessesByDocumentId.has(access.document_id)) {
96+
accessesByDocumentId.set(access.document_id, []);
97+
}
98+
accessesByDocumentId.get(access.document_id)?.push(access);
99+
}
100+
}
101+
102+
return accessesByDocumentId;
103+
}, [membersQuery, doc.id]);
104+
105+
console.log('accessesFromTopLevel', accessesFromTopLevel);
106+
85107
const membersData: QuickSearchData<Access> = useMemo(() => {
86-
const members =
87-
membersQuery.data?.pages.flatMap((page) => page.results) || [];
108+
const members: Access[] =
109+
membersQuery?.filter((access) => access.document_id === doc.id) ?? [];
88110

89-
const count = membersQuery.data?.pages[0]?.count ?? 1;
111+
const count = membersQuery?.length ?? 1;
90112

91113
return {
92114
groupName:
@@ -96,16 +118,8 @@ export const DocShareModal = ({ doc, onClose }: Props) => {
96118
count: count,
97119
}),
98120
elements: members,
99-
endActions: membersQuery.hasNextPage
100-
? [
101-
{
102-
content: <LoadMoreText data-testid="load-more-members" />,
103-
onSelect: () => void membersQuery.fetchNextPage(),
104-
},
105-
]
106-
: undefined,
107121
};
108-
}, [membersQuery, t]);
122+
}, [membersQuery, doc.id, t]);
109123

110124
const invitationsData: QuickSearchData<Invitation> = useMemo(() => {
111125
const invitations =
@@ -254,8 +268,17 @@ export const DocShareModal = ({ doc, onClose }: Props) => {
254268
loading={searchUsersQuery.isLoading}
255269
placeholder={t('Type a name or email')}
256270
>
271+
{accessesFromTopLevel && (
272+
<DocInheritedShareContent accesses={accessesFromTopLevel} />
273+
)}
257274
{canViewAccesses && (
258-
<>
275+
<Box
276+
$padding={{
277+
horizontal: 'base',
278+
vertical: 'base',
279+
top: '0px',
280+
}}
281+
>
259282
{!showMemberSection && inputValue !== '' && (
260283
<QuickSearchGroup
261284
group={searchUserData}
@@ -291,7 +314,7 @@ export const DocShareModal = ({ doc, onClose }: Props) => {
291314
</Box>
292315
</>
293316
)}
294-
</>
317+
</Box>
295318
)}
296319
</QuickSearch>
297320
)}

0 commit comments

Comments
 (0)