From f81db395ef9a333bd966a62a7d899d2a080daf56 Mon Sep 17 00:00:00 2001 From: Anthony LC Date: Fri, 9 Jan 2026 16:56:32 +0100 Subject: [PATCH 01/21] =?UTF-8?q?=E2=99=BB=EF=B8=8F(frontend)=20use=20dedi?= =?UTF-8?q?cated=20favorite=5Flist=20route?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It exists a dedicated endpoint to list favorite documents. Better use it to avoid relying on the general documents listing endpoint. --- .../docs/doc-header/components/DocToolBox.tsx | 5 +- .../features/docs/doc-management/api/index.ts | 1 + .../doc-management/api/useDocsFavorite.tsx | 72 +++++++++++++++++++ .../docs-grid/components/DocsGridActions.tsx | 5 +- .../components/LeftPanelFavorites.tsx | 5 +- 5 files changed, 81 insertions(+), 7 deletions(-) create mode 100644 src/frontend/apps/impress/src/features/docs/doc-management/api/useDocsFavorite.tsx 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 378b66e3..8c571d49 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 @@ -19,6 +19,7 @@ import { Doc, KEY_DOC, KEY_LIST_DOC, + KEY_LIST_FAVORITE_DOC, ModalRemoveDoc, getEmojiAndTitle, useCopyDocLink, @@ -67,10 +68,10 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => { }, }); const removeFavoriteDoc = useDeleteFavoriteDoc({ - listInvalidQueries: [KEY_LIST_DOC, KEY_DOC], + listInvalidQueries: [KEY_LIST_DOC, KEY_DOC, KEY_LIST_FAVORITE_DOC], }); const makeFavoriteDoc = useCreateFavoriteDoc({ - listInvalidQueries: [KEY_LIST_DOC, KEY_DOC], + listInvalidQueries: [KEY_LIST_DOC, KEY_DOC, KEY_LIST_FAVORITE_DOC], }); useEffect(() => { 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 e04d79f3..88c4b028 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 @@ -5,6 +5,7 @@ export * from './useDeleteFavoriteDoc'; export * from './useDoc'; export * from './useDocOptions'; export * from './useDocs'; +export * from './useDocsFavorite'; export * from './useDuplicateDoc'; export * from './useRestoreDoc'; export * from './useSubDocs'; diff --git a/src/frontend/apps/impress/src/features/docs/doc-management/api/useDocsFavorite.tsx b/src/frontend/apps/impress/src/features/docs/doc-management/api/useDocsFavorite.tsx new file mode 100644 index 00000000..3baa4510 --- /dev/null +++ b/src/frontend/apps/impress/src/features/docs/doc-management/api/useDocsFavorite.tsx @@ -0,0 +1,72 @@ +import { UseQueryOptions, useQuery } from '@tanstack/react-query'; + +import { + APIError, + APIList, + InfiniteQueryConfig, + errorCauses, + fetchAPI, + useAPIInfiniteQuery, +} from '@/api'; + +import { Doc } from '../types'; + +export type DocsFavoriteParams = { + page: number; +}; + +export type DocsFavoriteResponse = APIList; +export const getDocsFavorite = async ( + params: DocsFavoriteParams, +): Promise => { + const searchParams = new URLSearchParams(); + + if (params.page) { + searchParams.set('page', params.page.toString()); + } + + const response = await fetchAPI( + `documents/favorite_list/?${searchParams.toString()}`, + ); + + if (!response.ok) { + throw new APIError( + 'Failed to get the favorite docs', + await errorCauses(response), + ); + } + + return response.json() as Promise; +}; + +export const KEY_LIST_FAVORITE_DOC = 'docs_favorite_list'; + +type UseDocsOptions = UseQueryOptions< + DocsFavoriteResponse, + APIError, + DocsFavoriteResponse +>; +type UseInfiniteDocsOptions = InfiniteQueryConfig; + +export function useDocsFavorite( + params: DocsFavoriteParams, + queryConfig?: UseDocsOptions, +) { + return useQuery({ + queryKey: [KEY_LIST_FAVORITE_DOC, params], + queryFn: () => getDocsFavorite(params), + ...queryConfig, + }); +} + +export const useInfiniteDocsFavorite = ( + params: DocsFavoriteParams, + queryConfig?: UseInfiniteDocsOptions, +) => { + return useAPIInfiniteQuery( + KEY_LIST_FAVORITE_DOC, + getDocsFavorite, + params, + queryConfig, + ); +}; 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 94720b12..2758cd3b 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 @@ -6,6 +6,7 @@ import { DropdownMenu, DropdownMenuOption, Icon } from '@/components'; import { Doc, KEY_LIST_DOC, + KEY_LIST_FAVORITE_DOC, ModalRemoveDoc, useCreateFavoriteDoc, useDeleteFavoriteDoc, @@ -27,10 +28,10 @@ export const DocsGridActions = ({ const { mutate: duplicateDoc } = useDuplicateDoc(); const removeFavoriteDoc = useDeleteFavoriteDoc({ - listInvalidQueries: [KEY_LIST_DOC], + listInvalidQueries: [KEY_LIST_DOC, KEY_LIST_FAVORITE_DOC], }); const makeFavoriteDoc = useCreateFavoriteDoc({ - listInvalidQueries: [KEY_LIST_DOC], + listInvalidQueries: [KEY_LIST_DOC, KEY_LIST_FAVORITE_DOC], }); const options: DropdownMenuOption[] = [ diff --git a/src/frontend/apps/impress/src/features/left-panel/components/LeftPanelFavorites.tsx b/src/frontend/apps/impress/src/features/left-panel/components/LeftPanelFavorites.tsx index 7710bfbe..01111027 100644 --- a/src/frontend/apps/impress/src/features/left-panel/components/LeftPanelFavorites.tsx +++ b/src/frontend/apps/impress/src/features/left-panel/components/LeftPanelFavorites.tsx @@ -2,7 +2,7 @@ import { useTranslation } from 'react-i18next'; import { Box, HorizontalSeparator, InfiniteScroll, Text } from '@/components'; import { useCunninghamTheme } from '@/cunningham'; -import { useInfiniteDocs } from '@/docs/doc-management'; +import { useInfiniteDocsFavorite } from '@/docs/doc-management'; import { LeftPanelFavoriteItem } from './LeftPanelFavoriteItem'; @@ -11,9 +11,8 @@ export const LeftPanelFavorites = () => { const { spacingsTokens } = useCunninghamTheme(); - const docs = useInfiniteDocs({ + const docs = useInfiniteDocsFavorite({ page: 1, - is_favorite: true, }); const favoriteDocs = docs.data?.pages.flatMap((page) => page.results) || []; From dd02b9d94040c14997af6ff1a837826ab1999084 Mon Sep 17 00:00:00 2001 From: Anthony LC Date: Wed, 14 Jan 2026 10:02:33 +0100 Subject: [PATCH 02/21] =?UTF-8?q?=E2=99=BB=EF=B8=8F(backend)=20include=20s?= =?UTF-8?q?ub=20documents=20in=20the=20favorite=5Flist=20route?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The favorite_list route was returning all the favorite with depth=0. We also want to see favorited document with a depth > 0 --- CHANGELOG.md | 1 + src/backend/core/api/viewsets.py | 19 +++++++++++- .../test_api_documents_favorite_list.py | 31 +++++++++++++++++++ 3 files changed, 50 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bb4e3d53..b0561170 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ and this project adheres to - ✅(e2e) fix e2e test for other browsers #1799 - 🐛(frontend) add fallback for unsupported Blocknote languages #1810 - 🐛(frontend) fix emojipicker closing in tree #1808 +- 🐛(frontend) display children in favorite #1782 ### Changed diff --git a/src/backend/core/api/viewsets.py b/src/backend/core/api/viewsets.py index c4a137ee..3b62b893 100644 --- a/src/backend/core/api/viewsets.py +++ b/src/backend/core/api/viewsets.py @@ -628,12 +628,29 @@ class DocumentViewSet( """Get list of favorite documents for the current user.""" user = request.user + queryset = self.get_queryset() + + # Among the results, we may have documents that are ancestors/descendants + # of each other. In this case we want to keep only the highest ancestors. + root_paths = utils.filter_root_paths( + queryset.order_by("path").values_list("path", flat=True), + skip_sorting=True, + ) + + path_list = db.Q() + for path in root_paths: + path_list |= db.Q(path__startswith=path) + favorite_documents_ids = models.DocumentFavorite.objects.filter( user=user ).values_list("document_id", flat=True) - queryset = self.filter_queryset(self.get_queryset()) + queryset = self.queryset.filter(path_list) queryset = queryset.filter(id__in=favorite_documents_ids) + queryset = queryset.annotate_user_roles(user) + queryset = queryset.annotate( + is_favorite=db.Value(True, output_field=db.BooleanField()) + ) return self.get_response_for_queryset(queryset) @drf.decorators.action( diff --git a/src/backend/core/tests/documents/test_api_documents_favorite_list.py b/src/backend/core/tests/documents/test_api_documents_favorite_list.py index f93e95e0..d5bfe3c1 100644 --- a/src/backend/core/tests/documents/test_api_documents_favorite_list.py +++ b/src/backend/core/tests/documents/test_api_documents_favorite_list.py @@ -83,3 +83,34 @@ def test_api_document_favorite_list_authenticated_with_favorite(): } ], } + + +def test_api_document_favorite_list_with_favorite_children(): + """Authenticated users should receive their favorite documents, including children.""" + + user = factories.UserFactory() + client = APIClient() + client.force_login(user) + + root = factories.DocumentFactory(creator=user, users=[user]) + children = factories.DocumentFactory.create_batch( + 2, parent=root, favorited_by=[user] + ) + + access = factories.UserDocumentAccessFactory( + user=user, role=models.RoleChoices.READER, document__favorited_by=[user] + ) + + other_root = factories.DocumentFactory(creator=user, users=[user]) + factories.DocumentFactory.create_batch(2, parent=other_root) + + response = client.get("/api/v1.0/documents/favorite_list/") + + assert response.status_code == 200 + assert response.json()["count"] == 3 + + content = response.json()["results"] + + assert content[0]["id"] == str(children[0].id) + assert content[1]["id"] == str(children[1].id) + assert content[2]["id"] == str(access.document.id) From 548f32bf4e20afab4c1e1389e6e9ddb88c163a9f Mon Sep 17 00:00:00 2001 From: Anthony LC Date: Fri, 9 Jan 2026 15:27:50 +0100 Subject: [PATCH 03/21] =?UTF-8?q?=F0=9F=94=A5(frontend)=20remove=20all=20c?= =?UTF-8?q?ode=20related=20to=20template?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The template feature is removed from the frontend applications. It was used mainly for document export with predefined templates. --- .../__tests__/app-impress/doc-export.spec.ts | 17 ----- .../docs/doc-export/api/useExport.tsx | 44 ----------- .../docs/doc-export/api/useTemplates.tsx | 74 ------------------- .../doc-export/components/ModalExport.tsx | 44 +---------- .../src/features/docs/doc-export/types.ts | 18 ----- 5 files changed, 2 insertions(+), 195 deletions(-) delete mode 100644 src/frontend/apps/impress/src/features/docs/doc-export/api/useExport.tsx delete mode 100644 src/frontend/apps/impress/src/features/docs/doc-export/api/useTemplates.tsx diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-export.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-export.spec.ts index 5850b218..f6460d4b 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/doc-export.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-export.spec.ts @@ -35,9 +35,6 @@ test.describe('Doc Export', () => { await expect( page.getByText(/Download your document in a \.docx, \.odt.*format\./i), ).toBeVisible(); - await expect( - page.getByRole('combobox', { name: 'Template' }), - ).toBeVisible(); await expect(page.getByRole('combobox', { name: 'Format' })).toBeVisible(); await expect( page.getByRole('button', { @@ -236,20 +233,6 @@ test.describe('Doc Export', () => { }) .click(); - await page - .getByRole('combobox', { - name: 'Template', - }) - .click(); - - await page - .getByRole('option', { - name: 'Demo Template', - }) - .click({ - delay: 100, - }); - await new Promise((resolve) => setTimeout(resolve, 1000)); await expect(page.getByTestId('doc-export-download-button')).toBeVisible(); diff --git a/src/frontend/apps/impress/src/features/docs/doc-export/api/useExport.tsx b/src/frontend/apps/impress/src/features/docs/doc-export/api/useExport.tsx deleted file mode 100644 index 1be23ff5..00000000 --- a/src/frontend/apps/impress/src/features/docs/doc-export/api/useExport.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import { useMutation } from '@tanstack/react-query'; - -import { APIError, errorCauses, fetchAPI } from '@/api'; - -interface CreateExportParams { - templateId: string; - body: string; - body_type: 'html' | 'markdown'; - format: 'pdf' | 'docx'; -} - -export const createExport = async ({ - templateId, - body, - body_type, - format, -}: CreateExportParams): Promise => { - const response = await fetchAPI( - `templates/${templateId}/generate-document/`, - { - method: 'POST', - body: JSON.stringify({ - body, - body_type, - format, - }), - }, - ); - - if (!response.ok) { - throw new APIError( - 'Failed to export the document', - await errorCauses(response), - ); - } - - return await response.blob(); -}; - -export function useExport() { - return useMutation({ - mutationFn: createExport, - }); -} diff --git a/src/frontend/apps/impress/src/features/docs/doc-export/api/useTemplates.tsx b/src/frontend/apps/impress/src/features/docs/doc-export/api/useTemplates.tsx deleted file mode 100644 index 984393d6..00000000 --- a/src/frontend/apps/impress/src/features/docs/doc-export/api/useTemplates.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import { - DefinedInitialDataInfiniteOptions, - InfiniteData, - QueryKey, - useInfiniteQuery, -} from '@tanstack/react-query'; - -import { APIError, APIList, errorCauses, fetchAPI } from '@/api'; - -import { Template } from '../types'; - -export enum TemplatesOrdering { - BY_CREATED_ON = 'created_at', - BY_CREATED_ON_DESC = '-created_at', -} - -export type TemplatesParams = { - ordering: TemplatesOrdering; -}; -type TemplatesAPIParams = TemplatesParams & { - page: number; -}; - -type TemplatesResponse = APIList