✨(frontend) added new features for document management
- Created new files for managing subdocuments and detaching documents. - Refactored API request configuration to use an improved configuration type. - Removed unnecessary logs from the ModalConfirmDownloadUnsafe component.
This commit is contained in:
committed by
Anthony LC
parent
9a64ebc1e9
commit
2a3b31fcff
@@ -159,6 +159,46 @@ test.describe('Doc Tree', () => {
|
||||
`doc-sub-page-item-${firstSubPageJson.id}`,
|
||||
);
|
||||
});
|
||||
|
||||
test('it detachs a document', async ({ page, browserName }) => {
|
||||
await page.goto('/');
|
||||
const [docParent] = await createDoc(
|
||||
page,
|
||||
'doc-tree-detach',
|
||||
browserName,
|
||||
1,
|
||||
);
|
||||
await verifyDocName(page, docParent);
|
||||
|
||||
const [docChild] = await createDoc(
|
||||
page,
|
||||
'doc-tree-detach-child',
|
||||
browserName,
|
||||
1,
|
||||
true,
|
||||
);
|
||||
await verifyDocName(page, docChild);
|
||||
|
||||
const docTree = page.getByTestId('doc-tree');
|
||||
const child = docTree
|
||||
.getByRole('treeitem')
|
||||
.locator('.--docs-sub-page-item')
|
||||
.filter({
|
||||
hasText: docChild,
|
||||
});
|
||||
await child.hover();
|
||||
const menu = child.getByText(`more_horiz`);
|
||||
await menu.click();
|
||||
await page.getByText('Convert to doc').click();
|
||||
|
||||
await expect(
|
||||
page.getByRole('textbox', { name: 'doc title input' }),
|
||||
).not.toHaveText(docChild);
|
||||
|
||||
const header = page.locator('header').first();
|
||||
await header.locator('h2').getByText('Docs').click();
|
||||
await expect(page.getByText(docChild)).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Doc Tree: Inheritance', () => {
|
||||
|
||||
@@ -21,6 +21,11 @@ export type DefinedInitialDataInfiniteOptionsAPI<
|
||||
TPageParam
|
||||
>;
|
||||
|
||||
export type InfiniteQueryConfig<Q> = Omit<
|
||||
DefinedInitialDataInfiniteOptionsAPI<Q>,
|
||||
'queryKey' | 'initialData' | 'getNextPageParam' | 'initialPageParam'
|
||||
>;
|
||||
|
||||
/**
|
||||
* Custom React hook that wraps React Query's `useInfiniteQuery` for paginated API requests.
|
||||
*
|
||||
@@ -38,7 +43,7 @@ export const useAPIInfiniteQuery = <T, Q extends { next?: APIList<Q>['next'] }>(
|
||||
key: string,
|
||||
api: (props: T & { page: number }) => Promise<Q>,
|
||||
param: T,
|
||||
queryConfig?: DefinedInitialDataInfiniteOptionsAPI<Q>,
|
||||
queryConfig?: InfiniteQueryConfig<Q>,
|
||||
) => {
|
||||
return useInfiniteQuery<Q, APIError, InfiniteData<Q>, QueryKey, number>({
|
||||
initialPageParam: 1,
|
||||
|
||||
@@ -4,6 +4,7 @@ export * from './useDeleteFavoriteDoc';
|
||||
export * from './useDoc';
|
||||
export * from './useDocOptions';
|
||||
export * from './useDocs';
|
||||
export * from './useSubDocs';
|
||||
export * from './useDuplicateDoc';
|
||||
export * from './useUpdateDoc';
|
||||
export * from './useUpdateDocLink';
|
||||
|
||||
@@ -8,22 +8,7 @@ import {
|
||||
useAPIInfiniteQuery,
|
||||
} from '@/api';
|
||||
|
||||
import { Doc } from '../types';
|
||||
|
||||
export const isDocsOrdering = (data: string): data is DocsOrdering => {
|
||||
return !!docsOrdering.find((validKey) => validKey === data);
|
||||
};
|
||||
|
||||
const docsOrdering = [
|
||||
'created_at',
|
||||
'-created_at',
|
||||
'updated_at',
|
||||
'-updated_at',
|
||||
'title',
|
||||
'-title',
|
||||
] as const;
|
||||
|
||||
export type DocsOrdering = (typeof docsOrdering)[number];
|
||||
import { Doc, DocsOrdering } from '../types';
|
||||
|
||||
export type DocsParams = {
|
||||
page: number;
|
||||
@@ -33,26 +18,31 @@ export type DocsParams = {
|
||||
is_favorite?: boolean;
|
||||
};
|
||||
|
||||
export type DocsResponse = APIList<Doc>;
|
||||
export const getDocs = async (params: DocsParams): Promise<DocsResponse> => {
|
||||
export const constructParams = (params: DocsParams): URLSearchParams => {
|
||||
const searchParams = new URLSearchParams();
|
||||
|
||||
if (params.page) {
|
||||
searchParams.set('page', params.page.toString());
|
||||
}
|
||||
|
||||
if (params.ordering) {
|
||||
searchParams.set('ordering', params.ordering);
|
||||
}
|
||||
if (params.is_creator_me !== undefined) {
|
||||
searchParams.set('is_creator_me', params.is_creator_me.toString());
|
||||
}
|
||||
|
||||
if (params.title && params.title.length > 0) {
|
||||
searchParams.set('title', params.title);
|
||||
}
|
||||
if (params.is_favorite !== undefined) {
|
||||
searchParams.set('is_favorite', params.is_favorite.toString());
|
||||
}
|
||||
|
||||
return searchParams;
|
||||
};
|
||||
|
||||
export type DocsResponse = APIList<Doc>;
|
||||
export const getDocs = async (params: DocsParams): Promise<DocsResponse> => {
|
||||
const searchParams = constructParams(params);
|
||||
const response = await fetchAPI(`documents/?${searchParams.toString()}`);
|
||||
|
||||
if (!response.ok) {
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import { UseQueryOptions, useQuery } from '@tanstack/react-query';
|
||||
|
||||
import {
|
||||
APIError,
|
||||
InfiniteQueryConfig,
|
||||
errorCauses,
|
||||
fetchAPI,
|
||||
useAPIInfiniteQuery,
|
||||
} from '@/api';
|
||||
|
||||
import { DocsOrdering } from '../types';
|
||||
|
||||
import { DocsResponse, constructParams } from './useDocs';
|
||||
|
||||
export type SubDocsParams = {
|
||||
page: number;
|
||||
ordering?: DocsOrdering;
|
||||
is_creator_me?: boolean;
|
||||
title?: string;
|
||||
is_favorite?: boolean;
|
||||
parent_id: string;
|
||||
};
|
||||
|
||||
export const getSubDocs = async (
|
||||
params: SubDocsParams,
|
||||
): Promise<DocsResponse> => {
|
||||
const searchParams = constructParams(params);
|
||||
searchParams.set('parent_id', params.parent_id);
|
||||
|
||||
const response: Response = await fetchAPI(
|
||||
`documents/${params.parent_id}/descendants/?${searchParams.toString()}`,
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new APIError(
|
||||
'Failed to get the sub docs',
|
||||
await errorCauses(response),
|
||||
);
|
||||
}
|
||||
|
||||
return response.json() as Promise<DocsResponse>;
|
||||
};
|
||||
|
||||
export const KEY_LIST_SUB_DOC = 'sub-docs';
|
||||
|
||||
export function useSubDocs(
|
||||
params: SubDocsParams,
|
||||
queryConfig?: UseQueryOptions<DocsResponse, APIError, DocsResponse>,
|
||||
) {
|
||||
return useQuery<DocsResponse, APIError, DocsResponse>({
|
||||
queryKey: [KEY_LIST_SUB_DOC, params],
|
||||
queryFn: () => getSubDocs(params),
|
||||
...queryConfig,
|
||||
});
|
||||
}
|
||||
|
||||
export const useInfiniteSubDocs = (
|
||||
params: SubDocsParams,
|
||||
queryConfig?: InfiniteQueryConfig<DocsResponse>,
|
||||
) => {
|
||||
return useAPIInfiniteQuery(KEY_LIST_SUB_DOC, getSubDocs, params, queryConfig);
|
||||
};
|
||||
@@ -37,19 +37,20 @@ export type Base64 = string;
|
||||
export interface Doc {
|
||||
id: string;
|
||||
title?: string;
|
||||
children?: Doc[];
|
||||
childrenCount?: number;
|
||||
content: Base64;
|
||||
created_at: string;
|
||||
creator: string;
|
||||
depth: number;
|
||||
is_favorite: boolean;
|
||||
link_reach: LinkReach;
|
||||
link_role: LinkRole;
|
||||
user_roles: Role[];
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
nb_accesses_direct: number;
|
||||
nb_accesses_ancestors: number;
|
||||
children?: Doc[];
|
||||
childrenCount?: number;
|
||||
numchild: number;
|
||||
updated_at: string;
|
||||
user_roles: Role[];
|
||||
abilities: {
|
||||
accesses_manage: boolean;
|
||||
accesses_view: boolean;
|
||||
@@ -82,6 +83,15 @@ export enum DocDefaultFilter {
|
||||
SHARED_WITH_ME = 'shared_with_me',
|
||||
}
|
||||
|
||||
export type DocsOrdering =
|
||||
| 'title'
|
||||
| 'created_at'
|
||||
| '-created_at'
|
||||
| 'updated_at'
|
||||
| '-updated_at'
|
||||
| '-title'
|
||||
| undefined;
|
||||
|
||||
export interface AccessRequest {
|
||||
id: string;
|
||||
document: string;
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
import { APIError, errorCauses, fetchAPI } from '@/api';
|
||||
|
||||
import { KEY_DOC, KEY_LIST_DOC } from '../../doc-management';
|
||||
|
||||
export type DetachDocParam = {
|
||||
documentId: string;
|
||||
rootId: string;
|
||||
};
|
||||
|
||||
enum POSITION_MOVE {
|
||||
FIRST_CHILD = 'first-child',
|
||||
LAST_CHILD = 'last-child',
|
||||
FIRST_SIBLING = 'first-sibling',
|
||||
LAST_SIBLING = 'last-sibling',
|
||||
LEFT = 'left',
|
||||
RIGHT = 'right',
|
||||
}
|
||||
|
||||
export const detachDoc = async ({
|
||||
documentId,
|
||||
rootId,
|
||||
}: DetachDocParam): Promise<void> => {
|
||||
const response = await fetchAPI(`documents/${documentId}/move/`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
target_document_id: rootId,
|
||||
position: POSITION_MOVE.LAST_SIBLING,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new APIError('Failed to move the doc', await errorCauses(response));
|
||||
}
|
||||
|
||||
return response.json() as Promise<void>;
|
||||
};
|
||||
|
||||
export function useDetachDoc() {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<void, APIError, DetachDocParam>({
|
||||
mutationFn: detachDoc,
|
||||
onSuccess: (_data, variables) => {
|
||||
void queryClient.invalidateQueries({ queryKey: [KEY_LIST_DOC] });
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: [KEY_DOC, { id: variables.documentId }],
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -37,7 +37,7 @@ export const DocTreeItemActions = ({
|
||||
const { togglePanel } = useLeftPanelStore();
|
||||
const copyLink = useCopyDocLink(doc.id);
|
||||
const canUpdate = isOwnerOrAdmin(doc);
|
||||
const { isChild } = useTreeUtils(doc);
|
||||
const { isCurrentParent } = useTreeUtils(doc);
|
||||
const { mutate: detachDoc } = useDetachDoc();
|
||||
const treeContext = useTreeContext<Doc>();
|
||||
|
||||
@@ -66,7 +66,7 @@ export const DocTreeItemActions = ({
|
||||
icon: <Icon iconName="link" $size="24px" />,
|
||||
callback: copyLink,
|
||||
},
|
||||
...(isChild
|
||||
...(!isCurrentParent
|
||||
? [
|
||||
{
|
||||
label: t('Convert to doc'),
|
||||
|
||||
@@ -8,6 +8,6 @@ export const useTreeUtils = (doc: Doc) => {
|
||||
return {
|
||||
isParent: doc.nb_accesses_ancestors <= 1, // it is a parent
|
||||
isChild: doc.nb_accesses_ancestors > 1, // it is a child
|
||||
isCurrentParent: treeContext?.root?.id === doc.id, // it can be a child but not for the current user
|
||||
isCurrentParent: treeContext?.root?.id === doc.id || doc.depth === 1, // it can be a child but not for the current user
|
||||
} as const;
|
||||
};
|
||||
|
||||
@@ -172,6 +172,7 @@ export class ApiPlugin implements WorkboxPlugin {
|
||||
content: '',
|
||||
created_at: new Date().toISOString(),
|
||||
creator: 'dummy-id',
|
||||
depth: 1,
|
||||
is_favorite: false,
|
||||
nb_accesses_direct: 1,
|
||||
nb_accesses_ancestors: 1,
|
||||
|
||||
@@ -68,11 +68,3 @@ main ::-webkit-scrollbar-thumb:hover,
|
||||
/* Support for IE. */
|
||||
font-feature-settings: 'liga';
|
||||
}
|
||||
|
||||
[data-nextjs-dialog-overlay] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
nextjs-portal {
|
||||
display: none;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user