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
20 changed files with 167 additions and 74 deletions

View File

@@ -8,19 +8,22 @@ and this project adheres to
## [Unreleased]
## [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 +46,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 +636,8 @@ 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.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
[v3.2.0]: https://github.com/numerique-gouv/impress/releases/v3.2.0

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

View File

@@ -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)

View File

@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "impress"
version = "3.3.0"
version = "3.4.0"
authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }]
classifiers = [
"Development Status :: 5 - Production/Stable",

View File

@@ -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.'),

View File

@@ -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', () => {

View File

@@ -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();

View File

@@ -1,6 +1,6 @@
{
"name": "app-e2e",
"version": "3.3.0",
"version": "3.4.0",
"private": true,
"scripts": {
"lint": "eslint . --ext .ts",

View File

@@ -1,6 +1,6 @@
{
"name": "app-impress",
"version": "3.3.0",
"version": "3.4.0",
"private": true,
"scripts": {
"dev": "next dev",

View File

@@ -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);
},
});

View File

@@ -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 {

View File

@@ -1,3 +1,4 @@
export * from './useCreateChildren';
export * from './useDocChildren';
export * from './useDocTree';
export * from './useMove';

View File

@@ -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,

View File

@@ -1,6 +1,6 @@
{
"name": "impress",
"version": "3.3.0",
"version": "3.4.0",
"private": true,
"workspaces": {
"packages": [

View File

@@ -1,6 +1,6 @@
{
"name": "eslint-config-impress",
"version": "3.3.0",
"version": "3.4.0",
"license": "MIT",
"scripts": {
"lint": "eslint --ext .js ."

View File

@@ -1,6 +1,6 @@
{
"name": "packages-i18n",
"version": "3.3.0",
"version": "3.4.0",
"private": true,
"scripts": {
"extract-translation": "yarn extract-translation:impress",

View File

@@ -1,6 +1,6 @@
{
"name": "server-y-provider",
"version": "3.3.0",
"version": "3.4.0",
"description": "Y.js provider for docs",
"repository": "https://github.com/numerique-gouv/impress",
"license": "MIT",

View File

@@ -1,7 +1,7 @@
environments:
dev:
values:
- version: 3.3.0
- version: 3.4.0
---
repositories:
- name: bitnami

View File

@@ -1,5 +1,5 @@
apiVersion: v2
type: application
name: docs
version: 3.4.0-beta.2
version: 3.4.0
appVersion: latest

View File

@@ -1,6 +1,6 @@
{
"name": "mail_mjml",
"version": "3.3.0",
"version": "3.4.0",
"description": "An util to generate html and text django's templates from mjml templates",
"type": "module",
"dependencies": {