From 5bdf5d2210fd2e24a4a9701e44ea801677de0fb2 Mon Sep 17 00:00:00 2001 From: Anthony LC Date: Fri, 3 Oct 2025 12:37:37 +0200 Subject: [PATCH 01/10] =?UTF-8?q?=F0=9F=94=A7(backend)=20expose=20TRASHBIN?= =?UTF-8?q?=5FCUTOFF=5FDAYS?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit To know when a document in the trashbin will be permanently deleted. --- src/backend/core/api/viewsets.py | 1 + src/backend/core/tests/test_api_config.py | 3 ++- src/frontend/apps/e2e/__tests__/app-impress/utils-common.ts | 1 + src/frontend/apps/impress/src/core/config/api/useConfig.tsx | 1 + 4 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/backend/core/api/viewsets.py b/src/backend/core/api/viewsets.py index 1fb95c4eb..3897fa1f4 100644 --- a/src/backend/core/api/viewsets.py +++ b/src/backend/core/api/viewsets.py @@ -2157,6 +2157,7 @@ def get(self, request): "LANGUAGES", "LANGUAGE_CODE", "SENTRY_DSN", + "TRASHBIN_CUTOFF_DAYS" ] dict_settings = {} for setting in array_settings: diff --git a/src/backend/core/tests/test_api_config.py b/src/backend/core/tests/test_api_config.py index cac6bc077..0261125e8 100644 --- a/src/backend/core/tests/test_api_config.py +++ b/src/backend/core/tests/test_api_config.py @@ -42,6 +42,7 @@ def test_api_config(is_authenticated): response = client.get("/api/v1.0/config/") assert response.status_code == HTTP_200_OK assert response.json() == { + "AI_FEATURE_ENABLED": False, "COLLABORATION_WS_URL": "http://testcollab/", "COLLABORATION_WS_NOT_CONNECTED_READY_ONLY": True, "CRISP_WEBSITE_ID": "123", @@ -60,7 +61,7 @@ def test_api_config(is_authenticated): "MEDIA_BASE_URL": "http://testserver/", "POSTHOG_KEY": {"id": "132456", "host": "https://eu.i.posthog-test.com"}, "SENTRY_DSN": "https://sentry.test/123", - "AI_FEATURE_ENABLED": False, + "TRASHBIN_CUTOFF_DAYS": 30, "theme_customization": {}, } policy_list = sorted(response.headers["Content-Security-Policy"].split("; ")) diff --git a/src/frontend/apps/e2e/__tests__/app-impress/utils-common.ts b/src/frontend/apps/e2e/__tests__/app-impress/utils-common.ts index d7387378a..77b25f7da 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/utils-common.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/utils-common.ts @@ -23,6 +23,7 @@ export const CONFIG = { LANGUAGE_CODE: 'en-us', POSTHOG_KEY: {}, SENTRY_DSN: null, + TRASHBIN_CUTOFF_DAYS: 30, theme_customization: {}, } as const; diff --git a/src/frontend/apps/impress/src/core/config/api/useConfig.tsx b/src/frontend/apps/impress/src/core/config/api/useConfig.tsx index f2b287501..24452f3f2 100644 --- a/src/frontend/apps/impress/src/core/config/api/useConfig.tsx +++ b/src/frontend/apps/impress/src/core/config/api/useConfig.tsx @@ -27,6 +27,7 @@ export interface ConfigResponse { MEDIA_BASE_URL?: string; POSTHOG_KEY?: PostHogConf; SENTRY_DSN?: string; + TRASHBIN_CUTOFF_DAYS?: number; theme_customization?: ThemeCustomization; } From 390a615f488b138fd810a82279c5bd7558506123 Mon Sep 17 00:00:00 2001 From: Manuel Raynaud Date: Mon, 6 Oct 2025 08:38:40 +0200 Subject: [PATCH 02/10] =?UTF-8?q?=E2=9C=A8(backend)=20expose=20deleted=5Fa?= =?UTF-8?q?t=20information=20in=20serializer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The front needs to know when a document has been deleted. We expose the deleted_at property on a document object, --- src/backend/core/api/serializers.py | 9 +++++ src/backend/core/api/viewsets.py | 2 +- .../test_api_documents_children_list.py | 14 ++++++++ .../test_api_documents_descendants.py | 21 +++++++++++ .../test_api_documents_favorite_list.py | 1 + .../documents/test_api_documents_list.py | 1 + .../documents/test_api_documents_retrieve.py | 9 +++++ .../documents/test_api_documents_trashbin.py | 3 +- .../documents/test_api_documents_tree.py | 35 +++++++++++++++++++ 9 files changed, 93 insertions(+), 2 deletions(-) diff --git a/src/backend/core/api/serializers.py b/src/backend/core/api/serializers.py index fe94cd5f4..6c09cf18f 100644 --- a/src/backend/core/api/serializers.py +++ b/src/backend/core/api/serializers.py @@ -90,6 +90,7 @@ class ListDocumentSerializer(serializers.ModelSerializer): nb_accesses_direct = serializers.IntegerField(read_only=True) user_role = serializers.SerializerMethodField(read_only=True) abilities = serializers.SerializerMethodField(read_only=True) + deleted_at = serializers.SerializerMethodField(read_only=True) class Meta: model = models.Document @@ -102,6 +103,7 @@ class Meta: "computed_link_role", "created_at", "creator", + "deleted_at", "depth", "excerpt", "is_favorite", @@ -124,6 +126,7 @@ class Meta: "computed_link_role", "created_at", "creator", + "deleted_at", "depth", "excerpt", "is_favorite", @@ -165,6 +168,10 @@ def get_user_role(self, instance): request = self.context.get("request") return instance.get_role(request.user) if request else None + def get_deleted_at(self, instance): + """Return the deleted_at of the current document.""" + return instance.ancestors_deleted_at + class DocumentLightSerializer(serializers.ModelSerializer): """Minial document serializer for nesting in document accesses.""" @@ -193,6 +200,7 @@ class Meta: "content", "created_at", "creator", + "deleted_at", "depth", "excerpt", "is_favorite", @@ -216,6 +224,7 @@ class Meta: "computed_link_role", "created_at", "creator", + "deleted_at", "depth", "is_favorite", "link_role", diff --git a/src/backend/core/api/viewsets.py b/src/backend/core/api/viewsets.py index 3897fa1f4..1455f2166 100644 --- a/src/backend/core/api/viewsets.py +++ b/src/backend/core/api/viewsets.py @@ -2157,7 +2157,7 @@ def get(self, request): "LANGUAGES", "LANGUAGE_CODE", "SENTRY_DSN", - "TRASHBIN_CUTOFF_DAYS" + "TRASHBIN_CUTOFF_DAYS", ] dict_settings = {} for setting in array_settings: diff --git a/src/backend/core/tests/documents/test_api_documents_children_list.py b/src/backend/core/tests/documents/test_api_documents_children_list.py index 19bcfd192..e9a5cff31 100644 --- a/src/backend/core/tests/documents/test_api_documents_children_list.py +++ b/src/backend/core/tests/documents/test_api_documents_children_list.py @@ -41,6 +41,7 @@ def test_api_documents_children_list_anonymous_public_standalone( "computed_link_role": child1.computed_link_role, "created_at": child1.created_at.isoformat().replace("+00:00", "Z"), "creator": str(child1.creator.id), + "deleted_at": None, "depth": 2, "excerpt": child1.excerpt, "id": str(child1.id), @@ -63,6 +64,7 @@ def test_api_documents_children_list_anonymous_public_standalone( "computed_link_role": child2.computed_link_role, "created_at": child2.created_at.isoformat().replace("+00:00", "Z"), "creator": str(child2.creator.id), + "deleted_at": None, "depth": 2, "excerpt": child2.excerpt, "id": str(child2.id), @@ -115,6 +117,7 @@ def test_api_documents_children_list_anonymous_public_parent(django_assert_num_q "computed_link_role": child1.computed_link_role, "created_at": child1.created_at.isoformat().replace("+00:00", "Z"), "creator": str(child1.creator.id), + "deleted_at": None, "depth": 4, "excerpt": child1.excerpt, "id": str(child1.id), @@ -137,6 +140,7 @@ def test_api_documents_children_list_anonymous_public_parent(django_assert_num_q "computed_link_role": child2.computed_link_role, "created_at": child2.created_at.isoformat().replace("+00:00", "Z"), "creator": str(child2.creator.id), + "deleted_at": None, "depth": 4, "excerpt": child2.excerpt, "id": str(child2.id), @@ -208,6 +212,7 @@ def test_api_documents_children_list_authenticated_unrelated_public_or_authentic "computed_link_role": child1.computed_link_role, "created_at": child1.created_at.isoformat().replace("+00:00", "Z"), "creator": str(child1.creator.id), + "deleted_at": None, "depth": 2, "excerpt": child1.excerpt, "id": str(child1.id), @@ -230,6 +235,7 @@ def test_api_documents_children_list_authenticated_unrelated_public_or_authentic "computed_link_role": child2.computed_link_role, "created_at": child2.created_at.isoformat().replace("+00:00", "Z"), "creator": str(child2.creator.id), + "deleted_at": None, "depth": 2, "excerpt": child2.excerpt, "id": str(child2.id), @@ -287,6 +293,7 @@ def test_api_documents_children_list_authenticated_public_or_authenticated_paren "computed_link_role": child1.computed_link_role, "created_at": child1.created_at.isoformat().replace("+00:00", "Z"), "creator": str(child1.creator.id), + "deleted_at": None, "depth": 4, "excerpt": child1.excerpt, "id": str(child1.id), @@ -309,6 +316,7 @@ def test_api_documents_children_list_authenticated_public_or_authenticated_paren "computed_link_role": child2.computed_link_role, "created_at": child2.created_at.isoformat().replace("+00:00", "Z"), "creator": str(child2.creator.id), + "deleted_at": None, "depth": 4, "excerpt": child2.excerpt, "id": str(child2.id), @@ -393,6 +401,7 @@ def test_api_documents_children_list_authenticated_related_direct( "computed_link_role": child1.computed_link_role, "created_at": child1.created_at.isoformat().replace("+00:00", "Z"), "creator": str(child1.creator.id), + "deleted_at": None, "depth": 2, "excerpt": child1.excerpt, "id": str(child1.id), @@ -415,6 +424,7 @@ def test_api_documents_children_list_authenticated_related_direct( "computed_link_role": child2.computed_link_role, "created_at": child2.created_at.isoformat().replace("+00:00", "Z"), "creator": str(child2.creator.id), + "deleted_at": None, "depth": 2, "excerpt": child2.excerpt, "id": str(child2.id), @@ -475,6 +485,7 @@ def test_api_documents_children_list_authenticated_related_parent( "computed_link_role": child1.computed_link_role, "created_at": child1.created_at.isoformat().replace("+00:00", "Z"), "creator": str(child1.creator.id), + "deleted_at": None, "depth": 4, "excerpt": child1.excerpt, "id": str(child1.id), @@ -497,6 +508,7 @@ def test_api_documents_children_list_authenticated_related_parent( "computed_link_role": child2.computed_link_role, "created_at": child2.created_at.isoformat().replace("+00:00", "Z"), "creator": str(child2.creator.id), + "deleted_at": None, "depth": 4, "excerpt": child2.excerpt, "id": str(child2.id), @@ -609,6 +621,7 @@ def test_api_documents_children_list_authenticated_related_team_members( "computed_link_role": child1.computed_link_role, "created_at": child1.created_at.isoformat().replace("+00:00", "Z"), "creator": str(child1.creator.id), + "deleted_at": None, "depth": 2, "excerpt": child1.excerpt, "id": str(child1.id), @@ -631,6 +644,7 @@ def test_api_documents_children_list_authenticated_related_team_members( "computed_link_role": child2.computed_link_role, "created_at": child2.created_at.isoformat().replace("+00:00", "Z"), "creator": str(child2.creator.id), + "deleted_at": None, "depth": 2, "excerpt": child2.excerpt, "id": str(child2.id), diff --git a/src/backend/core/tests/documents/test_api_documents_descendants.py b/src/backend/core/tests/documents/test_api_documents_descendants.py index bd2785a7f..f320b0707 100644 --- a/src/backend/core/tests/documents/test_api_documents_descendants.py +++ b/src/backend/core/tests/documents/test_api_documents_descendants.py @@ -38,6 +38,7 @@ def test_api_documents_descendants_list_anonymous_public_standalone(): "computed_link_role": child1.computed_link_role, "created_at": child1.created_at.isoformat().replace("+00:00", "Z"), "creator": str(child1.creator.id), + "deleted_at": None, "depth": 2, "excerpt": child1.excerpt, "id": str(child1.id), @@ -62,6 +63,7 @@ def test_api_documents_descendants_list_anonymous_public_standalone(): "computed_link_role": grand_child.computed_link_role, "created_at": grand_child.created_at.isoformat().replace("+00:00", "Z"), "creator": str(grand_child.creator.id), + "deleted_at": None, "depth": 3, "excerpt": grand_child.excerpt, "id": str(grand_child.id), @@ -84,6 +86,7 @@ def test_api_documents_descendants_list_anonymous_public_standalone(): "computed_link_role": child2.computed_link_role, "created_at": child2.created_at.isoformat().replace("+00:00", "Z"), "creator": str(child2.creator.id), + "deleted_at": None, "depth": 2, "excerpt": child2.excerpt, "id": str(child2.id), @@ -135,6 +138,7 @@ def test_api_documents_descendants_list_anonymous_public_parent(): "computed_link_role": child1.computed_link_role, "created_at": child1.created_at.isoformat().replace("+00:00", "Z"), "creator": str(child1.creator.id), + "deleted_at": None, "depth": 4, "excerpt": child1.excerpt, "id": str(child1.id), @@ -157,6 +161,7 @@ def test_api_documents_descendants_list_anonymous_public_parent(): "computed_link_role": grand_child.computed_link_role, "created_at": grand_child.created_at.isoformat().replace("+00:00", "Z"), "creator": str(grand_child.creator.id), + "deleted_at": None, "depth": 5, "excerpt": grand_child.excerpt, "id": str(grand_child.id), @@ -179,6 +184,7 @@ def test_api_documents_descendants_list_anonymous_public_parent(): "computed_link_role": child2.computed_link_role, "created_at": child2.created_at.isoformat().replace("+00:00", "Z"), "creator": str(child2.creator.id), + "deleted_at": None, "depth": 4, "excerpt": child2.excerpt, "id": str(child2.id), @@ -251,6 +257,7 @@ def test_api_documents_descendants_list_authenticated_unrelated_public_or_authen "computed_link_role": child1.computed_link_role, "created_at": child1.created_at.isoformat().replace("+00:00", "Z"), "creator": str(child1.creator.id), + "deleted_at": None, "depth": 2, "excerpt": child1.excerpt, "id": str(child1.id), @@ -273,6 +280,7 @@ def test_api_documents_descendants_list_authenticated_unrelated_public_or_authen "computed_link_role": grand_child.computed_link_role, "created_at": grand_child.created_at.isoformat().replace("+00:00", "Z"), "creator": str(grand_child.creator.id), + "deleted_at": None, "depth": 3, "excerpt": grand_child.excerpt, "id": str(grand_child.id), @@ -295,6 +303,7 @@ def test_api_documents_descendants_list_authenticated_unrelated_public_or_authen "computed_link_role": child2.computed_link_role, "created_at": child2.created_at.isoformat().replace("+00:00", "Z"), "creator": str(child2.creator.id), + "deleted_at": None, "depth": 2, "excerpt": child2.excerpt, "id": str(child2.id), @@ -352,6 +361,7 @@ def test_api_documents_descendants_list_authenticated_public_or_authenticated_pa "computed_link_role": child1.computed_link_role, "created_at": child1.created_at.isoformat().replace("+00:00", "Z"), "creator": str(child1.creator.id), + "deleted_at": None, "depth": 4, "excerpt": child1.excerpt, "id": str(child1.id), @@ -374,6 +384,7 @@ def test_api_documents_descendants_list_authenticated_public_or_authenticated_pa "computed_link_role": grand_child.computed_link_role, "created_at": grand_child.created_at.isoformat().replace("+00:00", "Z"), "creator": str(grand_child.creator.id), + "deleted_at": None, "depth": 5, "excerpt": grand_child.excerpt, "id": str(grand_child.id), @@ -396,6 +407,7 @@ def test_api_documents_descendants_list_authenticated_public_or_authenticated_pa "computed_link_role": child2.computed_link_role, "created_at": child2.created_at.isoformat().replace("+00:00", "Z"), "creator": str(child2.creator.id), + "deleted_at": None, "depth": 4, "excerpt": child2.excerpt, "id": str(child2.id), @@ -474,6 +486,7 @@ def test_api_documents_descendants_list_authenticated_related_direct(): "computed_link_role": child1.computed_link_role, "created_at": child1.created_at.isoformat().replace("+00:00", "Z"), "creator": str(child1.creator.id), + "deleted_at": None, "depth": 2, "excerpt": child1.excerpt, "id": str(child1.id), @@ -496,6 +509,7 @@ def test_api_documents_descendants_list_authenticated_related_direct(): "computed_link_role": grand_child.computed_link_role, "created_at": grand_child.created_at.isoformat().replace("+00:00", "Z"), "creator": str(grand_child.creator.id), + "deleted_at": None, "depth": 3, "excerpt": grand_child.excerpt, "id": str(grand_child.id), @@ -518,6 +532,7 @@ def test_api_documents_descendants_list_authenticated_related_direct(): "computed_link_role": child2.computed_link_role, "created_at": child2.created_at.isoformat().replace("+00:00", "Z"), "creator": str(child2.creator.id), + "deleted_at": None, "depth": 2, "excerpt": child2.excerpt, "id": str(child2.id), @@ -576,6 +591,7 @@ def test_api_documents_descendants_list_authenticated_related_parent(): "computed_link_role": child1.computed_link_role, "created_at": child1.created_at.isoformat().replace("+00:00", "Z"), "creator": str(child1.creator.id), + "deleted_at": None, "depth": 4, "excerpt": child1.excerpt, "id": str(child1.id), @@ -598,6 +614,7 @@ def test_api_documents_descendants_list_authenticated_related_parent(): "computed_link_role": grand_child.computed_link_role, "created_at": grand_child.created_at.isoformat().replace("+00:00", "Z"), "creator": str(grand_child.creator.id), + "deleted_at": None, "depth": 5, "excerpt": grand_child.excerpt, "id": str(grand_child.id), @@ -620,6 +637,7 @@ def test_api_documents_descendants_list_authenticated_related_parent(): "computed_link_role": child2.computed_link_role, "created_at": child2.created_at.isoformat().replace("+00:00", "Z"), "creator": str(child2.creator.id), + "deleted_at": None, "depth": 4, "excerpt": child2.excerpt, "id": str(child2.id), @@ -724,6 +742,7 @@ def test_api_documents_descendants_list_authenticated_related_team_members( "computed_link_role": child1.computed_link_role, "created_at": child1.created_at.isoformat().replace("+00:00", "Z"), "creator": str(child1.creator.id), + "deleted_at": None, "depth": 2, "excerpt": child1.excerpt, "id": str(child1.id), @@ -746,6 +765,7 @@ def test_api_documents_descendants_list_authenticated_related_team_members( "computed_link_role": grand_child.computed_link_role, "created_at": grand_child.created_at.isoformat().replace("+00:00", "Z"), "creator": str(grand_child.creator.id), + "deleted_at": None, "depth": 3, "excerpt": grand_child.excerpt, "id": str(grand_child.id), @@ -768,6 +788,7 @@ def test_api_documents_descendants_list_authenticated_related_team_members( "computed_link_role": child2.computed_link_role, "created_at": child2.created_at.isoformat().replace("+00:00", "Z"), "creator": str(child2.creator.id), + "deleted_at": None, "depth": 2, "excerpt": child2.excerpt, "id": str(child2.id), 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 3ac9170ab..f93e95e08 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 @@ -65,6 +65,7 @@ def test_api_document_favorite_list_authenticated_with_favorite(): "computed_link_role": document.computed_link_role, "created_at": document.created_at.isoformat().replace("+00:00", "Z"), "creator": str(document.creator.id), + "deleted_at": None, "content": document.content, "depth": document.depth, "excerpt": document.excerpt, diff --git a/src/backend/core/tests/documents/test_api_documents_list.py b/src/backend/core/tests/documents/test_api_documents_list.py index 1fe235940..bb422a0ce 100644 --- a/src/backend/core/tests/documents/test_api_documents_list.py +++ b/src/backend/core/tests/documents/test_api_documents_list.py @@ -69,6 +69,7 @@ def test_api_documents_list_format(): "computed_link_role": document.computed_link_role, "created_at": document.created_at.isoformat().replace("+00:00", "Z"), "creator": str(document.creator.id), + "deleted_at": None, "depth": 1, "excerpt": document.excerpt, "is_favorite": True, diff --git a/src/backend/core/tests/documents/test_api_documents_retrieve.py b/src/backend/core/tests/documents/test_api_documents_retrieve.py index d1f8e1f03..fa8b1e2eb 100644 --- a/src/backend/core/tests/documents/test_api_documents_retrieve.py +++ b/src/backend/core/tests/documents/test_api_documents_retrieve.py @@ -70,6 +70,7 @@ def test_api_documents_retrieve_anonymous_public_standalone(): "content": document.content, "created_at": document.created_at.isoformat().replace("+00:00", "Z"), "creator": str(document.creator.id), + "deleted_at": None, "depth": 1, "excerpt": document.excerpt, "is_favorite": False, @@ -144,6 +145,7 @@ def test_api_documents_retrieve_anonymous_public_parent(): "content": document.content, "created_at": document.created_at.isoformat().replace("+00:00", "Z"), "creator": str(document.creator.id), + "deleted_at": None, "depth": 3, "excerpt": document.excerpt, "is_favorite": False, @@ -252,6 +254,7 @@ def test_api_documents_retrieve_authenticated_unrelated_public_or_authenticated( "created_at": document.created_at.isoformat().replace("+00:00", "Z"), "creator": str(document.creator.id), "depth": 1, + "deleted_at": None, "excerpt": document.excerpt, "is_favorite": False, "link_reach": reach, @@ -333,6 +336,7 @@ def test_api_documents_retrieve_authenticated_public_or_authenticated_parent(rea "created_at": document.created_at.isoformat().replace("+00:00", "Z"), "creator": str(document.creator.id), "depth": 3, + "deleted_at": None, "excerpt": document.excerpt, "is_favorite": False, "link_reach": document.link_reach, @@ -446,6 +450,7 @@ def test_api_documents_retrieve_authenticated_related_direct(): "content": document.content, "creator": str(document.creator.id), "created_at": document.created_at.isoformat().replace("+00:00", "Z"), + "deleted_at": None, "depth": 1, "excerpt": document.excerpt, "is_favorite": False, @@ -528,6 +533,7 @@ def test_api_documents_retrieve_authenticated_related_parent(): "creator": str(document.creator.id), "created_at": document.created_at.isoformat().replace("+00:00", "Z"), "depth": 3, + "deleted_at": None, "excerpt": document.excerpt, "is_favorite": False, "link_reach": "restricted", @@ -683,6 +689,7 @@ def test_api_documents_retrieve_authenticated_related_team_members( "content": document.content, "created_at": document.created_at.isoformat().replace("+00:00", "Z"), "creator": str(document.creator.id), + "deleted_at": None, "depth": 1, "excerpt": document.excerpt, "is_favorite": False, @@ -749,6 +756,7 @@ def test_api_documents_retrieve_authenticated_related_team_administrators( "content": document.content, "created_at": document.created_at.isoformat().replace("+00:00", "Z"), "creator": str(document.creator.id), + "deleted_at": None, "depth": 1, "excerpt": document.excerpt, "is_favorite": False, @@ -815,6 +823,7 @@ def test_api_documents_retrieve_authenticated_related_team_owners( "content": document.content, "created_at": document.created_at.isoformat().replace("+00:00", "Z"), "creator": str(document.creator.id), + "deleted_at": None, "depth": 1, "excerpt": document.excerpt, "is_favorite": False, diff --git a/src/backend/core/tests/documents/test_api_documents_trashbin.py b/src/backend/core/tests/documents/test_api_documents_trashbin.py index 28ea6a01a..0d82602b1 100644 --- a/src/backend/core/tests/documents/test_api_documents_trashbin.py +++ b/src/backend/core/tests/documents/test_api_documents_trashbin.py @@ -48,11 +48,11 @@ def test_api_documents_trashbin_format(): other_users = factories.UserFactory.create_batch(3) document = factories.DocumentFactory( - deleted_at=timezone.now(), users=factories.UserFactory.create_batch(2), favorited_by=[user, *other_users], link_traces=other_users, ) + document.soft_delete() factories.UserDocumentAccessFactory(document=document, user=user, role="owner") response = client.get("/api/v1.0/documents/trashbin/") @@ -113,6 +113,7 @@ def test_api_documents_trashbin_format(): "creator": str(document.creator.id), "depth": 1, "excerpt": document.excerpt, + "deleted_at": document.ancestors_deleted_at.isoformat().replace("+00:00", "Z"), "link_reach": document.link_reach, "link_role": document.link_role, "nb_accesses_ancestors": 0, diff --git a/src/backend/core/tests/documents/test_api_documents_tree.py b/src/backend/core/tests/documents/test_api_documents_tree.py index 0124b5075..bf0221e56 100644 --- a/src/backend/core/tests/documents/test_api_documents_tree.py +++ b/src/backend/core/tests/documents/test_api_documents_tree.py @@ -50,6 +50,7 @@ def test_api_documents_tree_list_anonymous_public_standalone(django_assert_num_q ), "creator": str(child.creator.id), "depth": 3, + "deleted_at": None, "excerpt": child.excerpt, "id": str(child.id), "is_favorite": False, @@ -73,6 +74,7 @@ def test_api_documents_tree_list_anonymous_public_standalone(django_assert_num_q "created_at": document.created_at.isoformat().replace("+00:00", "Z"), "creator": str(document.creator.id), "depth": 2, + "deleted_at": None, "excerpt": document.excerpt, "id": str(document.id), "is_favorite": False, @@ -96,6 +98,7 @@ def test_api_documents_tree_list_anonymous_public_standalone(django_assert_num_q "created_at": sibling1.created_at.isoformat().replace("+00:00", "Z"), "creator": str(sibling1.creator.id), "depth": 2, + "deleted_at": None, "excerpt": sibling1.excerpt, "id": str(sibling1.id), "is_favorite": False, @@ -119,6 +122,7 @@ def test_api_documents_tree_list_anonymous_public_standalone(django_assert_num_q "created_at": sibling2.created_at.isoformat().replace("+00:00", "Z"), "creator": str(sibling2.creator.id), "depth": 2, + "deleted_at": None, "excerpt": sibling2.excerpt, "id": str(sibling2.id), "is_favorite": False, @@ -138,6 +142,7 @@ def test_api_documents_tree_list_anonymous_public_standalone(django_assert_num_q "created_at": parent.created_at.isoformat().replace("+00:00", "Z"), "creator": str(parent.creator.id), "depth": 1, + "deleted_at": None, "excerpt": parent.excerpt, "id": str(parent.id), "is_favorite": False, @@ -210,6 +215,7 @@ def test_api_documents_tree_list_anonymous_public_parent(): ), "creator": str(child.creator.id), "depth": 5, + "deleted_at": None, "excerpt": child.excerpt, "id": str(child.id), "is_favorite": False, @@ -233,6 +239,7 @@ def test_api_documents_tree_list_anonymous_public_parent(): ), "creator": str(document.creator.id), "depth": 4, + "deleted_at": None, "excerpt": document.excerpt, "id": str(document.id), "is_favorite": False, @@ -260,6 +267,7 @@ def test_api_documents_tree_list_anonymous_public_parent(): ), "creator": str(document_sibling.creator.id), "depth": 4, + "deleted_at": None, "excerpt": document_sibling.excerpt, "id": str(document_sibling.id), "is_favorite": False, @@ -281,6 +289,7 @@ def test_api_documents_tree_list_anonymous_public_parent(): "created_at": parent.created_at.isoformat().replace("+00:00", "Z"), "creator": str(parent.creator.id), "depth": 3, + "deleted_at": None, "excerpt": parent.excerpt, "id": str(parent.id), "is_favorite": False, @@ -306,6 +315,7 @@ def test_api_documents_tree_list_anonymous_public_parent(): ), "creator": str(parent_sibling.creator.id), "depth": 3, + "deleted_at": None, "excerpt": parent_sibling.excerpt, "id": str(parent_sibling.id), "is_favorite": False, @@ -327,6 +337,7 @@ def test_api_documents_tree_list_anonymous_public_parent(): "created_at": grand_parent.created_at.isoformat().replace("+00:00", "Z"), "creator": str(grand_parent.creator.id), "depth": 2, + "deleted_at": None, "excerpt": grand_parent.excerpt, "id": str(grand_parent.id), "is_favorite": False, @@ -406,6 +417,7 @@ def test_api_documents_tree_list_authenticated_unrelated_public_or_authenticated ), "creator": str(child.creator.id), "depth": 3, + "deleted_at": None, "excerpt": child.excerpt, "id": str(child.id), "is_favorite": False, @@ -427,6 +439,7 @@ def test_api_documents_tree_list_authenticated_unrelated_public_or_authenticated "created_at": document.created_at.isoformat().replace("+00:00", "Z"), "creator": str(document.creator.id), "depth": 2, + "deleted_at": None, "excerpt": document.excerpt, "id": str(document.id), "is_favorite": False, @@ -450,6 +463,7 @@ def test_api_documents_tree_list_authenticated_unrelated_public_or_authenticated "created_at": sibling.created_at.isoformat().replace("+00:00", "Z"), "creator": str(sibling.creator.id), "depth": 2, + "deleted_at": None, "excerpt": sibling.excerpt, "id": str(sibling.id), "is_favorite": False, @@ -469,6 +483,7 @@ def test_api_documents_tree_list_authenticated_unrelated_public_or_authenticated "created_at": parent.created_at.isoformat().replace("+00:00", "Z"), "creator": str(parent.creator.id), "depth": 1, + "deleted_at": None, "excerpt": parent.excerpt, "id": str(parent.id), "is_favorite": False, @@ -546,6 +561,7 @@ def test_api_documents_tree_list_authenticated_public_or_authenticated_parent( ), "creator": str(child.creator.id), "depth": 5, + "deleted_at": None, "excerpt": child.excerpt, "id": str(child.id), "is_favorite": False, @@ -569,6 +585,7 @@ def test_api_documents_tree_list_authenticated_public_or_authenticated_parent( ), "creator": str(document.creator.id), "depth": 4, + "deleted_at": None, "excerpt": document.excerpt, "id": str(document.id), "is_favorite": False, @@ -596,6 +613,7 @@ def test_api_documents_tree_list_authenticated_public_or_authenticated_parent( ), "creator": str(document_sibling.creator.id), "depth": 4, + "deleted_at": None, "excerpt": document_sibling.excerpt, "id": str(document_sibling.id), "is_favorite": False, @@ -617,6 +635,7 @@ def test_api_documents_tree_list_authenticated_public_or_authenticated_parent( "created_at": parent.created_at.isoformat().replace("+00:00", "Z"), "creator": str(parent.creator.id), "depth": 3, + "deleted_at": None, "excerpt": parent.excerpt, "id": str(parent.id), "is_favorite": False, @@ -642,6 +661,7 @@ def test_api_documents_tree_list_authenticated_public_or_authenticated_parent( ), "creator": str(parent_sibling.creator.id), "depth": 3, + "deleted_at": None, "excerpt": parent_sibling.excerpt, "id": str(parent_sibling.id), "is_favorite": False, @@ -663,6 +683,7 @@ def test_api_documents_tree_list_authenticated_public_or_authenticated_parent( "created_at": grand_parent.created_at.isoformat().replace("+00:00", "Z"), "creator": str(grand_parent.creator.id), "depth": 2, + "deleted_at": None, "excerpt": grand_parent.excerpt, "id": str(grand_parent.id), "is_favorite": False, @@ -744,6 +765,7 @@ def test_api_documents_tree_list_authenticated_related_direct(): ), "creator": str(child.creator.id), "depth": 3, + "deleted_at": None, "excerpt": child.excerpt, "id": str(child.id), "is_favorite": False, @@ -765,6 +787,7 @@ def test_api_documents_tree_list_authenticated_related_direct(): "created_at": document.created_at.isoformat().replace("+00:00", "Z"), "creator": str(document.creator.id), "depth": 2, + "deleted_at": None, "excerpt": document.excerpt, "id": str(document.id), "is_favorite": False, @@ -788,6 +811,7 @@ def test_api_documents_tree_list_authenticated_related_direct(): "created_at": sibling.created_at.isoformat().replace("+00:00", "Z"), "creator": str(sibling.creator.id), "depth": 2, + "deleted_at": None, "excerpt": sibling.excerpt, "id": str(sibling.id), "is_favorite": False, @@ -807,6 +831,7 @@ def test_api_documents_tree_list_authenticated_related_direct(): "created_at": parent.created_at.isoformat().replace("+00:00", "Z"), "creator": str(parent.creator.id), "depth": 1, + "deleted_at": None, "excerpt": parent.excerpt, "id": str(parent.id), "is_favorite": False, @@ -888,6 +913,7 @@ def test_api_documents_tree_list_authenticated_related_parent(): ), "creator": str(child.creator.id), "depth": 5, + "deleted_at": None, "excerpt": child.excerpt, "id": str(child.id), "is_favorite": False, @@ -911,6 +937,7 @@ def test_api_documents_tree_list_authenticated_related_parent(): ), "creator": str(document.creator.id), "depth": 4, + "deleted_at": None, "excerpt": document.excerpt, "id": str(document.id), "is_favorite": False, @@ -938,6 +965,7 @@ def test_api_documents_tree_list_authenticated_related_parent(): ), "creator": str(document_sibling.creator.id), "depth": 4, + "deleted_at": None, "excerpt": document_sibling.excerpt, "id": str(document_sibling.id), "is_favorite": False, @@ -959,6 +987,7 @@ def test_api_documents_tree_list_authenticated_related_parent(): "created_at": parent.created_at.isoformat().replace("+00:00", "Z"), "creator": str(parent.creator.id), "depth": 3, + "deleted_at": None, "excerpt": parent.excerpt, "id": str(parent.id), "is_favorite": False, @@ -984,6 +1013,7 @@ def test_api_documents_tree_list_authenticated_related_parent(): ), "creator": str(parent_sibling.creator.id), "depth": 3, + "deleted_at": None, "excerpt": parent_sibling.excerpt, "id": str(parent_sibling.id), "is_favorite": False, @@ -1005,6 +1035,7 @@ def test_api_documents_tree_list_authenticated_related_parent(): "created_at": grand_parent.created_at.isoformat().replace("+00:00", "Z"), "creator": str(grand_parent.creator.id), "depth": 2, + "deleted_at": None, "excerpt": grand_parent.excerpt, "id": str(grand_parent.id), "is_favorite": False, @@ -1094,6 +1125,7 @@ def test_api_documents_tree_list_authenticated_related_team_members( ), "creator": str(child.creator.id), "depth": 3, + "deleted_at": None, "excerpt": child.excerpt, "id": str(child.id), "is_favorite": False, @@ -1115,6 +1147,7 @@ def test_api_documents_tree_list_authenticated_related_team_members( "created_at": document.created_at.isoformat().replace("+00:00", "Z"), "creator": str(document.creator.id), "depth": 2, + "deleted_at": None, "excerpt": document.excerpt, "id": str(document.id), "is_favorite": False, @@ -1138,6 +1171,7 @@ def test_api_documents_tree_list_authenticated_related_team_members( "created_at": sibling.created_at.isoformat().replace("+00:00", "Z"), "creator": str(sibling.creator.id), "depth": 2, + "deleted_at": None, "excerpt": sibling.excerpt, "id": str(sibling.id), "is_favorite": False, @@ -1157,6 +1191,7 @@ def test_api_documents_tree_list_authenticated_related_team_members( "created_at": parent.created_at.isoformat().replace("+00:00", "Z"), "creator": str(parent.creator.id), "depth": 1, + "deleted_at": None, "excerpt": parent.excerpt, "id": str(parent.id), "is_favorite": False, From f772801fd0e6ebc4bd50fc825259fb22bbefa6a1 Mon Sep 17 00:00:00 2001 From: Manuel Raynaud Date: Mon, 6 Oct 2025 08:50:06 +0200 Subject: [PATCH 03/10] =?UTF-8?q?=E2=99=BB=EF=B8=8F(backend)=20change=20ab?= =?UTF-8?q?ilities=20for=20deleted=20document?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The abilities for a deleted document were too open. We want to restrict them. Only the restore, retrieve and tree is allowed. The tree method will need some modifications to get the right informations. --- src/backend/core/models.py | 13 ++--- .../documents/test_api_documents_trashbin.py | 50 +++++++++---------- .../core/tests/test_models_documents.py | 38 +++++++++++++- 3 files changed, 68 insertions(+), 33 deletions(-) diff --git a/src/backend/core/models.py b/src/backend/core/models.py index d5e8cf9a8..941c72ec0 100644 --- a/src/backend/core/models.py +++ b/src/backend/core/models.py @@ -721,7 +721,7 @@ def get_abilities(self, user): # Characteristics that are based only on specific access is_owner = role == RoleChoices.OWNER - is_deleted = self.ancestors_deleted_at and not is_owner + is_deleted = self.ancestors_deleted_at is_owner_or_admin = (is_owner or role == RoleChoices.ADMIN) and not is_deleted # Compute access roles before adding link roles because we don't @@ -750,6 +750,7 @@ def get_abilities(self, user): role = RoleChoices.max(role, link_definition["link_role"]) can_get = bool(role) and not is_deleted + retrieve = can_get or is_owner can_update = ( is_owner_or_admin or role == RoleChoices.EDITOR ) and not is_deleted @@ -758,7 +759,7 @@ def get_abilities(self, user): is_owner if self.is_root() else (is_owner_or_admin or (user.is_authenticated and self.creator == user)) - ) + ) and not is_deleted ai_allow_reach_from = settings.AI_ALLOW_REACH_FROM ai_access = any( @@ -790,15 +791,15 @@ def get_abilities(self, user): "duplicate": can_get and user.is_authenticated, "favorite": can_get and user.is_authenticated, "link_configuration": is_owner_or_admin, - "invite_owner": is_owner, + "invite_owner": is_owner and not is_deleted, "mask": can_get and user.is_authenticated, - "move": is_owner_or_admin and not self.ancestors_deleted_at, + "move": is_owner_or_admin and not is_deleted, "partial_update": can_update, "restore": is_owner, - "retrieve": can_get, + "retrieve": retrieve, "media_auth": can_get, "link_select_options": link_select_options, - "tree": can_get, + "tree": retrieve, "update": can_update, "versions_destroy": is_owner_or_admin, "versions_list": has_access_role, diff --git a/src/backend/core/tests/documents/test_api_documents_trashbin.py b/src/backend/core/tests/documents/test_api_documents_trashbin.py index 0d82602b1..fbcc23175 100644 --- a/src/backend/core/tests/documents/test_api_documents_trashbin.py +++ b/src/backend/core/tests/documents/test_api_documents_trashbin.py @@ -70,40 +70,40 @@ def test_api_documents_trashbin_format(): assert results[0] == { "id": str(document.id), "abilities": { - "accesses_manage": True, - "accesses_view": True, - "ai_transform": True, - "ai_translate": True, - "attachment_upload": True, - "can_edit": True, - "children_create": True, - "children_list": True, - "collaboration_auth": True, - "descendants": True, - "cors_proxy": True, - "content": True, - "destroy": True, - "duplicate": True, - "favorite": True, - "invite_owner": True, - "link_configuration": True, + "accesses_manage": False, + "accesses_view": False, + "ai_transform": False, + "ai_translate": False, + "attachment_upload": False, + "can_edit": False, + "children_create": False, + "children_list": False, + "collaboration_auth": False, + "descendants": False, + "cors_proxy": False, + "content": False, + "destroy": False, + "duplicate": False, + "favorite": False, + "invite_owner": False, + "link_configuration": False, "link_select_options": { "authenticated": ["reader", "editor"], "public": ["reader", "editor"], "restricted": None, }, - "mask": True, - "media_auth": True, - "media_check": True, + "mask": False, + "media_auth": False, + "media_check": False, "move": False, # Can't move a deleted document - "partial_update": True, + "partial_update": False, "restore": True, "retrieve": True, "tree": True, - "update": True, - "versions_destroy": True, - "versions_list": True, - "versions_retrieve": True, + "update": False, + "versions_destroy": False, + "versions_list": False, + "versions_retrieve": False, }, "ancestors_link_reach": None, "ancestors_link_role": None, diff --git a/src/backend/core/tests/test_models_documents.py b/src/backend/core/tests/test_models_documents.py index cc760aff3..69236b6e9 100644 --- a/src/backend/core/tests/test_models_documents.py +++ b/src/backend/core/tests/test_models_documents.py @@ -375,8 +375,42 @@ def test_models_documents_get_abilities_owner(django_assert_num_queries): document.soft_delete() document.refresh_from_db() - expected_abilities["move"] = False - assert document.get_abilities(user) == expected_abilities + assert document.get_abilities(user) == { + "accesses_manage": False, + "accesses_view": False, + "ai_transform": False, + "ai_translate": False, + "attachment_upload": False, + "can_edit": False, + "children_create": False, + "children_list": False, + "collaboration_auth": False, + "descendants": False, + "cors_proxy": False, + "content": False, + "destroy": False, + "duplicate": False, + "favorite": False, + "invite_owner": False, + "link_configuration": False, + "link_select_options": { + "authenticated": ["reader", "editor"], + "public": ["reader", "editor"], + "restricted": None, + }, + "mask": False, + "media_auth": False, + "media_check": False, + "move": False, + "partial_update": False, + "restore": True, + "retrieve": True, + "tree": True, + "update": False, + "versions_destroy": False, + "versions_list": False, + "versions_retrieve": False, + } @override_settings( From 31389bcae20f6c53353aff1ce23b06ffa23c5144 Mon Sep 17 00:00:00 2001 From: Manuel Raynaud Date: Mon, 6 Oct 2025 11:25:17 +0200 Subject: [PATCH 04/10] =?UTF-8?q?=E2=99=BB=EF=B8=8F(backend)=20open=20tree?= =?UTF-8?q?=20endpoint=20to=20deleted=20documents=20only=20for=20owners?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The tree endpoint will now return a result only for owners. For other users the endpoint still returns a 403. Also, the endpoint does look for ancestors anymore, it only stay on the current document. --- src/backend/core/api/viewsets.py | 56 ++++++++++++------- .../documents/test_api_documents_tree.py | 53 ++++++++++++++++++ 2 files changed, 88 insertions(+), 21 deletions(-) diff --git a/src/backend/core/api/viewsets.py b/src/backend/core/api/viewsets.py index 1455f2166..2cd45e623 100644 --- a/src/backend/core/api/viewsets.py +++ b/src/backend/core/api/viewsets.py @@ -851,33 +851,47 @@ def tree(self, request, pk, *args, **kwargs): try: current_document = ( - self.queryset.select_related(None).only("depth", "path").get(pk=pk) + self.queryset.select_related(None) + .only("depth", "path", "ancestors_deleted_at") + .get(pk=pk) ) except models.Document.DoesNotExist as excpt: raise drf.exceptions.NotFound() from excpt - ancestors = ( - ( - current_document.get_ancestors() - | self.queryset.select_related(None).filter(pk=pk) - ) - .filter(ancestors_deleted_at__isnull=True) - .order_by("path") - ) + is_deleted = current_document.ancestors_deleted_at is not None - # Get the highest readable ancestor - highest_readable = ( - ancestors.select_related(None) - .readable_per_se(request.user) - .only("depth", "path") - .first() - ) - if highest_readable is None: - raise ( - drf.exceptions.PermissionDenied() - if request.user.is_authenticated - else drf.exceptions.NotAuthenticated() + if is_deleted: + if current_document.get_role(user) != models.RoleChoices.OWNER: + raise ( + drf.exceptions.PermissionDenied() + if request.user.is_authenticated + else drf.exceptions.NotAuthenticated() + ) + highest_readable = current_document + ancestors = self.queryset.select_related(None).filter(pk=pk) + else: + ancestors = ( + ( + current_document.get_ancestors() + | self.queryset.select_related(None).filter(pk=pk) + ) + .filter(ancestors_deleted_at__isnull=True) + .order_by("path") ) + # Get the highest readable ancestor + highest_readable = ( + ancestors.select_related(None) + .readable_per_se(request.user) + .only("depth", "path") + .first() + ) + + if highest_readable is None: + raise ( + drf.exceptions.PermissionDenied() + if request.user.is_authenticated + else drf.exceptions.NotAuthenticated() + ) paths_links_mapping = {} ancestors_links = [] children_clause = db.Q() diff --git a/src/backend/core/tests/documents/test_api_documents_tree.py b/src/backend/core/tests/documents/test_api_documents_tree.py index bf0221e56..c86eebc16 100644 --- a/src/backend/core/tests/documents/test_api_documents_tree.py +++ b/src/backend/core/tests/documents/test_api_documents_tree.py @@ -1205,3 +1205,56 @@ def test_api_documents_tree_list_authenticated_related_team_members( "updated_at": parent.updated_at.isoformat().replace("+00:00", "Z"), "user_role": access.role, } + + +def test_api_documents_tree_list_deleted_document(): + """ + Tree of a deleted document should only be accessible to the owner. + """ + user = factories.UserFactory() + client = APIClient() + client.force_login(user) + + parent = factories.DocumentFactory(link_reach="public") + document, _ = factories.DocumentFactory.create_batch(2, parent=parent) + factories.DocumentFactory(link_reach="public", parent=document) + + document.soft_delete() + + response = client.get(f"/api/v1.0/documents/{document.id!s}/tree/") + assert response.status_code == 403 + + +def test_api_documents_tree_list_deleted_document_owner(django_assert_num_queries): + """ + Tree of a deleted document should only be accessible to the owner. + """ + user = factories.UserFactory() + client = APIClient() + client.force_login(user) + + parent = factories.DocumentFactory(link_reach="public", users=[(user, "owner")]) + document, _ = factories.DocumentFactory.create_batch(2, parent=parent) + child = factories.DocumentFactory(parent=document) + + document.soft_delete() + document.refresh_from_db() + child.refresh_from_db() + + with django_assert_num_queries(9): + client.get(f"/api/v1.0/documents/{document.id!s}/tree/") + + with django_assert_num_queries(5): + response = client.get(f"/api/v1.0/documents/{document.id!s}/tree/") + + assert response.status_code == 200 + content = response.json() + assert content["id"] == str(document.id) + assert content["deleted_at"] == document.deleted_at.isoformat().replace( + "+00:00", "Z" + ) + assert len(content["children"]) == 1 + assert content["children"][0]["id"] == str(child.id) + assert content["children"][0][ + "deleted_at" + ] == child.ancestors_deleted_at.isoformat().replace("+00:00", "Z") From 2c1a9ff74fb6943ba2e92d53b0ca036927b3f061 Mon Sep 17 00:00:00 2001 From: Anthony LC Date: Wed, 8 Oct 2025 12:54:57 +0200 Subject: [PATCH 05/10] =?UTF-8?q?=F0=9F=8D=B1(frontend)=20add=20material-s?= =?UTF-8?q?ymbols-outlined=20font?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The design uses Material Symbols for icons. This commit adds the font to the project and updates the Icon component to be able to use it. --- src/frontend/apps/impress/package.json | 1 + src/frontend/apps/impress/src/components/Icon.tsx | 13 ++++++++++--- src/frontend/apps/impress/src/pages/globals.css | 9 +++++++-- src/frontend/yarn.lock | 5 +++++ 4 files changed, 23 insertions(+), 5 deletions(-) diff --git a/src/frontend/apps/impress/package.json b/src/frontend/apps/impress/package.json index 69dc0045c..837da970c 100644 --- a/src/frontend/apps/impress/package.json +++ b/src/frontend/apps/impress/package.json @@ -31,6 +31,7 @@ "@emoji-mart/data": "1.2.1", "@emoji-mart/react": "1.1.1", "@fontsource-variable/inter": "5.2.8", + "@fontsource-variable/material-symbols-outlined": "5.2.25", "@fontsource/material-icons": "5.2.5", "@gouvfr-lasuite/integration": "1.0.3", "@gouvfr-lasuite/ui-kit": "0.16.1", diff --git a/src/frontend/apps/impress/src/components/Icon.tsx b/src/frontend/apps/impress/src/components/Icon.tsx index b0820169d..fd1458a82 100644 --- a/src/frontend/apps/impress/src/components/Icon.tsx +++ b/src/frontend/apps/impress/src/components/Icon.tsx @@ -4,12 +4,16 @@ import { css } from 'styled-components'; import { Text, TextType } from '@/components'; type IconProps = TextType & { + disabled?: boolean; iconName: string; - variant?: 'filled' | 'outlined'; + variant?: 'filled' | 'outlined' | 'symbols-outlined'; }; export const Icon = ({ + className, iconName, + disabled, variant = 'outlined', + $variation, ...textProps }: IconProps) => { const hasLabel = 'aria-label' in textProps || 'aria-labelledby' in textProps; @@ -18,12 +22,15 @@ export const Icon = ({ return ( {iconName} diff --git a/src/frontend/apps/impress/src/pages/globals.css b/src/frontend/apps/impress/src/pages/globals.css index b09c0d5f1..aa9a83854 100644 --- a/src/frontend/apps/impress/src/pages/globals.css +++ b/src/frontend/apps/impress/src/pages/globals.css @@ -1,6 +1,7 @@ @import url('../cunningham/cunningham-style.css'); @import url('@fontsource/material-icons'); @import url('@fontsource/material-icons-outlined'); +@import url('@fontsource-variable/material-symbols-outlined'); @import url('@fontsource-variable/inter'); @import url('/assets/fonts/Marianne/Marianne-font.css'); @@ -47,13 +48,13 @@ main ::-webkit-scrollbar-thumb:hover, } .material-icons, -.material-icons-filled { +.material-icons-filled, +.material-symbols-outlined { font-family: 'Material Icons Outlined', 'Material Icons', sans-serif; font-weight: normal; font-style: normal; font-size: 24px; /* Preferred icon size */ display: inline-block; - line-height: 1; text-transform: none; letter-spacing: normal; overflow-wrap: normal; @@ -77,6 +78,10 @@ main ::-webkit-scrollbar-thumb:hover, font-family: 'Material Icons', sans-serif; } +.material-symbols-outlined { + font-family: 'Material Symbols Outlined Variable', sans-serif; +} + [data-nextjs-dialog-overlay] { display: none !important; } diff --git a/src/frontend/yarn.lock b/src/frontend/yarn.lock index 61406b858..379e67a78 100644 --- a/src/frontend/yarn.lock +++ b/src/frontend/yarn.lock @@ -1714,6 +1714,11 @@ resolved "https://registry.yarnpkg.com/@fontsource-variable/inter/-/inter-5.2.8.tgz#29b11476f5149f6a443b4df6516e26002d87941a" integrity sha512-kOfP2D+ykbcX/P3IFnokOhVRNoTozo5/JxhAIVYLpea/UBmCQ/YWPBfWIDuBImXX/15KH+eKh4xpEUyS2sQQGQ== +"@fontsource-variable/material-symbols-outlined@5.2.25": + version "5.2.25" + resolved "https://registry.yarnpkg.com/@fontsource-variable/material-symbols-outlined/-/material-symbols-outlined-5.2.25.tgz#c2ec742ca9d890a408cb657c65ddc1a6da8f3085" + integrity sha512-SEUmtSiqxVpLcOd1S5tVCnTXy3I2PBm/dZH/rvpfEady6Ab+l+rwIqdbjEqb6NrggcX8usGJfGYpN9cY6QCuJg== + "@fontsource-variable/roboto-flex@5.2.5": version "5.2.5" resolved "https://registry.yarnpkg.com/@fontsource-variable/roboto-flex/-/roboto-flex-5.2.5.tgz#38368ea754697c2fdf08df11b06e8b6d391ff4c1" From 37138c1a2375ef7312686d0fa1cf2a7c46e3529d Mon Sep 17 00:00:00 2001 From: Anthony LC Date: Fri, 3 Oct 2025 12:45:25 +0200 Subject: [PATCH 06/10] =?UTF-8?q?=E2=9C=A8(frontend)=20add=20trashbin=20li?= =?UTF-8?q?st?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit List the docs deleted in the trashbin list, it is displayed in the docs grid. --- CHANGELOG.md | 1 + .../__tests__/app-impress/doc-grid.spec.ts | 2 +- .../app-impress/doc-trashbin.spec.ts | 55 +++++++ .../e2e/__tests__/app-impress/utils-common.ts | 13 +- .../doc-management/assets/child-document.svg | 87 +++++++++++ .../components/SimpleDocItem.tsx | 27 +++- .../features/docs/doc-management/types.tsx | 2 + .../docs/docs-grid/components/DocsGrid.tsx | 59 ++++++-- .../docs-grid/components/DocsGridActions.tsx | 4 +- .../docs-grid/components/DocsGridItem.tsx | 65 ++++++-- .../components/DocsGridItemSharedButton.tsx | 21 ++- .../__tests__/DocsGridItemDate.test.tsx | 139 ++++++++++++++++++ .../components/LefPanelTargetFilters.tsx | 5 + .../service-worker/plugins/ApiPlugin.ts | 1 + .../apps/impress/src/hook/useDate.tsx | 14 +- .../apps/impress/src/i18n/translations.json | 4 +- 16 files changed, 460 insertions(+), 39 deletions(-) create mode 100644 src/frontend/apps/e2e/__tests__/app-impress/doc-trashbin.spec.ts create mode 100644 src/frontend/apps/impress/src/features/docs/doc-management/assets/child-document.svg create mode 100644 src/frontend/apps/impress/src/features/docs/docs-grid/components/__tests__/DocsGridItemDate.test.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f7726498..7adc56396 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to ### Added - ✨(frontend) add pdf block to the editor #1293 +- ✨List and restore deleted docs #1450 ### Changed diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-grid.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-grid.spec.ts index 0347d804a..1e12421fd 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/doc-grid.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-grid.spec.ts @@ -139,7 +139,7 @@ test.describe('Document grid item options', () => { const row = await getGridRow(page, docTitle); await row.getByText(`more_horiz`).click(); - await page.getByRole('menuitem', { name: 'Remove' }).click(); + await page.getByRole('menuitem', { name: 'Delete' }).click(); await expect( page.getByRole('heading', { name: 'Delete a doc' }), diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-trashbin.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-trashbin.spec.ts new file mode 100644 index 000000000..6f1a56dab --- /dev/null +++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-trashbin.spec.ts @@ -0,0 +1,55 @@ +import { expect, test } from '@playwright/test'; + +import { + clickInGridMenu, + createDoc, + getGridRow, + verifyDocName, +} from './utils-common'; +import { addNewMember } from './utils-share'; + +test.beforeEach(async ({ page }) => { + await page.goto('/'); +}); + +test.describe('Doc Trashbin', () => { + test('it controls UI and interaction from the grid page', async ({ + page, + browserName, + }) => { + const [title1] = await createDoc(page, 'my-trash-doc-1', browserName, 1); + const [title2] = await createDoc(page, 'my-trash-doc-2', browserName, 1); + await verifyDocName(page, title2); + await page.getByRole('button', { name: 'Share' }).click(); + await addNewMember(page, 0, 'Editor'); + await page.getByRole('button', { name: 'close' }).click(); + + await page.getByRole('button', { name: 'Back to homepage' }).click(); + + const row1 = await getGridRow(page, title1); + await clickInGridMenu(page, row1, 'Delete'); + await page.getByRole('button', { name: 'Delete document' }).click(); + + const row2 = await getGridRow(page, title2); + await clickInGridMenu(page, row2, 'Delete'); + await page.getByRole('button', { name: 'Delete document' }).click(); + + await page.getByRole('link', { name: 'Trashbin' }).click(); + + const docsGrid = page.getByTestId('docs-grid'); + await expect(docsGrid.getByText('Days remaining')).toBeVisible(); + await expect(row1.getByText(title1)).toBeVisible(); + await expect(row1.getByText('30 days')).toBeVisible(); + await expect(row2.getByText(title2)).toBeVisible(); + await expect( + row2.getByRole('button', { + name: 'Open the sharing settings for the document', + }), + ).toBeVisible(); + await expect( + row2.getByRole('button', { + name: 'Open the sharing settings for the document', + }), + ).toBeDisabled(); + }); +}); diff --git a/src/frontend/apps/e2e/__tests__/app-impress/utils-common.ts b/src/frontend/apps/e2e/__tests__/app-impress/utils-common.ts index 77b25f7da..cba597926 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/utils-common.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/utils-common.ts @@ -1,4 +1,4 @@ -import { Page, expect } from '@playwright/test'; +import { Locator, Page, expect } from '@playwright/test'; export type BrowserName = 'chromium' | 'firefox' | 'webkit'; export const BROWSERS: BrowserName[] = ['chromium', 'webkit', 'firefox']; @@ -326,3 +326,14 @@ export async function waitForLanguageSwitch( await page.getByRole('menuitem', { name: lang.label }).click(); } + +export const clickInGridMenu = async ( + page: Page, + row: Locator, + textButton: string, +) => { + await row + .getByRole('button', { name: /Open the menu of actions for the document/ }) + .click(); + await page.getByRole('menuitem', { name: textButton }).click(); +}; diff --git a/src/frontend/apps/impress/src/features/docs/doc-management/assets/child-document.svg b/src/frontend/apps/impress/src/features/docs/doc-management/assets/child-document.svg new file mode 100644 index 000000000..008a9adde --- /dev/null +++ b/src/frontend/apps/impress/src/features/docs/doc-management/assets/child-document.svg @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/src/frontend/apps/impress/src/features/docs/doc-management/components/SimpleDocItem.tsx b/src/frontend/apps/impress/src/features/docs/doc-management/components/SimpleDocItem.tsx index 41a753a47..27b31cd1a 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-management/components/SimpleDocItem.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-management/components/SimpleDocItem.tsx @@ -4,9 +4,15 @@ import { css } from 'styled-components'; import { Box, Text } from '@/components'; import { useCunninghamTheme } from '@/cunningham'; -import { Doc, getEmojiAndTitle, useTrans } from '@/docs/doc-management'; +import { + Doc, + getEmojiAndTitle, + useDocUtils, + useTrans, +} from '@/docs/doc-management'; import { useResponsiveStore } from '@/stores'; +import ChildDocument from '../assets/child-document.svg'; import PinnedDocumentIcon from '../assets/pinned-document.svg'; import SimpleFileIcon from '../assets/simple-document.svg'; @@ -37,6 +43,7 @@ export const SimpleDocItem = ({ const { spacingsTokens, colorsTokens } = useCunninghamTheme(); const { isDesktop } = useResponsiveStore(); const { untitledDocument } = useTrans(); + const { isChild } = useDocUtils(doc); const { emoji, titleWithoutEmoji: displayTitle } = getEmojiAndTitle( doc.title || untitledDocument, @@ -73,11 +80,19 @@ export const SimpleDocItem = ({