Skip to content

Commit 2a3b31f

Browse files
PanchoutNathanAntoLC
authored andcommitted
✨(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.
1 parent 9a64ebc commit 2a3b31f

File tree

11 files changed

+189
-37
lines changed

11 files changed

+189
-37
lines changed

src/frontend/apps/e2e/__tests__/app-impress/doc-tree.spec.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,46 @@ test.describe('Doc Tree', () => {
159159
`doc-sub-page-item-${firstSubPageJson.id}`,
160160
);
161161
});
162+
163+
test('it detachs a document', async ({ page, browserName }) => {
164+
await page.goto('/');
165+
const [docParent] = await createDoc(
166+
page,
167+
'doc-tree-detach',
168+
browserName,
169+
1,
170+
);
171+
await verifyDocName(page, docParent);
172+
173+
const [docChild] = await createDoc(
174+
page,
175+
'doc-tree-detach-child',
176+
browserName,
177+
1,
178+
true,
179+
);
180+
await verifyDocName(page, docChild);
181+
182+
const docTree = page.getByTestId('doc-tree');
183+
const child = docTree
184+
.getByRole('treeitem')
185+
.locator('.--docs-sub-page-item')
186+
.filter({
187+
hasText: docChild,
188+
});
189+
await child.hover();
190+
const menu = child.getByText(`more_horiz`);
191+
await menu.click();
192+
await page.getByText('Convert to doc').click();
193+
194+
await expect(
195+
page.getByRole('textbox', { name: 'doc title input' }),
196+
).not.toHaveText(docChild);
197+
198+
const header = page.locator('header').first();
199+
await header.locator('h2').getByText('Docs').click();
200+
await expect(page.getByText(docChild)).toBeVisible();
201+
});
162202
});
163203

164204
test.describe('Doc Tree: Inheritance', () => {

src/frontend/apps/impress/src/api/helpers.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@ export type DefinedInitialDataInfiniteOptionsAPI<
2121
TPageParam
2222
>;
2323

24+
export type InfiniteQueryConfig<Q> = Omit<
25+
DefinedInitialDataInfiniteOptionsAPI<Q>,
26+
'queryKey' | 'initialData' | 'getNextPageParam' | 'initialPageParam'
27+
>;
28+
2429
/**
2530
* Custom React hook that wraps React Query's `useInfiniteQuery` for paginated API requests.
2631
*
@@ -38,7 +43,7 @@ export const useAPIInfiniteQuery = <T, Q extends { next?: APIList<Q>['next'] }>(
3843
key: string,
3944
api: (props: T & { page: number }) => Promise<Q>,
4045
param: T,
41-
queryConfig?: DefinedInitialDataInfiniteOptionsAPI<Q>,
46+
queryConfig?: InfiniteQueryConfig<Q>,
4247
) => {
4348
return useInfiniteQuery<Q, APIError, InfiniteData<Q>, QueryKey, number>({
4449
initialPageParam: 1,

src/frontend/apps/impress/src/features/docs/doc-management/api/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export * from './useDeleteFavoriteDoc';
44
export * from './useDoc';
55
export * from './useDocOptions';
66
export * from './useDocs';
7+
export * from './useSubDocs';
78
export * from './useDuplicateDoc';
89
export * from './useUpdateDoc';
910
export * from './useUpdateDocLink';

src/frontend/apps/impress/src/features/docs/doc-management/api/useDocs.tsx

Lines changed: 10 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -8,22 +8,7 @@ import {
88
useAPIInfiniteQuery,
99
} from '@/api';
1010

11-
import { Doc } from '../types';
12-
13-
export const isDocsOrdering = (data: string): data is DocsOrdering => {
14-
return !!docsOrdering.find((validKey) => validKey === data);
15-
};
16-
17-
const docsOrdering = [
18-
'created_at',
19-
'-created_at',
20-
'updated_at',
21-
'-updated_at',
22-
'title',
23-
'-title',
24-
] as const;
25-
26-
export type DocsOrdering = (typeof docsOrdering)[number];
11+
import { Doc, DocsOrdering } from '../types';
2712

2813
export type DocsParams = {
2914
page: number;
@@ -33,26 +18,31 @@ export type DocsParams = {
3318
is_favorite?: boolean;
3419
};
3520

36-
export type DocsResponse = APIList<Doc>;
37-
export const getDocs = async (params: DocsParams): Promise<DocsResponse> => {
21+
export const constructParams = (params: DocsParams): URLSearchParams => {
3822
const searchParams = new URLSearchParams();
23+
3924
if (params.page) {
4025
searchParams.set('page', params.page.toString());
4126
}
42-
4327
if (params.ordering) {
4428
searchParams.set('ordering', params.ordering);
4529
}
4630
if (params.is_creator_me !== undefined) {
4731
searchParams.set('is_creator_me', params.is_creator_me.toString());
4832
}
49-
5033
if (params.title && params.title.length > 0) {
5134
searchParams.set('title', params.title);
5235
}
5336
if (params.is_favorite !== undefined) {
5437
searchParams.set('is_favorite', params.is_favorite.toString());
5538
}
39+
40+
return searchParams;
41+
};
42+
43+
export type DocsResponse = APIList<Doc>;
44+
export const getDocs = async (params: DocsParams): Promise<DocsResponse> => {
45+
const searchParams = constructParams(params);
5646
const response = await fetchAPI(`documents/?${searchParams.toString()}`);
5747

5848
if (!response.ok) {
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { UseQueryOptions, useQuery } from '@tanstack/react-query';
2+
3+
import {
4+
APIError,
5+
InfiniteQueryConfig,
6+
errorCauses,
7+
fetchAPI,
8+
useAPIInfiniteQuery,
9+
} from '@/api';
10+
11+
import { DocsOrdering } from '../types';
12+
13+
import { DocsResponse, constructParams } from './useDocs';
14+
15+
export type SubDocsParams = {
16+
page: number;
17+
ordering?: DocsOrdering;
18+
is_creator_me?: boolean;
19+
title?: string;
20+
is_favorite?: boolean;
21+
parent_id: string;
22+
};
23+
24+
export const getSubDocs = async (
25+
params: SubDocsParams,
26+
): Promise<DocsResponse> => {
27+
const searchParams = constructParams(params);
28+
searchParams.set('parent_id', params.parent_id);
29+
30+
const response: Response = await fetchAPI(
31+
`documents/${params.parent_id}/descendants/?${searchParams.toString()}`,
32+
);
33+
34+
if (!response.ok) {
35+
throw new APIError(
36+
'Failed to get the sub docs',
37+
await errorCauses(response),
38+
);
39+
}
40+
41+
return response.json() as Promise<DocsResponse>;
42+
};
43+
44+
export const KEY_LIST_SUB_DOC = 'sub-docs';
45+
46+
export function useSubDocs(
47+
params: SubDocsParams,
48+
queryConfig?: UseQueryOptions<DocsResponse, APIError, DocsResponse>,
49+
) {
50+
return useQuery<DocsResponse, APIError, DocsResponse>({
51+
queryKey: [KEY_LIST_SUB_DOC, params],
52+
queryFn: () => getSubDocs(params),
53+
...queryConfig,
54+
});
55+
}
56+
57+
export const useInfiniteSubDocs = (
58+
params: SubDocsParams,
59+
queryConfig?: InfiniteQueryConfig<DocsResponse>,
60+
) => {
61+
return useAPIInfiniteQuery(KEY_LIST_SUB_DOC, getSubDocs, params, queryConfig);
62+
};

src/frontend/apps/impress/src/features/docs/doc-management/types.tsx

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,19 +37,20 @@ export type Base64 = string;
3737
export interface Doc {
3838
id: string;
3939
title?: string;
40+
children?: Doc[];
41+
childrenCount?: number;
4042
content: Base64;
43+
created_at: string;
4144
creator: string;
45+
depth: number;
4246
is_favorite: boolean;
4347
link_reach: LinkReach;
4448
link_role: LinkRole;
45-
user_roles: Role[];
46-
created_at: string;
47-
updated_at: string;
4849
nb_accesses_direct: number;
4950
nb_accesses_ancestors: number;
50-
children?: Doc[];
51-
childrenCount?: number;
5251
numchild: number;
52+
updated_at: string;
53+
user_roles: Role[];
5354
abilities: {
5455
accesses_manage: boolean;
5556
accesses_view: boolean;
@@ -82,6 +83,15 @@ export enum DocDefaultFilter {
8283
SHARED_WITH_ME = 'shared_with_me',
8384
}
8485

86+
export type DocsOrdering =
87+
| 'title'
88+
| 'created_at'
89+
| '-created_at'
90+
| 'updated_at'
91+
| '-updated_at'
92+
| '-title'
93+
| undefined;
94+
8595
export interface AccessRequest {
8696
id: string;
8797
document: string;
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { useMutation, useQueryClient } from '@tanstack/react-query';
2+
3+
import { APIError, errorCauses, fetchAPI } from '@/api';
4+
5+
import { KEY_DOC, KEY_LIST_DOC } from '../../doc-management';
6+
7+
export type DetachDocParam = {
8+
documentId: string;
9+
rootId: string;
10+
};
11+
12+
enum POSITION_MOVE {
13+
FIRST_CHILD = 'first-child',
14+
LAST_CHILD = 'last-child',
15+
FIRST_SIBLING = 'first-sibling',
16+
LAST_SIBLING = 'last-sibling',
17+
LEFT = 'left',
18+
RIGHT = 'right',
19+
}
20+
21+
export const detachDoc = async ({
22+
documentId,
23+
rootId,
24+
}: DetachDocParam): Promise<void> => {
25+
const response = await fetchAPI(`documents/${documentId}/move/`, {
26+
method: 'POST',
27+
body: JSON.stringify({
28+
target_document_id: rootId,
29+
position: POSITION_MOVE.LAST_SIBLING,
30+
}),
31+
});
32+
33+
if (!response.ok) {
34+
throw new APIError('Failed to move the doc', await errorCauses(response));
35+
}
36+
37+
return response.json() as Promise<void>;
38+
};
39+
40+
export function useDetachDoc() {
41+
const queryClient = useQueryClient();
42+
return useMutation<void, APIError, DetachDocParam>({
43+
mutationFn: detachDoc,
44+
onSuccess: (_data, variables) => {
45+
void queryClient.invalidateQueries({ queryKey: [KEY_LIST_DOC] });
46+
void queryClient.invalidateQueries({
47+
queryKey: [KEY_DOC, { id: variables.documentId }],
48+
});
49+
},
50+
});
51+
}

src/frontend/apps/impress/src/features/docs/doc-tree/components/DocTreeItemActions.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export const DocTreeItemActions = ({
3737
const { togglePanel } = useLeftPanelStore();
3838
const copyLink = useCopyDocLink(doc.id);
3939
const canUpdate = isOwnerOrAdmin(doc);
40-
const { isChild } = useTreeUtils(doc);
40+
const { isCurrentParent } = useTreeUtils(doc);
4141
const { mutate: detachDoc } = useDetachDoc();
4242
const treeContext = useTreeContext<Doc>();
4343

@@ -66,7 +66,7 @@ export const DocTreeItemActions = ({
6666
icon: <Icon iconName="link" $size="24px" />,
6767
callback: copyLink,
6868
},
69-
...(isChild
69+
...(!isCurrentParent
7070
? [
7171
{
7272
label: t('Convert to doc'),

src/frontend/apps/impress/src/features/docs/doc-tree/hooks/useTreeUtils.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,6 @@ export const useTreeUtils = (doc: Doc) => {
88
return {
99
isParent: doc.nb_accesses_ancestors <= 1, // it is a parent
1010
isChild: doc.nb_accesses_ancestors > 1, // it is a child
11-
isCurrentParent: treeContext?.root?.id === doc.id, // it can be a child but not for the current user
11+
isCurrentParent: treeContext?.root?.id === doc.id || doc.depth === 1, // it can be a child but not for the current user
1212
} as const;
1313
};

src/frontend/apps/impress/src/features/service-worker/plugins/ApiPlugin.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@ export class ApiPlugin implements WorkboxPlugin {
172172
content: '',
173173
created_at: new Date().toISOString(),
174174
creator: 'dummy-id',
175+
depth: 1,
175176
is_favorite: false,
176177
nb_accesses_direct: 1,
177178
nb_accesses_ancestors: 1,

0 commit comments

Comments
 (0)