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,