From 9e4e55717345eee2c2a4a5715e0d5225f5a0cf6c Mon Sep 17 00:00:00 2001 From: Anthony LC Date: Tue, 29 Jul 2025 09:20:09 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8(frontend)=20Can=20mask=20a=20document?= =?UTF-8?q?=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 9f783d4e..76270aec 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 b84321c1..cb3075cf 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 3a0f3437..a026de2b 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 88f385df..bc587dde 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 00000000..ea881bb3 --- /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 adf2d777..f5714e41 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 00000000..3e89b626 --- /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 7c8c2ea8..1bd20ddc 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 fc0dba2f..1b964072 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 f2fbc04c..eded7674 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 f050e98e..a771563b 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,