From 11dfc9ff035ff219b5e718441bcca06a463d27fc Mon Sep 17 00:00:00 2001 From: Anthony LC Date: Mon, 28 Jul 2025 17:38:27 +0200 Subject: [PATCH 1/2] =?UTF-8?q?=E2=9C=A8(backend)=20add=20is=5Fmasked=20to?= =?UTF-8?q?=20document=20view?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We added the `is_masked` annotation to the document view to indicate whether a document is masked for the current user. This will allow the frontend to handle masked documents appropriately in the UI. --- src/backend/core/api/serializers.py | 4 ++++ src/backend/core/api/viewsets.py | 4 +++- src/backend/core/models.py | 12 +++++++++++ .../test_api_documents_children_list.py | 14 +++++++++++++ .../test_api_documents_descendants.py | 21 +++++++++++++++++++ .../documents/test_api_documents_list.py | 4 ++++ 6 files changed, 58 insertions(+), 1 deletion(-) diff --git a/src/backend/core/api/serializers.py b/src/backend/core/api/serializers.py index 83afc260d9..b8fb80b73b 100644 --- a/src/backend/core/api/serializers.py +++ b/src/backend/core/api/serializers.py @@ -66,6 +66,7 @@ class ListDocumentSerializer(serializers.ModelSerializer): """Serialize documents with limited fields for display in lists.""" is_favorite = serializers.BooleanField(read_only=True) + is_masked = serializers.BooleanField(read_only=True) nb_accesses_ancestors = serializers.IntegerField(read_only=True) nb_accesses_direct = serializers.IntegerField(read_only=True) user_role = serializers.SerializerMethodField(read_only=True) @@ -85,6 +86,7 @@ class Meta: "depth", "excerpt", "is_favorite", + "is_masked", "link_role", "link_reach", "nb_accesses_ancestors", @@ -107,6 +109,7 @@ class Meta: "depth", "excerpt", "is_favorite", + "is_masked", "link_role", "link_reach", "nb_accesses_ancestors", @@ -176,6 +179,7 @@ class Meta: "depth", "excerpt", "is_favorite", + "is_masked", "link_role", "link_reach", "nb_accesses_ancestors", diff --git a/src/backend/core/api/viewsets.py b/src/backend/core/api/viewsets.py index ee0c594eb1..918c483178 100644 --- a/src/backend/core/api/viewsets.py +++ b/src/backend/core/api/viewsets.py @@ -405,6 +405,7 @@ def filter_queryset(self, queryset): queryset = super().filter_queryset(queryset) user = self.request.user queryset = queryset.annotate_is_favorite(user) + queryset = queryset.annotate_is_masked(user) queryset = queryset.annotate_user_roles(user) return queryset @@ -453,8 +454,9 @@ def list(self, request, *args, **kwargs): ) queryset = queryset.filter(path__in=root_paths) - # Annotate favorite status and filter if applicable as late as possible + # Annotate favorite and masked status and filter if applicable as late as possible queryset = queryset.annotate_is_favorite(user) + queryset = queryset.annotate_is_masked(user) for field in ["is_favorite", "is_masked"]: queryset = filterset.filters[field].filter(queryset, filter_data[field]) diff --git a/src/backend/core/models.py b/src/backend/core/models.py index a1182964da..d4c02d270b 100644 --- a/src/backend/core/models.py +++ b/src/backend/core/models.py @@ -326,6 +326,18 @@ def annotate_is_favorite(self, user): return self.annotate(is_favorite=models.Value(False)) + def annotate_is_masked(self, user): + """ + Annotate document queryset with the masked status for the current user. + """ + if user.is_authenticated: + masked_exists_subquery = LinkTrace.objects.filter( + document_id=models.OuterRef("pk"), user=user, is_masked=True + ) + return self.annotate(is_masked=models.Exists(masked_exists_subquery)) + + return self.annotate(is_masked=models.Value(False)) + def annotate_user_roles(self, user): """ Annotate document queryset with the roles of the current user diff --git a/src/backend/core/tests/documents/test_api_documents_children_list.py b/src/backend/core/tests/documents/test_api_documents_children_list.py index 19bcfd1920..20098d04d9 100644 --- a/src/backend/core/tests/documents/test_api_documents_children_list.py +++ b/src/backend/core/tests/documents/test_api_documents_children_list.py @@ -45,6 +45,7 @@ def test_api_documents_children_list_anonymous_public_standalone( "excerpt": child1.excerpt, "id": str(child1.id), "is_favorite": False, + "is_masked": False, "link_reach": child1.link_reach, "link_role": child1.link_role, "numchild": 0, @@ -67,6 +68,7 @@ def test_api_documents_children_list_anonymous_public_standalone( "excerpt": child2.excerpt, "id": str(child2.id), "is_favorite": False, + "is_masked": False, "link_reach": child2.link_reach, "link_role": child2.link_role, "numchild": 0, @@ -119,6 +121,7 @@ def test_api_documents_children_list_anonymous_public_parent(django_assert_num_q "excerpt": child1.excerpt, "id": str(child1.id), "is_favorite": False, + "is_masked": False, "link_reach": child1.link_reach, "link_role": child1.link_role, "numchild": 0, @@ -141,6 +144,7 @@ def test_api_documents_children_list_anonymous_public_parent(django_assert_num_q "excerpt": child2.excerpt, "id": str(child2.id), "is_favorite": False, + "is_masked": False, "link_reach": child2.link_reach, "link_role": child2.link_role, "numchild": 0, @@ -212,6 +216,7 @@ def test_api_documents_children_list_authenticated_unrelated_public_or_authentic "excerpt": child1.excerpt, "id": str(child1.id), "is_favorite": False, + "is_masked": False, "link_reach": child1.link_reach, "link_role": child1.link_role, "numchild": 0, @@ -234,6 +239,7 @@ def test_api_documents_children_list_authenticated_unrelated_public_or_authentic "excerpt": child2.excerpt, "id": str(child2.id), "is_favorite": False, + "is_masked": False, "link_reach": child2.link_reach, "link_role": child2.link_role, "numchild": 0, @@ -291,6 +297,7 @@ def test_api_documents_children_list_authenticated_public_or_authenticated_paren "excerpt": child1.excerpt, "id": str(child1.id), "is_favorite": False, + "is_masked": False, "link_reach": child1.link_reach, "link_role": child1.link_role, "numchild": 0, @@ -313,6 +320,7 @@ def test_api_documents_children_list_authenticated_public_or_authenticated_paren "excerpt": child2.excerpt, "id": str(child2.id), "is_favorite": False, + "is_masked": False, "link_reach": child2.link_reach, "link_role": child2.link_role, "numchild": 0, @@ -397,6 +405,7 @@ def test_api_documents_children_list_authenticated_related_direct( "excerpt": child1.excerpt, "id": str(child1.id), "is_favorite": False, + "is_masked": False, "link_reach": child1.link_reach, "link_role": child1.link_role, "numchild": 0, @@ -419,6 +428,7 @@ def test_api_documents_children_list_authenticated_related_direct( "excerpt": child2.excerpt, "id": str(child2.id), "is_favorite": False, + "is_masked": False, "link_reach": child2.link_reach, "link_role": child2.link_role, "numchild": 0, @@ -479,6 +489,7 @@ def test_api_documents_children_list_authenticated_related_parent( "excerpt": child1.excerpt, "id": str(child1.id), "is_favorite": False, + "is_masked": False, "link_reach": child1.link_reach, "link_role": child1.link_role, "numchild": 0, @@ -501,6 +512,7 @@ def test_api_documents_children_list_authenticated_related_parent( "excerpt": child2.excerpt, "id": str(child2.id), "is_favorite": False, + "is_masked": False, "link_reach": child2.link_reach, "link_role": child2.link_role, "numchild": 0, @@ -613,6 +625,7 @@ def test_api_documents_children_list_authenticated_related_team_members( "excerpt": child1.excerpt, "id": str(child1.id), "is_favorite": False, + "is_masked": False, "link_reach": child1.link_reach, "link_role": child1.link_role, "numchild": 0, @@ -635,6 +648,7 @@ def test_api_documents_children_list_authenticated_related_team_members( "excerpt": child2.excerpt, "id": str(child2.id), "is_favorite": False, + "is_masked": False, "link_reach": child2.link_reach, "link_role": child2.link_role, "numchild": 0, diff --git a/src/backend/core/tests/documents/test_api_documents_descendants.py b/src/backend/core/tests/documents/test_api_documents_descendants.py index bd2785a7f6..4fa08f68e2 100644 --- a/src/backend/core/tests/documents/test_api_documents_descendants.py +++ b/src/backend/core/tests/documents/test_api_documents_descendants.py @@ -42,6 +42,7 @@ def test_api_documents_descendants_list_anonymous_public_standalone(): "excerpt": child1.excerpt, "id": str(child1.id), "is_favorite": False, + "is_masked": False, "link_reach": child1.link_reach, "link_role": child1.link_role, "numchild": 1, @@ -66,6 +67,7 @@ def test_api_documents_descendants_list_anonymous_public_standalone(): "excerpt": grand_child.excerpt, "id": str(grand_child.id), "is_favorite": False, + "is_masked": False, "link_reach": grand_child.link_reach, "link_role": grand_child.link_role, "numchild": 0, @@ -88,6 +90,7 @@ def test_api_documents_descendants_list_anonymous_public_standalone(): "excerpt": child2.excerpt, "id": str(child2.id), "is_favorite": False, + "is_masked": False, "link_reach": child2.link_reach, "link_role": child2.link_role, "numchild": 0, @@ -139,6 +142,7 @@ def test_api_documents_descendants_list_anonymous_public_parent(): "excerpt": child1.excerpt, "id": str(child1.id), "is_favorite": False, + "is_masked": False, "link_reach": child1.link_reach, "link_role": child1.link_role, "numchild": 1, @@ -161,6 +165,7 @@ def test_api_documents_descendants_list_anonymous_public_parent(): "excerpt": grand_child.excerpt, "id": str(grand_child.id), "is_favorite": False, + "is_masked": False, "link_reach": grand_child.link_reach, "link_role": grand_child.link_role, "numchild": 0, @@ -183,6 +188,7 @@ def test_api_documents_descendants_list_anonymous_public_parent(): "excerpt": child2.excerpt, "id": str(child2.id), "is_favorite": False, + "is_masked": False, "link_reach": child2.link_reach, "link_role": child2.link_role, "numchild": 0, @@ -255,6 +261,7 @@ def test_api_documents_descendants_list_authenticated_unrelated_public_or_authen "excerpt": child1.excerpt, "id": str(child1.id), "is_favorite": False, + "is_masked": False, "link_reach": child1.link_reach, "link_role": child1.link_role, "numchild": 1, @@ -277,6 +284,7 @@ def test_api_documents_descendants_list_authenticated_unrelated_public_or_authen "excerpt": grand_child.excerpt, "id": str(grand_child.id), "is_favorite": False, + "is_masked": False, "link_reach": grand_child.link_reach, "link_role": grand_child.link_role, "numchild": 0, @@ -299,6 +307,7 @@ def test_api_documents_descendants_list_authenticated_unrelated_public_or_authen "excerpt": child2.excerpt, "id": str(child2.id), "is_favorite": False, + "is_masked": False, "link_reach": child2.link_reach, "link_role": child2.link_role, "numchild": 0, @@ -356,6 +365,7 @@ def test_api_documents_descendants_list_authenticated_public_or_authenticated_pa "excerpt": child1.excerpt, "id": str(child1.id), "is_favorite": False, + "is_masked": False, "link_reach": child1.link_reach, "link_role": child1.link_role, "numchild": 1, @@ -378,6 +388,7 @@ def test_api_documents_descendants_list_authenticated_public_or_authenticated_pa "excerpt": grand_child.excerpt, "id": str(grand_child.id), "is_favorite": False, + "is_masked": False, "link_reach": grand_child.link_reach, "link_role": grand_child.link_role, "numchild": 0, @@ -400,6 +411,7 @@ def test_api_documents_descendants_list_authenticated_public_or_authenticated_pa "excerpt": child2.excerpt, "id": str(child2.id), "is_favorite": False, + "is_masked": False, "link_reach": child2.link_reach, "link_role": child2.link_role, "numchild": 0, @@ -478,6 +490,7 @@ def test_api_documents_descendants_list_authenticated_related_direct(): "excerpt": child1.excerpt, "id": str(child1.id), "is_favorite": False, + "is_masked": False, "link_reach": child1.link_reach, "link_role": child1.link_role, "numchild": 1, @@ -500,6 +513,7 @@ def test_api_documents_descendants_list_authenticated_related_direct(): "excerpt": grand_child.excerpt, "id": str(grand_child.id), "is_favorite": False, + "is_masked": False, "link_reach": grand_child.link_reach, "link_role": grand_child.link_role, "numchild": 0, @@ -522,6 +536,7 @@ def test_api_documents_descendants_list_authenticated_related_direct(): "excerpt": child2.excerpt, "id": str(child2.id), "is_favorite": False, + "is_masked": False, "link_reach": child2.link_reach, "link_role": child2.link_role, "numchild": 0, @@ -580,6 +595,7 @@ def test_api_documents_descendants_list_authenticated_related_parent(): "excerpt": child1.excerpt, "id": str(child1.id), "is_favorite": False, + "is_masked": False, "link_reach": child1.link_reach, "link_role": child1.link_role, "numchild": 1, @@ -602,6 +618,7 @@ def test_api_documents_descendants_list_authenticated_related_parent(): "excerpt": grand_child.excerpt, "id": str(grand_child.id), "is_favorite": False, + "is_masked": False, "link_reach": grand_child.link_reach, "link_role": grand_child.link_role, "numchild": 0, @@ -624,6 +641,7 @@ def test_api_documents_descendants_list_authenticated_related_parent(): "excerpt": child2.excerpt, "id": str(child2.id), "is_favorite": False, + "is_masked": False, "link_reach": child2.link_reach, "link_role": child2.link_role, "numchild": 0, @@ -728,6 +746,7 @@ def test_api_documents_descendants_list_authenticated_related_team_members( "excerpt": child1.excerpt, "id": str(child1.id), "is_favorite": False, + "is_masked": False, "link_reach": child1.link_reach, "link_role": child1.link_role, "numchild": 1, @@ -750,6 +769,7 @@ def test_api_documents_descendants_list_authenticated_related_team_members( "excerpt": grand_child.excerpt, "id": str(grand_child.id), "is_favorite": False, + "is_masked": False, "link_reach": grand_child.link_reach, "link_role": grand_child.link_role, "numchild": 0, @@ -772,6 +792,7 @@ def test_api_documents_descendants_list_authenticated_related_team_members( "excerpt": child2.excerpt, "id": str(child2.id), "is_favorite": False, + "is_masked": False, "link_reach": child2.link_reach, "link_role": child2.link_role, "numchild": 0, diff --git a/src/backend/core/tests/documents/test_api_documents_list.py b/src/backend/core/tests/documents/test_api_documents_list.py index cfaa3e0a1e..2fc9e85ef7 100644 --- a/src/backend/core/tests/documents/test_api_documents_list.py +++ b/src/backend/core/tests/documents/test_api_documents_list.py @@ -72,6 +72,7 @@ def test_api_documents_list_format(): "depth": 1, "excerpt": document.excerpt, "is_favorite": True, + "is_masked": False, "link_reach": document.link_reach, "link_role": document.link_role, "nb_accesses_ancestors": 3, @@ -408,6 +409,7 @@ def test_api_documents_list_favorites_no_extra_queries(django_assert_num_queries assert len(results) == 5 assert all(result["is_favorite"] is False for result in results) + assert all(result["is_masked"] is False for result in results) # Mark documents as favorite and check results again for document in special_documents: @@ -427,3 +429,5 @@ def test_api_documents_list_favorites_no_extra_queries(django_assert_num_queries assert result["is_favorite"] is True else: assert result["is_favorite"] is False + # All documents should be unmasked in this test + assert result["is_masked"] is False From 9e4e55717345eee2c2a4a5715e0d5225f5a0cf6c Mon Sep 17 00:00:00 2001 From: Anthony LC Date: Tue, 29 Jul 2025 09:20:09 +0200 Subject: [PATCH 2/2] =?UTF-8?q?=E2=9C=A8(frontend)=20Can=20mask=20a=20docu?= =?UTF-8?q?ment=20in=20the=20list=20view?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We can be member of some documents, but sometimes we want to mask them from the list view because we don't want to interact with them anymore. This commit adds the ability to mask a document in the list view. --- CHANGELOG.md | 1 + .../docs/doc-header/components/DocToolBox.tsx | 25 ++++-- .../features/docs/doc-management/api/index.ts | 3 +- .../docs/doc-management/api/useDocs.tsx | 4 + .../docs/doc-management/api/useMaskDoc.tsx | 77 +++++++++++++++++++ .../docs/doc-management/hooks/index.ts | 1 + .../doc-management/hooks/useMaskDocOption.tsx | 42 ++++++++++ .../features/docs/doc-management/types.tsx | 2 + .../docs/docs-grid/components/DocsGrid.tsx | 12 ++- .../docs-grid/components/DocsGridActions.tsx | 23 ++++-- .../service-worker/plugins/ApiPlugin.ts | 2 + 11 files changed, 172 insertions(+), 20 deletions(-) create mode 100644 src/frontend/apps/impress/src/features/docs/doc-management/api/useMaskDoc.tsx create mode 100644 src/frontend/apps/impress/src/features/docs/doc-management/hooks/useMaskDocOption.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f783d4e29..76270aece3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ and this project adheres to - ✨(frontend) subdocs can manage link reach #1190 - ✨(frontend) add duplicate action to doc tree #1175 - ✨(frontend) add multi columns support for editor #1219 +- ✨(frontend) Can mask a document from the list view #1233 ### Changed diff --git a/src/frontend/apps/impress/src/features/docs/doc-header/components/DocToolBox.tsx b/src/frontend/apps/impress/src/features/docs/doc-header/components/DocToolBox.tsx index b84321c17d..cb3075cf32 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-header/components/DocToolBox.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-header/components/DocToolBox.tsx @@ -24,6 +24,7 @@ import { useCreateFavoriteDoc, useDeleteFavoriteDoc, useDuplicateDoc, + useMaskDocOption, } from '@/docs/doc-management'; import { DocShareModal } from '@/docs/doc-share'; import { @@ -81,6 +82,7 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => { const makeFavoriteDoc = useCreateFavoriteDoc({ listInvalideQueries: [KEY_LIST_DOC, KEY_DOC], }); + const maskDocOption = useMaskDocOption(doc); useEffect(() => { if (selectHistoryModal.isOpen) { @@ -126,6 +128,7 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => { } }, testId: `docs-actions-${doc.is_favorite ? 'unpin' : 'pin'}-${doc.id}`, + showSeparator: true, }, { label: t('Version history'), @@ -162,17 +165,23 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => { canSave: doc.abilities.partial_update, }); }, - }, - { - label: t('Delete document'), - icon: 'delete', - disabled: !doc.abilities.destroy, - callback: () => { - setIsModalRemoveOpen(true); - }, + showSeparator: true, }, ]; + const leaveDocOption: DropdownMenuOption = doc.abilities.destroy + ? { + label: t('Delete document'), + icon: 'delete', + disabled: !doc.abilities.destroy, + callback: () => { + setIsModalRemoveOpen(true); + }, + } + : maskDocOption; + + options.push(leaveDocOption); + const copyCurrentEditorToClipboard = useCopyCurrentEditorToClipboard(); return ( diff --git a/src/frontend/apps/impress/src/features/docs/doc-management/api/index.ts b/src/frontend/apps/impress/src/features/docs/doc-management/api/index.ts index 3a0f3437b5..a026de2b2d 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-management/api/index.ts +++ b/src/frontend/apps/impress/src/features/docs/doc-management/api/index.ts @@ -4,7 +4,8 @@ export * from './useDeleteFavoriteDoc'; export * from './useDoc'; export * from './useDocOptions'; export * from './useDocs'; -export * from './useSubDocs'; export * from './useDuplicateDoc'; +export * from './useMaskDoc'; +export * from './useSubDocs'; export * from './useUpdateDoc'; export * from './useUpdateDocLink'; diff --git a/src/frontend/apps/impress/src/features/docs/doc-management/api/useDocs.tsx b/src/frontend/apps/impress/src/features/docs/doc-management/api/useDocs.tsx index 88f385df5f..bc587dde45 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-management/api/useDocs.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-management/api/useDocs.tsx @@ -16,6 +16,7 @@ export type DocsParams = { is_creator_me?: boolean; title?: string; is_favorite?: boolean; + is_masked?: boolean; }; export const constructParams = (params: DocsParams): URLSearchParams => { @@ -36,6 +37,9 @@ export const constructParams = (params: DocsParams): URLSearchParams => { if (params.is_favorite !== undefined) { searchParams.set('is_favorite', params.is_favorite.toString()); } + if (params.is_masked !== undefined) { + searchParams.set('is_masked', params.is_masked.toString()); + } return searchParams; }; diff --git a/src/frontend/apps/impress/src/features/docs/doc-management/api/useMaskDoc.tsx b/src/frontend/apps/impress/src/features/docs/doc-management/api/useMaskDoc.tsx new file mode 100644 index 0000000000..ea881bb3ff --- /dev/null +++ b/src/frontend/apps/impress/src/features/docs/doc-management/api/useMaskDoc.tsx @@ -0,0 +1,77 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; + +import { APIError, errorCauses, fetchAPI } from '@/api'; +import { Doc } from '@/docs/doc-management'; + +export type MaskDocParams = Pick; + +export const maskDoc = async ({ id }: MaskDocParams) => { + const response = await fetchAPI(`documents/${id}/mask/`, { + method: 'POST', + }); + + if (!response.ok) { + throw new APIError( + 'Failed to make the doc as masked', + await errorCauses(response), + ); + } +}; + +interface MaskDocProps { + onSuccess?: () => void; + listInvalideQueries?: string[]; +} + +export function useMaskDoc({ onSuccess, listInvalideQueries }: MaskDocProps) { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: maskDoc, + onSuccess: () => { + listInvalideQueries?.forEach((queryKey) => { + void queryClient.invalidateQueries({ + queryKey: [queryKey], + }); + }); + onSuccess?.(); + }, + }); +} + +export type DeleteMaskDocParams = Pick; + +export const deleteMaskDoc = async ({ id }: DeleteMaskDocParams) => { + const response = await fetchAPI(`documents/${id}/mask/`, { + method: 'DELETE', + }); + + if (!response.ok) { + throw new APIError( + 'Failed to remove the doc as masked', + await errorCauses(response), + ); + } +}; + +interface DeleteMaskDocProps { + onSuccess?: () => void; + listInvalideQueries?: string[]; +} + +export function useDeleteMaskDoc({ + onSuccess, + listInvalideQueries, +}: DeleteMaskDocProps) { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: deleteMaskDoc, + onSuccess: () => { + listInvalideQueries?.forEach((queryKey) => { + void queryClient.invalidateQueries({ + queryKey: [queryKey], + }); + }); + onSuccess?.(); + }, + }); +} diff --git a/src/frontend/apps/impress/src/features/docs/doc-management/hooks/index.ts b/src/frontend/apps/impress/src/features/docs/doc-management/hooks/index.ts index adf2d777ee..f5714e4143 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-management/hooks/index.ts +++ b/src/frontend/apps/impress/src/features/docs/doc-management/hooks/index.ts @@ -2,4 +2,5 @@ export * from './useCollaboration'; export * from './useCopyDocLink'; export * from './useDocUtils'; export * from './useIsCollaborativeEditable'; +export * from './useMaskDocOption'; export * from './useTrans'; diff --git a/src/frontend/apps/impress/src/features/docs/doc-management/hooks/useMaskDocOption.tsx b/src/frontend/apps/impress/src/features/docs/doc-management/hooks/useMaskDocOption.tsx new file mode 100644 index 0000000000..3e89b62689 --- /dev/null +++ b/src/frontend/apps/impress/src/features/docs/doc-management/hooks/useMaskDocOption.tsx @@ -0,0 +1,42 @@ +import { useTranslation } from 'react-i18next'; + +import { DropdownMenuOption } from '@/components'; + +import { KEY_DOC, KEY_LIST_DOC, useDeleteMaskDoc, useMaskDoc } from '../api'; +import { Doc } from '../types'; + +export const useMaskDocOption = (doc: Doc) => { + const { t } = useTranslation(); + const maskDoc = useMaskDoc({ + listInvalideQueries: [KEY_LIST_DOC, KEY_DOC], + }); + const deleteMaskDoc = useDeleteMaskDoc({ + listInvalideQueries: [KEY_LIST_DOC, KEY_DOC], + }); + + const leaveDocOption: DropdownMenuOption = doc.is_masked + ? { + label: t('Join the doc'), + icon: 'login', + callback: () => { + deleteMaskDoc.mutate({ + id: doc.id, + }); + }, + disabled: !doc.abilities.mask, + testId: `docs-grid-actions-mask-${doc.id}`, + } + : { + label: t('Leave doc'), + icon: 'logout', + callback: () => { + maskDoc.mutate({ + id: doc.id, + }); + }, + disabled: !doc.abilities.mask, + testId: `docs-grid-actions-mask-${doc.id}`, + }; + + return leaveDocOption; +}; diff --git a/src/frontend/apps/impress/src/features/docs/doc-management/types.tsx b/src/frontend/apps/impress/src/features/docs/doc-management/types.tsx index 7c8c2ea8e9..1bd20ddcf0 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-management/types.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-management/types.tsx @@ -59,6 +59,7 @@ export interface Doc { depth: number; path: string; is_favorite: boolean; + is_masked: boolean; link_reach: LinkReach; link_role: LinkRole; nb_accesses_direct: number; @@ -84,6 +85,7 @@ export interface Doc { favorite: boolean; invite_owner: boolean; link_configuration: boolean; + mask: boolean; media_auth: boolean; move: boolean; partial_update: boolean; diff --git a/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocsGrid.tsx b/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocsGrid.tsx index fc0dba2ff2..1b96407268 100644 --- a/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocsGrid.tsx +++ b/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocsGrid.tsx @@ -32,10 +32,14 @@ export const DocsGrid = ({ hasNextPage, } = useInfiniteDocs({ page: 1, - ...(target && - target !== DocDefaultFilter.ALL_DOCS && { - is_creator_me: target === DocDefaultFilter.MY_DOCS, - }), + is_masked: + !target || target === DocDefaultFilter.ALL_DOCS ? false : undefined, + is_creator_me: + target === DocDefaultFilter.MY_DOCS + ? true + : target === DocDefaultFilter.SHARED_WITH_ME + ? false + : undefined, }); const docs = data?.pages.flatMap((page) => page.results) ?? []; diff --git a/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocsGridActions.tsx b/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocsGridActions.tsx index f2fbc04c0a..eded7674b4 100644 --- a/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocsGridActions.tsx +++ b/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocsGridActions.tsx @@ -9,6 +9,7 @@ import { useCreateFavoriteDoc, useDeleteFavoriteDoc, useDuplicateDoc, + useMaskDocOption, } from '@/docs/doc-management'; interface DocsGridActionsProps { @@ -31,6 +32,7 @@ export const DocsGridActions = ({ const makeFavoriteDoc = useCreateFavoriteDoc({ listInvalideQueries: [KEY_LIST_DOC], }); + const maskDocOption = useMaskDocOption(doc); const options: DropdownMenuOption[] = [ { @@ -44,6 +46,7 @@ export const DocsGridActions = ({ } }, testId: `docs-grid-actions-${doc.is_favorite ? 'unpin' : 'pin'}-${doc.id}`, + showSeparator: true, }, { label: t('Share'), @@ -65,16 +68,22 @@ export const DocsGridActions = ({ canSave: false, }); }, - }, - { - label: t('Remove'), - icon: 'delete', - callback: () => deleteModal.open(), - disabled: !doc.abilities.destroy, - testId: `docs-grid-actions-remove-${doc.id}`, + showSeparator: true, }, ]; + const leaveDocOption: DropdownMenuOption = doc.abilities.destroy + ? { + label: t('Delete document'), + icon: 'delete', + callback: () => deleteModal.open(), + disabled: !doc.abilities.destroy, + testId: `docs-grid-actions-remove-${doc.id}`, + } + : maskDocOption; + + options.push(leaveDocOption); + return ( <> diff --git a/src/frontend/apps/impress/src/features/service-worker/plugins/ApiPlugin.ts b/src/frontend/apps/impress/src/features/service-worker/plugins/ApiPlugin.ts index f050e98ee1..a771563bda 100644 --- a/src/frontend/apps/impress/src/features/service-worker/plugins/ApiPlugin.ts +++ b/src/frontend/apps/impress/src/features/service-worker/plugins/ApiPlugin.ts @@ -174,6 +174,7 @@ export class ApiPlugin implements WorkboxPlugin { creator: 'dummy-id', depth: 1, is_favorite: false, + is_masked: false, nb_accesses_direct: 1, nb_accesses_ancestors: 1, numchild: 0, @@ -192,6 +193,7 @@ export class ApiPlugin implements WorkboxPlugin { favorite: true, invite_owner: true, link_configuration: true, + mask: true, media_auth: true, move: true, partial_update: true,