Compare commits

...

13 Commits

Author SHA1 Message Date
Anthony LC d12b608db9 🔖(patch) release 3.4.1
Fixed:
- 🌐(frontend) keep simple tag during export
- 🐛(back) manage can-edit endpoint without created room in the ws
- 🐛(frontend) fix action buttons not clickable
- 🐛(frontend) fix crash share modal on grid options
2025-07-15 16:14:43 +02:00
Anthony LC 08a0eb59c8 🐛(frontend) fix crash share modal on grid options
The share modal in the DocsGridItem component
was crashing when opened due to a provider not
initialized.
2025-07-15 11:36:44 +02:00
renovate[bot] 0afc50fb93 ⬆️(dependencies) update js dependencies 2025-07-15 03:15:51 +00:00
renovate[bot] c48a4309c1 ⬆️(dependencies) update python dependencies 2025-07-11 06:14:43 +00:00
Anthony LC a212417fb8 🐛(frontend) fix action buttons not clickable (#1162)
If the title was too long, or the children deepness too deep, the action
buttons in the doc tree were not clickable.
This commit fixes the issue by ensuring that the action buttons are
always clickable, regardless of the title length or children depth.
2025-07-11 08:13:01 +02:00
Manuel Raynaud 500d4ea5ac 🐛(back) manage can-edit endpoint without created room in the ws (#1152)
In a scenario where the first user is editing a docs without websocket
and nobody has reached the websocket server first, the y-provider
service will return a 404 and we don't handle this case in the can-edit
endpoint leading to a server error.
2025-07-10 12:24:38 +00:00
Anthony LC 8a057b9c39 🌐(i18n) update translated strings
Update translated files with new translations
2025-07-10 12:48:52 +02:00
Anthony LC 6a12ac560e 🌐(frontend) keep simple tag during export
When we export translations, we want to keep the
simple tags like `<strong>` instead of converting
it to `<1>` and `</1>`.
2025-07-10 12:38:28 +02:00
Anthony LC 2e6cb109ef 🔖(minor) release 3.4.0
Added:
- (frontend) multi-pages
- (frontend) Duplicate a doc
- Ask for access
- (frontend) add customization for translations
- (backend) add ancestors links definitions to document abilities
- (backend) include ancestors accesses on document accesses list view
- (backend) add ancestors links reach and role to document API
- 📝(project) add troubleshoot doc
- 📝(project) add system-requirement doc
- 🔧(front) configure x-frame-options to DENY in nginx conf
- (backend) allow to disable checking unsafe mimetype on attachment upload
- (doc) add documentation to install with compose
-  Give priority to users connected to collaboration server
  (aka no websocket feature)

Changed:
- ♻️(backend) stop requiring owner for non-root documents
- ♻️(backend) simplify roles by ranking them and return only the max role
- 📌(yjs) stop pinning node to minor version on yjs docker image
- 🧑‍💻(docker) add .next to .dockerignore
- 🧑‍💻(docker) handle frontend development images with docker compose
- 🧑‍💻(docker) add y-provider config to development environment
- ️(frontend) optimize document fetch error handling

Fixed:
- 🐛(backend) fix link definition select options linked to ancestors
- 🐛(frontend) table of content disappearing
- 🐛(frontend) fix multiple EmojiPicker
- 🐛(frontend) fix meta title
- 🔧(git) set LF line endings for all text files
- 📝(docs) minor fixes to docs/env.md
- support `_FILE` environment variables for secrets

Removed:
- 🔥(frontend) remove Beta from logo
2025-07-09 17:26:02 +02:00
Manuel Raynaud 70635136cb 🐛(back) duplicating a child should not create accesses
Children does not have accesses created for now, they inherit from their
parent for now. We have to ignore access creation while owrk on the
children accesses has not been made.
2025-07-09 17:26:02 +02:00
Anthony LC 52a8dd0b5c 🩹(frontend) refresh tree after duplicate
After duplicating a document, the tree is now
refreshed to reflect the new structure.
This ensures that the user sees the updated
document tree immediately after the duplication
action.
2025-07-09 17:26:02 +02:00
Anthony LC 8a3dfe0252 🛂(frontend) blocked edition if multiple ancestors
With child documents we need to check the parent
documents to know if the parent doc are collaborative
or not.
2025-07-09 17:26:02 +02:00
Anthony LC 1110ec92d5 (backend) fix test access create
Importing the french translation broke a test
because the subject was not in english anymore.
We change the admin user language to english
to keep the subject in english.
2025-07-09 17:26:02 +02:00
31 changed files with 809 additions and 512 deletions
+21 -7
View File
@@ -8,19 +8,32 @@ and this project adheres to
## [Unreleased]
## [3.4.1] - 2025-07-15
### Fixed
- 🌐(frontend) keep simple tag during export #1154
- 🐛(back) manage can-edit endpoint without created room
in the ws #1152
- 🐛(frontend) fix action buttons not clickable #1162
- 🐛(frontend) fix crash share modal on grid options #1174
## [3.4.0] - 2025-07-09
### Added
- ✨(frontend) multi-pages #701
- ✨(frontend) Duplicate a doc #1078
- ✨Ask for access #1081
- ✨(frontend) add customization for translations #857
- ✨(backend) add ancestors links definitions to document abilities #846
- ✨(backend) include ancestors accesses on document accesses list view # 846
- ✨(backend) add ancestors links reach and role to document API #846
- ✨(frontend) add customization for translations #857
- ✨(frontend) Duplicate a doc #1078
- 📝(project) add troubleshoot doc #1066
- 📝(project) add system-requirement doc #1066
- 🔧(front) configure x-frame-options to DENY in nginx conf #1084
- ✨(backend) allow to disable checking unsafe mimetype on attachment upload
- ✨Ask for access #1081
- ✨(backend) allow to disable checking unsafe mimetype on
attachment upload #1099
- ✨(doc) add documentation to install with compose #855
- ✨ Give priority to users connected to collaboration server
(aka no websocket feature) #1093
@@ -43,8 +56,7 @@ and this project adheres to
- 🐛(frontend) fix meta title #1017
- 🔧(git) set LF line endings for all text files #1032
- 📝(docs) minor fixes to docs/env.md
-(backend) support `_FILE` environment variables for secrets #912
- ✨(frontend) support `_FILE` environment variables for secrets #912
- ✨support `_FILE` environment variables for secrets #912
### Removed
@@ -634,7 +646,9 @@ and this project adheres to
- ✨(frontend) Coming Soon page (#67)
- 🚀 Impress, project to manage your documents easily and collaboratively.
[unreleased]: https://github.com/numerique-gouv/impress/compare/v3.3.0...main
[unreleased]: https://github.com/numerique-gouv/impress/compare/v3.4.1...main
[v3.4.1]: https://github.com/numerique-gouv/impress/releases/v3.4.1
[v3.4.0]: https://github.com/numerique-gouv/impress/releases/v3.4.0
[v3.3.0]: https://github.com/numerique-gouv/impress/releases/v3.3.0
[v3.2.1]: https://github.com/numerique-gouv/impress/releases/v3.2.1
[v3.2.0]: https://github.com/numerique-gouv/impress/releases/v3.2.0
+24 -23
View File
@@ -948,33 +948,34 @@ class DocumentViewSet(
**link_kwargs,
)
# Always add the logged-in user as OWNER
accesses_to_create = [
models.DocumentAccess(
document=duplicated_document,
user=request.user,
role=models.RoleChoices.OWNER,
)
]
# If accesses should be duplicated, add other users' accesses as per original document
if with_accesses and is_owner_or_admin:
original_accesses = models.DocumentAccess.objects.filter(
document=document
).exclude(user=request.user)
accesses_to_create.extend(
# Always add the logged-in user as OWNER for root documents
if document.is_root():
accesses_to_create = [
models.DocumentAccess(
document=duplicated_document,
user_id=access.user_id,
team=access.team,
role=access.role,
user=request.user,
role=models.RoleChoices.OWNER,
)
for access in original_accesses
)
]
# Bulk create all the duplicated accesses
models.DocumentAccess.objects.bulk_create(accesses_to_create)
# If accesses should be duplicated, add other users' accesses as per original document
if with_accesses and is_owner_or_admin:
original_accesses = models.DocumentAccess.objects.filter(
document=document
).exclude(user=request.user)
accesses_to_create.extend(
models.DocumentAccess(
document=duplicated_document,
user_id=access.user_id,
team=access.team,
role=access.role,
)
for access in original_accesses
)
# Bulk create all the duplicated accesses
models.DocumentAccess.objects.bulk_create(accesses_to_create)
return drf_response.Response(
{"id": str(duplicated_document.id)}, status=status.HTTP_201_CREATED
@@ -62,10 +62,14 @@ class CollaborationService:
except requests.RequestException as e:
raise requests.HTTPError("Failed to get document connection info.") from e
if response.status_code != 200:
raise requests.HTTPError(
f"Failed to get document connection info. Status code: {response.status_code}, "
f"Response: {response.text}"
)
result = response.json()
return result.get("count", 0), result.get("exists", False)
if response.status_code == 200:
result = response.json()
return result.get("count", 0), result.get("exists", False)
if response.status_code == 404:
return 0, False
raise requests.HTTPError(
f"Failed to get document connection info. Status code: {response.status_code}, "
f"Response: {response.text}"
)
@@ -48,7 +48,7 @@ def test_api_documents_ask_for_access_create_authenticated():
An email should be sent to document owners and admins to notify them.
"""
owner_user = UserFactory(language="en-us")
admin_user = UserFactory(language="fr-fr")
admin_user = UserFactory(language="en-us")
document = DocumentFactory(
users=[
(owner_user, RoleChoices.OWNER),
@@ -246,3 +246,73 @@ def test_api_documents_can_edit_websocket_server_unreachable_fallback_to_no_webs
assert cache.get(f"docs:no-websocket:{document.id}") == "other_session_key"
assert ws_resp.call_count == 1
@responses.activate
def test_api_documents_can_edit_websocket_server_room_not_found(
settings,
):
"""
When the websocket server returns a 404, the document can be updated like if the user was
not connected to the websocket.
"""
user = factories.UserFactory(with_owned_document=True)
client = APIClient()
client.force_login(user)
session_key = client.session.session_key
document = factories.DocumentFactory(users=[(user, "editor")])
settings.COLLABORATION_API_URL = "http://example.com/"
settings.COLLABORATION_SERVER_SECRET = "secret-token"
settings.COLLABORATION_WS_NOT_CONNECTED_READY_ONLY = True
endpoint_url = (
f"{settings.COLLABORATION_API_URL}get-connections/"
f"?room={document.id}&sessionKey={session_key}"
)
ws_resp = responses.get(endpoint_url, status=404)
assert cache.get(f"docs:no-websocket:{document.id}") is None
response = client.get(
f"/api/v1.0/documents/{document.id!s}/can-edit/",
)
assert response.status_code == 200
assert response.json() == {"can_edit": True}
assert ws_resp.call_count == 1
@responses.activate
def test_api_documents_can_edit_websocket_server_room_not_found_other_already_editing(
settings,
):
"""
When the websocket server returns a 404 and another user is editing the document,
the response should be can-edit=False.
"""
user = factories.UserFactory(with_owned_document=True)
client = APIClient()
client.force_login(user)
session_key = client.session.session_key
document = factories.DocumentFactory(users=[(user, "editor")])
settings.COLLABORATION_API_URL = "http://example.com/"
settings.COLLABORATION_SERVER_SECRET = "secret-token"
settings.COLLABORATION_WS_NOT_CONNECTED_READY_ONLY = True
endpoint_url = (
f"{settings.COLLABORATION_API_URL}get-connections/"
f"?room={document.id}&sessionKey={session_key}"
)
ws_resp = responses.get(endpoint_url, status=404)
cache.set(f"docs:no-websocket:{document.id}", "other_session_key")
response = client.get(
f"/api/v1.0/documents/{document.id!s}/can-edit/",
)
assert response.status_code == 200
assert response.json() == {"can_edit": False}
assert ws_resp.call_count == 1
@@ -252,3 +252,44 @@ def test_api_documents_duplicate_with_accesses_non_admin(role):
duplicated_accesses = duplicated_document.accesses
assert duplicated_accesses.count() == 1
assert duplicated_accesses.get(user=user).role == "owner"
@pytest.mark.parametrize("role", ["editor", "reader"])
def test_api_documents_duplicate_non_root_document(role):
"""
Non-root documents can be duplicated but without accesses.
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
document = factories.DocumentFactory(users=[(user, "owner")])
child = factories.DocumentFactory(
parent=document, users=[(user, role)], title="document with accesses"
)
assert child.accesses.count() == 1
# Duplicate the document via the API endpoint requesting to duplicate accesses
response = client.post(
f"/api/v1.0/documents/{child.id!s}/duplicate/",
{"with_accesses": True},
format="json",
)
assert response.status_code == 201
duplicated_document = models.Document.objects.get(id=response.json()["id"])
assert duplicated_document.title == "Copy of document with accesses"
assert duplicated_document.content == child.content
assert duplicated_document.link_reach == child.link_reach
assert duplicated_document.link_role == child.link_role
assert duplicated_document.creator == user
assert duplicated_document.duplicated_from == child
assert duplicated_document.attachments == []
# No access should be created for non root documents
duplicated_accesses = duplicated_document.accesses
assert duplicated_accesses.count() == 0
assert duplicated_document.is_sibling_of(child)
assert duplicated_document.is_child_of(document)
@@ -539,6 +539,47 @@ def test_api_documents_update_websocket_server_unreachable_fallback_to_no_websoc
assert ws_resp.call_count == 1
@responses.activate
def test_api_documents_update_websocket_server_room_not_found_fallback_to_no_websocket_other_users(
settings,
):
"""
When the WebSocket server does not have the room created, the logic should fallback to
no-WebSocket. If another user is already editing, the update must be denied.
"""
user = factories.UserFactory(with_owned_document=True)
client = APIClient()
client.force_login(user)
session_key = client.session.session_key
document = factories.DocumentFactory(users=[(user, "editor")])
new_document_values = serializers.DocumentSerializer(
instance=factories.DocumentFactory()
).data
new_document_values["websocket"] = False
settings.COLLABORATION_API_URL = "http://example.com/"
settings.COLLABORATION_SERVER_SECRET = "secret-token"
settings.COLLABORATION_WS_NOT_CONNECTED_READY_ONLY = True
endpoint_url = (
f"{settings.COLLABORATION_API_URL}get-connections/"
f"?room={document.id}&sessionKey={session_key}"
)
ws_resp = responses.get(endpoint_url, status=404)
cache.set(f"docs:no-websocket:{document.id}", "other_session_key")
response = client.put(
f"/api/v1.0/documents/{document.id!s}/",
new_document_values,
format="json",
)
assert response.status_code == 403
assert cache.get(f"docs:no-websocket:{document.id}") == "other_session_key"
assert ws_resp.call_count == 1
@responses.activate
def test_api_documents_update_force_websocket_param_to_true(settings):
"""
+5 -5
View File
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "impress"
version = "3.3.0"
version = "3.4.1"
authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }]
classifiers = [
"Development Status :: 5 - Production/Stable",
@@ -26,7 +26,7 @@ readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"beautifulsoup4==4.13.4",
"boto3==1.39.3",
"boto3==1.39.4",
"Brotli==1.1.0",
"celery[redis]==5.5.3",
"django-configurations==2.5.1",
@@ -34,7 +34,7 @@ dependencies = [
"django-countries==7.6.1",
"django-csp==4.0",
"django-filter==25.1",
"django-lasuite[all]==0.0.10",
"django-lasuite[all]==0.0.11",
"django-parler==2.3",
"django-redis==6.0.0",
"django-storages[s3]==1.14.6",
@@ -52,9 +52,9 @@ dependencies = [
"markdown==3.8.2",
"mozilla-django-oidc==4.0.1",
"nested-multipart-parser==1.5.0",
"openai==1.93.0",
"openai==1.95.0",
"psycopg[binary]==3.2.9",
"pycrdt==0.12.23",
"pycrdt==0.12.25",
"PyJWT==2.10.1",
"python-magic==0.4.27",
"redis<6.0.0",
@@ -10,6 +10,7 @@ import {
overrideConfig,
verifyDocName,
} from './common';
import { createRootSubPage } from './sub-pages-utils';
test.beforeEach(async ({ page }) => {
await page.goto('/');
@@ -524,6 +525,8 @@ test.describe('Doc Editor', () => {
page,
browserName,
}) => {
test.slow();
/**
* The good port is 4444, but we want to simulate a not connected
* collaborative server.
@@ -536,7 +539,12 @@ test.describe('Doc Editor', () => {
await page.goto('/');
const [title] = await createDoc(page, 'editing-blocking', browserName, 1);
const [parentTitle] = await createDoc(
page,
'editing-blocking',
browserName,
1,
);
const card = page.getByLabel('It is the card information');
await expect(
@@ -571,12 +579,20 @@ test.describe('Doc Editor', () => {
// Close the modal
await page.getByRole('button', { name: 'close' }).first().click();
const urlParentDoc = page.url();
const { name: childTitle } = await createRootSubPage(
page,
browserName,
'editing-blocking - child',
);
let responseCanEdit = await responseCanEditPromise;
expect(responseCanEdit.ok()).toBeTruthy();
let jsonCanEdit = (await responseCanEdit.json()) as { can_edit: boolean };
expect(jsonCanEdit.can_edit).toBeTruthy();
const urlDoc = page.url();
const urlChildDoc = page.url();
/**
* We open another browser that will connect to the collaborative server
@@ -603,14 +619,14 @@ test.describe('Doc Editor', () => {
},
);
await otherPage.goto(urlDoc);
await otherPage.goto(urlChildDoc);
const webSocket = await webSocketPromise;
expect(webSocket.url()).toContain(
'ws://localhost:4444/collaboration/ws/?room=',
);
await verifyDocName(otherPage, title);
await verifyDocName(otherPage, childTitle);
await page.reload();
@@ -633,6 +649,10 @@ test.describe('Doc Editor', () => {
await expect(editor).toHaveAttribute('contenteditable', 'false');
await page.goto(urlParentDoc);
await verifyDocName(page, parentTitle);
await page.getByRole('button', { name: 'Share' }).click();
await page.getByLabel('Visibility mode').click();
@@ -641,18 +661,9 @@ test.describe('Doc Editor', () => {
// Close the modal
await page.getByRole('button', { name: 'close' }).first().click();
await page.reload();
await page.goto(urlChildDoc);
responseCanEditPromise = page.waitForResponse(
(response) =>
response.url().includes(`/can-edit/`) && response.status() === 200,
);
responseCanEdit = await responseCanEditPromise;
expect(responseCanEdit.ok()).toBeTruthy();
jsonCanEdit = (await responseCanEdit.json()) as { can_edit: boolean };
expect(jsonCanEdit.can_edit).toBeTruthy();
await expect(editor).toHaveAttribute('contenteditable', 'true');
await expect(
card.getByText('Others are editing. Your network prevent changes.'),
@@ -91,6 +91,22 @@ test.describe('Document grid item options', () => {
await page.goto('/');
});
test('it checks the share modal', async ({ page, browserName }) => {
const [docTitle] = await createDoc(page, `check share modal`, browserName);
await page.goto('/');
await expect(page.getByText(docTitle)).toBeVisible();
const row = await getGridRow(page, docTitle);
await row.getByText(`more_horiz`).click();
await page.getByRole('menuitem', { name: 'Share' }).click();
await expect(
page.getByRole('dialog').getByText('Share the document'),
).toBeVisible();
});
test('it pins a document', async ({ page, browserName }) => {
const [docTitle] = await createDoc(page, `Favorite doc`, browserName);
@@ -9,6 +9,7 @@ import {
mockedInvitations,
verifyDocName,
} from './common';
import { createRootSubPage } from './sub-pages-utils';
test.beforeEach(async ({ page }) => {
await page.goto('/');
@@ -456,6 +457,32 @@ test.describe('Doc Header', () => {
await page.getByText(duplicateDuplicateTitle).click();
await expect(page.getByText('Hello Duplicated World')).toBeVisible();
});
test('it duplicates a child document', async ({ page, browserName }) => {
await createDoc(page, `Duplicate doc`, browserName);
const { name: childTitle } = await createRootSubPage(
page,
browserName,
'Duplicate doc - child',
);
const editor = page.locator('.ProseMirror');
await editor.click();
await editor.fill('Hello Duplicated World');
await page.getByLabel('Open the document options').click();
await page.getByRole('menuitem', { name: 'Duplicate' }).click();
await expect(
page.getByText('Document duplicated successfully!'),
).toBeVisible();
const duplicateDuplicateTitle = 'Copy of ' + childTitle;
await expect(
page.getByTestId('doc-tree').getByText(duplicateDuplicateTitle),
).toBeVisible();
});
});
test.describe('Documents Header mobile', () => {
@@ -441,7 +441,7 @@ test.describe('Doc Visibility: Authenticated', () => {
const { name: childTitle } = await createRootSubPage(
page,
browserName,
'Authenticated read onlyc - child',
'Authenticated read only - child',
);
const urlChildDoc = page.url();
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "app-e2e",
"version": "3.3.0",
"version": "3.4.1",
"private": true,
"scripts": {
"lint": "eslint . --ext .ts",
@@ -12,7 +12,7 @@
"test:ui::chromium": "yarn test:ui --project=chromium"
},
"devDependencies": {
"@playwright/test": "1.53.2",
"@playwright/test": "1.54.1",
"@types/node": "*",
"@types/pdf-parse": "1.1.5",
"eslint-config-impress": "*",
+11 -11
View File
@@ -1,6 +1,6 @@
{
"name": "app-impress",
"version": "3.3.0",
"version": "3.4.1",
"private": true,
"scripts": {
"dev": "next dev",
@@ -32,27 +32,27 @@
"@hocuspocus/provider": "2.15.2",
"@openfun/cunningham-react": "3.1.0",
"@react-pdf/renderer": "4.3.0",
"@sentry/nextjs": "9.35.0",
"@tanstack/react-query": "5.81.5",
"@sentry/nextjs": "9.38.0",
"@tanstack/react-query": "5.83.0",
"canvg": "4.0.3",
"clsx": "2.1.1",
"cmdk": "1.1.1",
"crisp-sdk-web": "1.0.25",
"docx": "9.5.0",
"emoji-mart": "5.6.0",
"i18next": "25.3.1",
"i18next": "25.3.2",
"i18next-browser-languagedetector": "8.2.0",
"idb": "8.0.3",
"lodash": "4.17.21",
"luxon": "3.6.1",
"next": "15.3.5",
"posthog-js": "1.256.2",
"luxon": "3.7.1",
"next": "15.4.1",
"posthog-js": "1.257.0",
"react": "*",
"react-aria-components": "1.10.1",
"react-dom": "*",
"react-i18next": "15.6.0",
"react-intersection-observer": "9.16.0",
"react-select": "5.10.1",
"react-select": "5.10.2",
"styled-components": "6.1.19",
"use-debounce": "10.0.5",
"y-protocols": "1.0.6",
@@ -61,7 +61,7 @@
},
"devDependencies": {
"@svgr/webpack": "8.1.0",
"@tanstack/react-query-devtools": "5.81.5",
"@tanstack/react-query-devtools": "5.83.0",
"@testing-library/dom": "10.4.0",
"@testing-library/jest-dom": "6.6.3",
"@testing-library/react": "16.3.0",
@@ -73,7 +73,7 @@
"@types/react": "*",
"@types/react-dom": "*",
"cross-env": "7.0.3",
"dotenv": "17.1.0",
"dotenv": "17.2.0",
"eslint-config-impress": "*",
"fetch-mock": "9.11.0",
"jest": "30.0.4",
@@ -84,7 +84,7 @@
"stylelint-config-standard": "38.0.0",
"stylelint-prettier": "5.0.3",
"typescript": "*",
"webpack": "5.99.9",
"webpack": "5.100.1",
"workbox-webpack-plugin": "7.1.0"
}
}
@@ -7,6 +7,7 @@ import * as Y from 'yjs';
import { APIError, errorCauses, fetchAPI } from '@/api';
import { toBase64 } from '@/docs/doc-editor';
import { KEY_DOC_TREE } from '@/docs/doc-tree';
import { KEY_LIST_DOC_VERSIONS } from '@/docs/doc-versioning';
import { useProviderStore } from '../stores';
@@ -85,6 +86,9 @@ export function useDuplicateDoc(options: DuplicateDocOptions) {
void queryClient.resetQueries({
queryKey: [KEY_LIST_DOC],
});
void queryClient.resetQueries({
queryKey: [KEY_DOC_TREE],
});
void options.onSuccess?.(data, variables, context);
},
});
@@ -5,15 +5,20 @@ import { useIsOffline } from '@/features/service-worker';
import { KEY_CAN_EDIT, useDocCanEdit } from '../api/useDocCanEdit';
import { useProviderStore } from '../stores';
import { Doc, LinkReach } from '../types';
import { Doc, LinkReach, LinkRole } from '../types';
export const useIsCollaborativeEditable = (doc: Doc) => {
const { isConnected } = useProviderStore();
const { data: conf } = useConfig();
const docIsPublic = doc.link_reach === LinkReach.PUBLIC;
const docIsAuth = doc.link_reach === LinkReach.AUTHENTICATED;
const docHasMember = doc.nb_accesses_direct > 1;
const docIsPublic =
doc.computed_link_reach === LinkReach.PUBLIC &&
doc.computed_link_role === LinkRole.EDITOR;
const docIsAuth =
doc.computed_link_reach === LinkReach.AUTHENTICATED &&
doc.computed_link_role === LinkRole.EDITOR;
const docHasMember =
doc.nb_accesses_direct > 1 || doc.nb_accesses_ancestors > 1;
const isUserReader = !doc.abilities.partial_update;
const isShared = docIsPublic || docIsAuth || docHasMember;
const { isOffline } = useIsOffline();
@@ -21,23 +26,23 @@ export const useIsCollaborativeEditable = (doc: Doc) => {
const [isEditable, setIsEditable] = useState(true);
const [isLoading, setIsLoading] = useState(!_isEditable);
const timeout = useRef<NodeJS.Timeout | null>(null);
const {
data: { can_edit } = { can_edit: _isEditable },
isLoading: isLoadingCanEdit,
} = useDocCanEdit(doc.id, {
enabled: !_isEditable,
queryKey: [KEY_CAN_EDIT, doc.id],
staleTime: 0,
});
const { data: editingRight, isLoading: isLoadingCanEdit } = useDocCanEdit(
doc.id,
{
enabled: !_isEditable,
queryKey: [KEY_CAN_EDIT, doc.id],
staleTime: 0,
},
);
useEffect(() => {
if (isLoadingCanEdit) {
if (isLoadingCanEdit || _isEditable || !editingRight) {
return;
}
// Connection to the WebSocket can take some time, so we set a timeout to ensure the loading state is cleared after a reasonable time.
timeout.current = setTimeout(() => {
setIsEditable(can_edit);
setIsEditable(editingRight.can_edit);
setIsLoading(false);
}, 1500);
@@ -46,7 +51,7 @@ export const useIsCollaborativeEditable = (doc: Doc) => {
clearTimeout(timeout.current);
}
};
}, [can_edit, isLoadingCanEdit]);
}, [editingRight, isLoadingCanEdit, _isEditable]);
useEffect(() => {
if (!_isEditable) {
@@ -59,7 +64,7 @@ export const useIsCollaborativeEditable = (doc: Doc) => {
setIsEditable(true);
setIsLoading(false);
}, [_isEditable, isLoading]);
}, [_isEditable]);
if (!conf?.COLLABORATION_WS_NOT_CONNECTED_READY_ONLY) {
return {
@@ -1,3 +1,4 @@
export * from './useCreateChildren';
export * from './useDocChildren';
export * from './useDocTree';
export * from './useMove';
@@ -25,7 +25,7 @@ export const getDocTree = async ({ docId }: DocsTreeParams): Promise<Doc> => {
return response.json() as Promise<Doc>;
};
export const KEY_LIST_DOC_CHILDREN = 'doc-tree';
export const KEY_DOC_TREE = 'doc-tree';
export function useDocTree(
params: DocsTreeParams,
@@ -35,7 +35,7 @@ export function useDocTree(
>,
) {
return useQuery<Doc, APIError, Doc>({
queryKey: [KEY_LIST_DOC_CHILDREN, params],
queryKey: [KEY_DOC_TREE, params],
queryFn: () => getDocTree(params),
staleTime: 0,
refetchOnWindowFocus: false,
@@ -18,7 +18,7 @@ import {
import { useLeftPanelStore } from '@/features/left-panel';
import { useResponsiveStore } from '@/stores';
import Logo from './../assets/sub-page-logo.svg';
import SubPageIcon from './../assets/sub-page-logo.svg';
import { DocTreeItemActions } from './DocTreeItemActions';
const ItemTextCss = css`
@@ -99,6 +99,7 @@ export const DocSubPageItem = (props: Props) => {
return (
<Box
className="--docs-sub-page-item"
$position="relative"
$css={css`
background-color: ${actionsOpen
? 'var(--c--theme--colors--greyscale-100)'
@@ -106,6 +107,17 @@ export const DocSubPageItem = (props: Props) => {
.light-doc-item-actions {
display: ${actionsOpen || !isDesktop ? 'flex' : 'none'};
position: absolute;
right: 0;
background: ${isDesktop
? 'var(--c--theme--colors--greyscale-100)'
: 'var(--c--theme--colors--greyscale-000)'};
}
.c__tree-view--node.isSelected {
.light-doc-item-actions {
background: var(--c--theme--colors--greyscale-100);
}
}
&:hover {
@@ -114,6 +126,7 @@ export const DocSubPageItem = (props: Props) => {
.light-doc-item-actions {
display: flex;
background: var(--c--theme--colors--greyscale-100);
}
}
`}
@@ -136,7 +149,7 @@ export const DocSubPageItem = (props: Props) => {
$minHeight="24px"
>
<Box $width="16px" $height="16px">
<Logo />
<SubPageIcon />
</Box>
<Box
@@ -136,6 +136,10 @@ export const DocTree = ({ initialTargetId }: DocTreeProps) => {
.c__tree-view--container {
z-index: 1;
margin-top: -10px;
.c__tree-view {
overflow: hidden !important;
}
}
`}
>
@@ -1,3 +1,4 @@
import { TreeProvider } from '@gouvfr-lasuite/ui-kit';
import { Tooltip, useModal } from '@openfun/cunningham-react';
import { DateTime } from 'luxon';
import { useTranslation } from 'react-i18next';
@@ -143,7 +144,9 @@ export const DocsGridItem = ({ doc, dragMode = false }: DocsGridItemProps) => {
</Box>
</Box>
{shareModal.isOpen && (
<DocShareModal doc={doc} onClose={shareModal.close} />
<TreeProvider initialNodeId={doc.id}>
<DocShareModal doc={doc} onClose={shareModal.close} />
</TreeProvider>
)}
</>
);
@@ -591,7 +591,7 @@
"The document has been deleted.": "Le document a bien été supprimé.",
"The document visibility has been updated.": "La visibilité du document a été mise à jour.",
"The export failed": "Lexportation a échoué",
"This document and <1>any sub-documents</1> will be permanently deleted. This action is irreversible.": "Ce document et <1>tous les sous-documents</1> seront définitivement supprimés. Cette action est irréversible.",
"This document and <strong>any sub-documents</strong> will be permanently deleted. This action is irreversible.": "Ce document et <strong>tous les sous-documents</strong> seront définitivement supprimés. Cette action est irréversible.",
"This file is flagged as unsafe.": "Ce fichier est marqué comme non sûr.",
"This means you can't edit until others leave.": "Cela signifie que vous ne pouvez pas éditer tant que d'autres éditeurs sont présents sur le document.",
"This user has access inherited from a parent page.": "Cet utilisateur a un accès hérité d'une page parente.",
+4 -4
View File
@@ -1,6 +1,6 @@
{
"name": "impress",
"version": "3.3.0",
"version": "3.4.1",
"private": true,
"workspaces": {
"packages": [
@@ -28,11 +28,11 @@
"server:test": "yarn COLLABORATION_SERVER run test"
},
"resolutions": {
"@types/node": "22.16.0",
"@types/node": "22.16.3",
"@types/react": "19.1.8",
"@types/react-dom": "19.1.6",
"@typescript-eslint/eslint-plugin": "8.36.0",
"@typescript-eslint/parser": "8.36.0",
"@typescript-eslint/eslint-plugin": "8.37.0",
"@typescript-eslint/parser": "8.37.0",
"eslint": "8.57.0",
"react": "19.1.0",
"react-dom": "19.1.0",
@@ -1,24 +1,24 @@
{
"name": "eslint-config-impress",
"version": "3.3.0",
"version": "3.4.1",
"license": "MIT",
"scripts": {
"lint": "eslint --ext .js ."
},
"dependencies": {
"@next/eslint-plugin-next": "15.3.5",
"@next/eslint-plugin-next": "15.4.1",
"@tanstack/eslint-plugin-query": "5.81.2",
"@typescript-eslint/eslint-plugin": "*",
"@typescript-eslint/parser": "*",
"eslint": "*",
"eslint-config-next": "15.3.5",
"eslint-config-next": "15.4.1",
"eslint-config-prettier": "10.1.5",
"eslint-plugin-import": "2.32.0",
"eslint-plugin-jest": "29.0.1",
"eslint-plugin-jsx-a11y": "6.10.2",
"eslint-plugin-playwright": "2.2.0",
"eslint-plugin-prettier": "5.5.1",
"eslint-plugin-testing-library": "7.5.3",
"eslint-plugin-testing-library": "7.6.0",
"prettier": "3.6.2"
}
}
@@ -7,6 +7,16 @@ const config = {
keySeparator: false,
nsSeparator: false,
namespaceSeparator: false,
lexers: {
tsx: [
{
lexer: 'JsxLexer',
functions: ['t'],
transSupportBasicHtmlNodes: true, // Disable automatic conversion
transKeepBasicHtmlNodesFor: ['strong', 'b', 'i', 'code', 'br'],
},
],
},
};
export default config;
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "packages-i18n",
"version": "3.3.0",
"version": "3.4.1",
"private": true,
"scripts": {
"extract-translation": "yarn extract-translation:impress",
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "server-y-provider",
"version": "3.3.0",
"version": "3.4.1",
"description": "Y.js provider for docs",
"repository": "https://github.com/numerique-gouv/impress",
"license": "MIT",
@@ -18,8 +18,8 @@
"dependencies": {
"@blocknote/server-util": "0.33.0",
"@hocuspocus/server": "2.15.2",
"@sentry/node": "9.35.0",
"@sentry/profiling-node": "9.35.0",
"@sentry/node": "9.38.0",
"@sentry/profiling-node": "9.38.0",
"axios": "1.10.0",
"cors": "2.8.5",
"express": "5.1.0",
+435 -403
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1,7 +1,7 @@
environments:
dev:
values:
- version: 3.3.0
- version: 3.4.1
---
repositories:
- name: bitnami
+1 -1
View File
@@ -1,5 +1,5 @@
apiVersion: v2
type: application
name: docs
version: 3.4.0-beta.2
version: 3.4.1
appVersion: latest
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "mail_mjml",
"version": "3.3.0",
"version": "3.4.1",
"description": "An util to generate html and text django's templates from mjml templates",
"type": "module",
"dependencies": {