✨(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}`,
|
`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', () => {
|
test.describe('Doc Tree: Inheritance', () => {
|
||||||
|
|||||||
@@ -21,6 +21,11 @@ export type DefinedInitialDataInfiniteOptionsAPI<
|
|||||||
TPageParam
|
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.
|
* 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,
|
key: string,
|
||||||
api: (props: T & { page: number }) => Promise<Q>,
|
api: (props: T & { page: number }) => Promise<Q>,
|
||||||
param: T,
|
param: T,
|
||||||
queryConfig?: DefinedInitialDataInfiniteOptionsAPI<Q>,
|
queryConfig?: InfiniteQueryConfig<Q>,
|
||||||
) => {
|
) => {
|
||||||
return useInfiniteQuery<Q, APIError, InfiniteData<Q>, QueryKey, number>({
|
return useInfiniteQuery<Q, APIError, InfiniteData<Q>, QueryKey, number>({
|
||||||
initialPageParam: 1,
|
initialPageParam: 1,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ export * from './useDeleteFavoriteDoc';
|
|||||||
export * from './useDoc';
|
export * from './useDoc';
|
||||||
export * from './useDocOptions';
|
export * from './useDocOptions';
|
||||||
export * from './useDocs';
|
export * from './useDocs';
|
||||||
|
export * from './useSubDocs';
|
||||||
export * from './useDuplicateDoc';
|
export * from './useDuplicateDoc';
|
||||||
export * from './useUpdateDoc';
|
export * from './useUpdateDoc';
|
||||||
export * from './useUpdateDocLink';
|
export * from './useUpdateDocLink';
|
||||||
|
|||||||
@@ -8,22 +8,7 @@ import {
|
|||||||
useAPIInfiniteQuery,
|
useAPIInfiniteQuery,
|
||||||
} from '@/api';
|
} from '@/api';
|
||||||
|
|
||||||
import { Doc } from '../types';
|
import { Doc, DocsOrdering } 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];
|
|
||||||
|
|
||||||
export type DocsParams = {
|
export type DocsParams = {
|
||||||
page: number;
|
page: number;
|
||||||
@@ -33,26 +18,31 @@ export type DocsParams = {
|
|||||||
is_favorite?: boolean;
|
is_favorite?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DocsResponse = APIList<Doc>;
|
export const constructParams = (params: DocsParams): URLSearchParams => {
|
||||||
export const getDocs = async (params: DocsParams): Promise<DocsResponse> => {
|
|
||||||
const searchParams = new URLSearchParams();
|
const searchParams = new URLSearchParams();
|
||||||
|
|
||||||
if (params.page) {
|
if (params.page) {
|
||||||
searchParams.set('page', params.page.toString());
|
searchParams.set('page', params.page.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (params.ordering) {
|
if (params.ordering) {
|
||||||
searchParams.set('ordering', params.ordering);
|
searchParams.set('ordering', params.ordering);
|
||||||
}
|
}
|
||||||
if (params.is_creator_me !== undefined) {
|
if (params.is_creator_me !== undefined) {
|
||||||
searchParams.set('is_creator_me', params.is_creator_me.toString());
|
searchParams.set('is_creator_me', params.is_creator_me.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (params.title && params.title.length > 0) {
|
if (params.title && params.title.length > 0) {
|
||||||
searchParams.set('title', params.title);
|
searchParams.set('title', params.title);
|
||||||
}
|
}
|
||||||
if (params.is_favorite !== undefined) {
|
if (params.is_favorite !== undefined) {
|
||||||
searchParams.set('is_favorite', params.is_favorite.toString());
|
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()}`);
|
const response = await fetchAPI(`documents/?${searchParams.toString()}`);
|
||||||
|
|
||||||
if (!response.ok) {
|
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 {
|
export interface Doc {
|
||||||
id: string;
|
id: string;
|
||||||
title?: string;
|
title?: string;
|
||||||
|
children?: Doc[];
|
||||||
|
childrenCount?: number;
|
||||||
content: Base64;
|
content: Base64;
|
||||||
|
created_at: string;
|
||||||
creator: string;
|
creator: string;
|
||||||
|
depth: number;
|
||||||
is_favorite: boolean;
|
is_favorite: boolean;
|
||||||
link_reach: LinkReach;
|
link_reach: LinkReach;
|
||||||
link_role: LinkRole;
|
link_role: LinkRole;
|
||||||
user_roles: Role[];
|
|
||||||
created_at: string;
|
|
||||||
updated_at: string;
|
|
||||||
nb_accesses_direct: number;
|
nb_accesses_direct: number;
|
||||||
nb_accesses_ancestors: number;
|
nb_accesses_ancestors: number;
|
||||||
children?: Doc[];
|
|
||||||
childrenCount?: number;
|
|
||||||
numchild: number;
|
numchild: number;
|
||||||
|
updated_at: string;
|
||||||
|
user_roles: Role[];
|
||||||
abilities: {
|
abilities: {
|
||||||
accesses_manage: boolean;
|
accesses_manage: boolean;
|
||||||
accesses_view: boolean;
|
accesses_view: boolean;
|
||||||
@@ -82,6 +83,15 @@ export enum DocDefaultFilter {
|
|||||||
SHARED_WITH_ME = 'shared_with_me',
|
SHARED_WITH_ME = 'shared_with_me',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type DocsOrdering =
|
||||||
|
| 'title'
|
||||||
|
| 'created_at'
|
||||||
|
| '-created_at'
|
||||||
|
| 'updated_at'
|
||||||
|
| '-updated_at'
|
||||||
|
| '-title'
|
||||||
|
| undefined;
|
||||||
|
|
||||||
export interface AccessRequest {
|
export interface AccessRequest {
|
||||||
id: string;
|
id: string;
|
||||||
document: 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 { togglePanel } = useLeftPanelStore();
|
||||||
const copyLink = useCopyDocLink(doc.id);
|
const copyLink = useCopyDocLink(doc.id);
|
||||||
const canUpdate = isOwnerOrAdmin(doc);
|
const canUpdate = isOwnerOrAdmin(doc);
|
||||||
const { isChild } = useTreeUtils(doc);
|
const { isCurrentParent } = useTreeUtils(doc);
|
||||||
const { mutate: detachDoc } = useDetachDoc();
|
const { mutate: detachDoc } = useDetachDoc();
|
||||||
const treeContext = useTreeContext<Doc>();
|
const treeContext = useTreeContext<Doc>();
|
||||||
|
|
||||||
@@ -66,7 +66,7 @@ export const DocTreeItemActions = ({
|
|||||||
icon: <Icon iconName="link" $size="24px" />,
|
icon: <Icon iconName="link" $size="24px" />,
|
||||||
callback: copyLink,
|
callback: copyLink,
|
||||||
},
|
},
|
||||||
...(isChild
|
...(!isCurrentParent
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
label: t('Convert to doc'),
|
label: t('Convert to doc'),
|
||||||
|
|||||||
@@ -8,6 +8,6 @@ export const useTreeUtils = (doc: Doc) => {
|
|||||||
return {
|
return {
|
||||||
isParent: doc.nb_accesses_ancestors <= 1, // it is a parent
|
isParent: doc.nb_accesses_ancestors <= 1, // it is a parent
|
||||||
isChild: doc.nb_accesses_ancestors > 1, // it is a child
|
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;
|
} as const;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -172,6 +172,7 @@ export class ApiPlugin implements WorkboxPlugin {
|
|||||||
content: '',
|
content: '',
|
||||||
created_at: new Date().toISOString(),
|
created_at: new Date().toISOString(),
|
||||||
creator: 'dummy-id',
|
creator: 'dummy-id',
|
||||||
|
depth: 1,
|
||||||
is_favorite: false,
|
is_favorite: false,
|
||||||
nb_accesses_direct: 1,
|
nb_accesses_direct: 1,
|
||||||
nb_accesses_ancestors: 1,
|
nb_accesses_ancestors: 1,
|
||||||
|
|||||||
@@ -68,11 +68,3 @@ main ::-webkit-scrollbar-thumb:hover,
|
|||||||
/* Support for IE. */
|
/* Support for IE. */
|
||||||
font-feature-settings: 'liga';
|
font-feature-settings: 'liga';
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-nextjs-dialog-overlay] {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
nextjs-portal {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user