(frontend) Can mask a document in the list view

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.
This commit is contained in:
Anthony LC 2025-07-29 09:20:09 +02:00
parent 11dfc9ff03
commit 9e4e557173
No known key found for this signature in database
11 changed files with 172 additions and 20 deletions

View file

@ -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

View file

@ -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 (

View file

@ -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';

View file

@ -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;
};

View file

@ -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<Doc, 'id'>;
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<void, APIError, MaskDocParams>({
mutationFn: maskDoc,
onSuccess: () => {
listInvalideQueries?.forEach((queryKey) => {
void queryClient.invalidateQueries({
queryKey: [queryKey],
});
});
onSuccess?.();
},
});
}
export type DeleteMaskDocParams = Pick<Doc, 'id'>;
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<void, APIError, DeleteMaskDocParams>({
mutationFn: deleteMaskDoc,
onSuccess: () => {
listInvalideQueries?.forEach((queryKey) => {
void queryClient.invalidateQueries({
queryKey: [queryKey],
});
});
onSuccess?.();
},
});
}

View file

@ -2,4 +2,5 @@ export * from './useCollaboration';
export * from './useCopyDocLink';
export * from './useDocUtils';
export * from './useIsCollaborativeEditable';
export * from './useMaskDocOption';
export * from './useTrans';

View file

@ -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;
};

View file

@ -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;

View file

@ -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) ?? [];

View file

@ -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 (
<>
<DropdownMenu options={options}>

View file

@ -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,