Compare commits

..

4 Commits

Author SHA1 Message Date
Anthony LC 1e924f54e9 🔖(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:11:58 +02:00
Manuel Raynaud 2eb50ae2bf 🐛(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:11:58 +02:00
Anthony LC fb0528b364 🩹(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:11:58 +02:00
Anthony LC 0a106428ef 🛂(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:11:58 +02:00
22 changed files with 449 additions and 653 deletions
+1 -12
View File
@@ -8,16 +8,6 @@ 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
@@ -646,8 +636,7 @@ 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.4.1...main
[v3.4.1]: https://github.com/numerique-gouv/impress/releases/v3.4.1
[unreleased]: https://github.com/numerique-gouv/impress/compare/v3.4.0...main
[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
@@ -62,14 +62,10 @@ class CollaborationService:
except requests.RequestException as e:
raise requests.HTTPError("Failed to get document connection info.") from e
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}"
)
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)
@@ -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="en-us")
admin_user = UserFactory(language="fr-fr")
document = DocumentFactory(
users=[
(owner_user, RoleChoices.OWNER),
@@ -246,73 +246,3 @@ 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
@@ -539,47 +539,6 @@ 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.4.1"
version = "3.4.0"
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.4",
"boto3==1.39.3",
"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.11",
"django-lasuite[all]==0.0.10",
"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.95.0",
"openai==1.93.0",
"psycopg[binary]==3.2.9",
"pycrdt==0.12.25",
"pycrdt==0.12.23",
"PyJWT==2.10.1",
"python-magic==0.4.27",
"redis<6.0.0",
@@ -91,22 +91,6 @@ 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);
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "app-e2e",
"version": "3.4.1",
"version": "3.4.0",
"private": true,
"scripts": {
"lint": "eslint . --ext .ts",
@@ -12,7 +12,7 @@
"test:ui::chromium": "yarn test:ui --project=chromium"
},
"devDependencies": {
"@playwright/test": "1.54.1",
"@playwright/test": "1.53.2",
"@types/node": "*",
"@types/pdf-parse": "1.1.5",
"eslint-config-impress": "*",
+11 -11
View File
@@ -1,6 +1,6 @@
{
"name": "app-impress",
"version": "3.4.1",
"version": "3.4.0",
"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.38.0",
"@tanstack/react-query": "5.83.0",
"@sentry/nextjs": "9.35.0",
"@tanstack/react-query": "5.81.5",
"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.2",
"i18next": "25.3.1",
"i18next-browser-languagedetector": "8.2.0",
"idb": "8.0.3",
"lodash": "4.17.21",
"luxon": "3.7.1",
"next": "15.4.1",
"posthog-js": "1.257.0",
"luxon": "3.6.1",
"next": "15.3.5",
"posthog-js": "1.256.2",
"react": "*",
"react-aria-components": "1.10.1",
"react-dom": "*",
"react-i18next": "15.6.0",
"react-intersection-observer": "9.16.0",
"react-select": "5.10.2",
"react-select": "5.10.1",
"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.83.0",
"@tanstack/react-query-devtools": "5.81.5",
"@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.2.0",
"dotenv": "17.1.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.100.1",
"webpack": "5.99.9",
"workbox-webpack-plugin": "7.1.0"
}
}
@@ -18,7 +18,7 @@ import {
import { useLeftPanelStore } from '@/features/left-panel';
import { useResponsiveStore } from '@/stores';
import SubPageIcon from './../assets/sub-page-logo.svg';
import Logo from './../assets/sub-page-logo.svg';
import { DocTreeItemActions } from './DocTreeItemActions';
const ItemTextCss = css`
@@ -99,7 +99,6 @@ 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)'
@@ -107,17 +106,6 @@ 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 {
@@ -126,7 +114,6 @@ export const DocSubPageItem = (props: Props) => {
.light-doc-item-actions {
display: flex;
background: var(--c--theme--colors--greyscale-100);
}
}
`}
@@ -149,7 +136,7 @@ export const DocSubPageItem = (props: Props) => {
$minHeight="24px"
>
<Box $width="16px" $height="16px">
<SubPageIcon />
<Logo />
</Box>
<Box
@@ -136,10 +136,6 @@ export const DocTree = ({ initialTargetId }: DocTreeProps) => {
.c__tree-view--container {
z-index: 1;
margin-top: -10px;
.c__tree-view {
overflow: hidden !important;
}
}
`}
>
@@ -1,4 +1,3 @@
import { TreeProvider } from '@gouvfr-lasuite/ui-kit';
import { Tooltip, useModal } from '@openfun/cunningham-react';
import { DateTime } from 'luxon';
import { useTranslation } from 'react-i18next';
@@ -144,9 +143,7 @@ export const DocsGridItem = ({ doc, dragMode = false }: DocsGridItemProps) => {
</Box>
</Box>
{shareModal.isOpen && (
<TreeProvider initialNodeId={doc.id}>
<DocShareModal doc={doc} onClose={shareModal.close} />
</TreeProvider>
<DocShareModal doc={doc} onClose={shareModal.close} />
)}
</>
);
@@ -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 <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 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 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.4.1",
"version": "3.4.0",
"private": true,
"workspaces": {
"packages": [
@@ -28,11 +28,11 @@
"server:test": "yarn COLLABORATION_SERVER run test"
},
"resolutions": {
"@types/node": "22.16.3",
"@types/node": "22.16.0",
"@types/react": "19.1.8",
"@types/react-dom": "19.1.6",
"@typescript-eslint/eslint-plugin": "8.37.0",
"@typescript-eslint/parser": "8.37.0",
"@typescript-eslint/eslint-plugin": "8.36.0",
"@typescript-eslint/parser": "8.36.0",
"eslint": "8.57.0",
"react": "19.1.0",
"react-dom": "19.1.0",
@@ -1,24 +1,24 @@
{
"name": "eslint-config-impress",
"version": "3.4.1",
"version": "3.4.0",
"license": "MIT",
"scripts": {
"lint": "eslint --ext .js ."
},
"dependencies": {
"@next/eslint-plugin-next": "15.4.1",
"@next/eslint-plugin-next": "15.3.5",
"@tanstack/eslint-plugin-query": "5.81.2",
"@typescript-eslint/eslint-plugin": "*",
"@typescript-eslint/parser": "*",
"eslint": "*",
"eslint-config-next": "15.4.1",
"eslint-config-next": "15.3.5",
"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.6.0",
"eslint-plugin-testing-library": "7.5.3",
"prettier": "3.6.2"
}
}
@@ -7,16 +7,6 @@ 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.4.1",
"version": "3.4.0",
"private": true,
"scripts": {
"extract-translation": "yarn extract-translation:impress",
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "server-y-provider",
"version": "3.4.1",
"version": "3.4.0",
"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.38.0",
"@sentry/profiling-node": "9.38.0",
"@sentry/node": "9.35.0",
"@sentry/profiling-node": "9.35.0",
"axios": "1.10.0",
"cors": "2.8.5",
"express": "5.1.0",
+403 -435
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.4.1
- version: 3.4.0
---
repositories:
- name: bitnami
+1 -1
View File
@@ -1,5 +1,5 @@
apiVersion: v2
type: application
name: docs
version: 3.4.1
version: 3.4.0
appVersion: latest
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "mail_mjml",
"version": "3.4.1",
"version": "3.4.0",
"description": "An util to generate html and text django's templates from mjml templates",
"type": "module",
"dependencies": {