Compare commits

...

8 Commits

Author SHA1 Message Date
Anthony LC a11258f778 🔖(patch) release 3.8.2
Fixed:

- 🐛(service-worker) fix sw registration and page reload
  logic
2025-10-17 15:54:56 +02:00
Anthony LC 33647f124f 🐛(service-worker) fix sw registration and page reload logic
When a new service worker is installed, the page
was reloaded to ensure the new service worker took
control, it is not a big issue in normal browsing mode
because the service worker is only updated once in a
while (every release).
However, in incognito mode, the service worker has to be
re-registered on each new session, which means that
the page was reloading each time the user opened a
new incognito window, creating a bad user experience.
We now take in consideration the case where the
service-worker is installed for the first time, and don't
reload if it is this case.
2025-10-17 15:14:04 +02:00
Anthony LC e339cda5c6 🔖(patch) release 3.8.1
Fixed:
- ️(backend) improve trashbin endpoint performance
- 🐛(backend) manage invitation partial update without email
- (frontend) improve accessibility:
  -  add missing aria-label to add sub-doc button
  for accessibility
  -  add missing aria-label to more options button
  on sub-docs

Removed:
- 🔥(backend) remove treebeard form for the document admin
2025-10-17 10:41:38 +02:00
Manuel Raynaud 4ce65c654f 🔥(backend) remove treebeard form for the document admin
The document change admin page is unusable. The django treebeard library
can change the form used by one provided but this one is really slow.
And it is collapsing the configuration made with the other fields and
readonly fields declared on the DocumentAdmin class. In a first time we
remove the form usage, it seems useless. Later we have to provide more
information on this admin page.
2025-10-17 08:35:22 +00:00
Manuel Raynaud c048b2ae95 🐛(backend) manage invitation partial update without email
An invitation can be updated to change its role. The front use a PATCH
sending only the changed role, so the email is missing in the
InivtationSerializer.validate method. We have to check first if an email
is present before working on it.
2025-10-16 15:26:02 +00:00
Manuel Raynaud 5908afb098 ️(backend) improve trashbin endpoint performance (#1495)
The trashbin endpoint is slow. To filter documents the user has owner
access, we use a subquery to compute the roles and then filter on this
subquery. This is very slow. To improve it, we use the same way to
filter children used in the tree endpoint. First we look for all highest
ancestors the user has access on with the owner role. Then we create one
queryset filtering on all the docs starting by the given path and are
deleted.
2025-10-16 17:06:47 +02:00
Cyril e2298a3658 (frontend) add missing aria-label to more options button on sub-docs
improves accessibility by making the options button screen reader friendly

Signed-off-by: Cyril <c.gromoff@gmail.com>
2025-10-16 15:02:04 +02:00
Cyril 278eb233e9 (frontend) add missing aria-label to add sub-doc button for a11y
improves screen reader support for the add sub-doc action in the document tree

Signed-off-by: Cyril <c.gromoff@gmail.com>
2025-10-16 14:18:55 +02:00
19 changed files with 157 additions and 59 deletions
+24 -1
View File
@@ -6,6 +6,27 @@ and this project adheres to
## [Unreleased]
## [3.8.2] - 2025-10-17
### Fixed
🐛(service-worker) fix sw registration and page reload logic #1500
## [3.8.1] - 2025-10-17
### Fixed
- ⚡️(backend) improve trashbin endpoint performance #1495
- 🐛(backend) manage invitation partial update without email #1494
- ♿(frontend) improve accessibility:
- ♿ add missing aria-label to add sub-doc button for accessibility #1480
- ♿ add missing aria-label to more options button on sub-docs #1481
### Removed
- 🔥(backend) remove treebeard form for the document admin #1470
## [3.8.0] - 2025-10-14
### Added
@@ -788,7 +809,9 @@ and this project adheres to
- ✨(frontend) Coming Soon page (#67)
- 🚀 Impress, project to manage your documents easily and collaboratively.
[unreleased]: https://github.com/suitenumerique/docs/compare/v3.8.0...main
[unreleased]: https://github.com/suitenumerique/docs/compare/v3.8.2...main
[v3.8.2]: https://github.com/suitenumerique/docs/releases/v3.8.2
[v3.8.1]: https://github.com/suitenumerique/docs/releases/v3.8.1
[v3.8.0]: https://github.com/suitenumerique/docs/releases/v3.8.0
[v3.7.0]: https://github.com/suitenumerique/docs/releases/v3.7.0
[v3.6.0]: https://github.com/suitenumerique/docs/releases/v3.6.0
-2
View File
@@ -5,7 +5,6 @@ from django.contrib.auth import admin as auth_admin
from django.utils.translation import gettext_lazy as _
from treebeard.admin import TreeAdmin
from treebeard.forms import movenodeform_factory
from . import models
@@ -157,7 +156,6 @@ class DocumentAdmin(TreeAdmin):
},
),
)
form = movenodeform_factory(models.Document)
inlines = (DocumentAccessInline,)
list_display = (
"id",
+2 -1
View File
@@ -749,7 +749,8 @@ class InvitationSerializer(serializers.ModelSerializer):
if self.instance is None:
attrs["issuer"] = user
attrs["email"] = attrs["email"].lower()
if attrs.get("email"):
attrs["email"] = attrs["email"].lower()
return attrs
+18 -1
View File
@@ -623,12 +623,29 @@ class DocumentViewSet(
The selected documents are those deleted within the cutoff period defined in the
settings (see TRASHBIN_CUTOFF_DAYS), before they are considered permanently deleted.
"""
if not request.user.is_authenticated:
return self.get_response_for_queryset(self.queryset.none())
access_documents_paths = (
models.DocumentAccess.objects.select_related("document")
.filter(
db.Q(user=self.request.user) | db.Q(team__in=self.request.user.teams),
role=models.RoleChoices.OWNER,
)
.values_list("document__path", flat=True)
)
children_clause = db.Q()
for path in access_documents_paths:
children_clause |= db.Q(path__startswith=path)
queryset = self.queryset.filter(
children_clause,
deleted_at__isnull=False,
deleted_at__gte=models.get_trashbin_cutoff(),
)
queryset = queryset.annotate_user_roles(self.request.user)
queryset = queryset.filter(user_roles__contains=[models.RoleChoices.OWNER])
return self.get_response_for_queryset(queryset)
@@ -769,6 +769,37 @@ def test_api_document_invitations_update_authenticated_unprivileged(
assert value == old_invitation_values[key]
@pytest.mark.parametrize("via", VIA)
@pytest.mark.parametrize("role", ["administrator", "owner"])
def test_api_document_invitations_patch(via, role, mock_user_teams):
"""Partially updating an invitation should be allowed."""
user = factories.UserFactory()
invitation = factories.InvitationFactory(role="editor")
if via == USER:
factories.UserDocumentAccessFactory(
document=invitation.document, user=user, role=role
)
elif via == TEAM:
mock_user_teams.return_value = ["lasuite", "unknown"]
factories.TeamDocumentAccessFactory(
document=invitation.document, team="lasuite", role=role
)
client = APIClient()
client.force_login(user)
response = client.patch(
f"/api/v1.0/documents/{invitation.document.id!s}/invitations/{invitation.id!s}/",
{"role": "reader"},
format="json",
)
assert response.status_code == 200
invitation.refresh_from_db()
assert invitation.role == "reader"
# Delete
@@ -166,10 +166,10 @@ def test_api_documents_trashbin_authenticated_direct(django_assert_num_queries):
expected_ids = {str(document1.id), str(document2.id), str(document3.id)}
with django_assert_num_queries(10):
with django_assert_num_queries(11):
response = client.get("/api/v1.0/documents/trashbin/")
with django_assert_num_queries(4):
with django_assert_num_queries(5):
response = client.get("/api/v1.0/documents/trashbin/")
assert response.status_code == 200
@@ -208,10 +208,10 @@ def test_api_documents_trashbin_authenticated_via_team(
expected_ids = {str(deleted_document_team1.id), str(deleted_document_team2.id)}
with django_assert_num_queries(7):
with django_assert_num_queries(8):
response = client.get("/api/v1.0/documents/trashbin/")
with django_assert_num_queries(3):
with django_assert_num_queries(4):
response = client.get("/api/v1.0/documents/trashbin/")
assert response.status_code == 200
+1 -1
View File
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "impress"
version = "3.8.0"
version = "3.8.2"
authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }]
classifiers = [
"Development Status :: 5 - Production/Stable",
@@ -202,9 +202,19 @@ test.describe('Document create member', () => {
);
await expect(userInvitation).toBeVisible();
const responsePromisePatchInvitation = page.waitForResponse(
(response) =>
response.url().includes('/invitations/') &&
response.status() === 200 &&
response.request().method() === 'PATCH',
);
await userInvitation.getByLabel('doc-role-dropdown').click();
await page.getByRole('menuitem', { name: 'Reader' }).click();
const responsePatchInvitation = await responsePromisePatchInvitation;
expect(responsePatchInvitation.ok()).toBeTruthy();
const moreActions = userInvitation.getByRole('button', {
name: 'Open invitation actions menu',
});
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "app-e2e",
"version": "3.8.0",
"version": "3.8.2",
"repository": "https://github.com/suitenumerique/docs",
"author": "DINUM",
"license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "app-impress",
"version": "3.8.0",
"version": "3.8.2",
"repository": "https://github.com/suitenumerique/docs",
"author": "DINUM",
"license": "MIT",
@@ -152,7 +152,6 @@ export const DocTreeItemActions = ({
options={options}
isOpen={isOpen}
onOpenChange={onOpenChange}
aria-label={t('Open document actions menu')}
>
<Icon
onClick={(e) => {
@@ -164,7 +163,7 @@ export const DocTreeItemActions = ({
variant="filled"
$theme="primary"
$variation="600"
aria-hidden="true"
aria-label={t('More options')}
/>
</DropdownMenu>
{doc.abilities.children_create && (
@@ -178,6 +177,7 @@ export const DocTreeItemActions = ({
});
}}
color="primary"
aria-label={t('Add a sub page')}
data-testid="doc-tree-item-actions-add-child"
>
<Icon
@@ -3,51 +3,69 @@ import { useEffect } from 'react';
export const useSWRegister = () => {
useEffect(() => {
if (
'serviceWorker' in navigator &&
process.env.NEXT_PUBLIC_SW_DEACTIVATED !== 'true'
!('serviceWorker' in navigator) ||
process.env.NEXT_PUBLIC_SW_DEACTIVATED === 'true'
) {
navigator.serviceWorker
.register(`/service-worker.js`)
.then((registration) => {
registration.onupdatefound = () => {
const newWorker = registration.installing;
if (!newWorker) {
return;
return;
}
const hadControllerAtStart = !!navigator.serviceWorker.controller;
navigator.serviceWorker
.register(`/service-worker.js`)
.then((registration) => {
registration.onupdatefound = () => {
const newWorker = registration.installing;
if (!newWorker) {
return;
}
newWorker.onstatechange = () => {
if (
newWorker.state === 'installed' &&
navigator.serviceWorker.controller
) {
newWorker.postMessage({ type: 'SKIP_WAITING' });
}
newWorker.onstatechange = () => {
if (
newWorker.state === 'installed' &&
navigator.serviceWorker.controller
) {
newWorker.postMessage({ type: 'SKIP_WAITING' });
}
};
};
})
.catch((err) => {
console.error('Service worker registration failed:', err);
});
};
})
.catch((err) => {
console.error('Service worker registration failed:', err);
});
let refreshing = false;
const onControllerChange = () => {
if (refreshing) {
return;
}
refreshing = true;
let refreshing = false;
const onControllerChange = () => {
if (!hadControllerAtStart || refreshing) {
return;
}
refreshing = true;
if (document.visibilityState === 'visible') {
window.location.reload();
return;
}
const onVisible = () => {
if (document.visibilityState === 'visible') {
window.location.reload();
}
};
navigator.serviceWorker.addEventListener(
document.addEventListener('visibilitychange', onVisible, { once: true });
};
navigator.serviceWorker.addEventListener(
'controllerchange',
onControllerChange,
);
return () => {
navigator.serviceWorker.removeEventListener(
'controllerchange',
onControllerChange,
);
return () => {
navigator.serviceWorker.removeEventListener(
'controllerchange',
onControllerChange,
);
};
}
};
}, []);
};
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "impress",
"version": "3.8.0",
"version": "3.8.2",
"private": true,
"repository": "https://github.com/suitenumerique/docs",
"author": "DINUM",
@@ -1,6 +1,6 @@
{
"name": "eslint-plugin-docs",
"version": "3.8.0",
"version": "3.8.2",
"repository": "https://github.com/suitenumerique/docs",
"author": "DINUM",
"license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "packages-i18n",
"version": "3.8.0",
"version": "3.8.2",
"repository": "https://github.com/suitenumerique/docs",
"author": "DINUM",
"license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "server-y-provider",
"version": "3.8.0",
"version": "3.8.2",
"description": "Y.js provider for docs",
"repository": "https://github.com/suitenumerique/docs",
"license": "MIT",
+2 -2
View File
@@ -1,10 +1,10 @@
environments:
dev:
values:
- version: 3.8.0
- version: 3.8.2
feature:
values:
- version: 3.8.0
- version: 3.8.2
feature: ci
domain: example.com
imageTag: demo
+1 -1
View File
@@ -1,5 +1,5 @@
apiVersion: v2
type: application
name: docs
version: 3.8.0
version: 3.8.2
appVersion: latest
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "mail_mjml",
"version": "3.8.0",
"version": "3.8.2",
"description": "An util to generate html and text django's templates from mjml templates",
"type": "module",
"dependencies": {