Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 22ef6e0e40 | |||
| 500d4ea5ac | |||
| 8a057b9c39 | |||
| 6a12ac560e | |||
| 2e6cb109ef | |||
| 70635136cb | |||
| 52a8dd0b5c | |||
| 8a3dfe0252 | |||
| 1110ec92d5 |
+15
-7
@@ -8,19 +8,27 @@ and this project adheres to
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Fixed
|
||||
|
||||
- 🌐(frontend) keep simple tag during export #1154
|
||||
- 🐛(back) manage can-edit endpoint without created room in the ws
|
||||
|
||||
## [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 +51,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 +641,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
|
||||
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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.'),
|
||||
|
||||
@@ -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,48 @@ 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();
|
||||
|
||||
const docTree = page.getByTestId('doc-tree');
|
||||
|
||||
await docTree.getByText(duplicateDuplicateTitle).hover();
|
||||
await docTree
|
||||
.getByText(duplicateDuplicateTitle)
|
||||
.getByRole('button', { name: 'add_box' })
|
||||
.click();
|
||||
|
||||
await page.getByRole('menuitem', { name: 'Duplicate' }).click();
|
||||
|
||||
const duplicateDuplicateDuplicateTitle =
|
||||
'Copy of ' + duplicateDuplicateTitle;
|
||||
await expect(
|
||||
page.getByTestId('doc-tree').getByText(duplicateDuplicateDuplicateTitle),
|
||||
).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();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "app-e2e",
|
||||
"version": "3.3.0",
|
||||
"version": "3.4.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"lint": "eslint . --ext .ts",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "app-impress",
|
||||
"version": "3.3.0",
|
||||
"version": "3.4.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
});
|
||||
|
||||
+21
-16
@@ -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,
|
||||
|
||||
+31
-1
@@ -3,7 +3,11 @@ import {
|
||||
DropdownMenuOption,
|
||||
useTreeContext,
|
||||
} from '@gouvfr-lasuite/ui-kit';
|
||||
import { useModal } from '@openfun/cunningham-react';
|
||||
import {
|
||||
VariantType,
|
||||
useModal,
|
||||
useToastProvider,
|
||||
} from '@openfun/cunningham-react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { Fragment } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -16,6 +20,7 @@ import {
|
||||
ModalRemoveDoc,
|
||||
Role,
|
||||
useCopyDocLink,
|
||||
useDuplicateDoc,
|
||||
} from '../../doc-management';
|
||||
import { useCreateChildrenDoc } from '../api/useCreateChildren';
|
||||
import { useDetachDoc } from '../api/useDetach';
|
||||
@@ -45,6 +50,19 @@ export const DocTreeItemActions = ({
|
||||
const { isCurrentParent } = useTreeUtils(doc);
|
||||
const { mutate: detachDoc } = useDetachDoc();
|
||||
const treeContext = useTreeContext<Doc>();
|
||||
const { toast } = useToastProvider();
|
||||
const { mutate: duplicateDoc } = useDuplicateDoc({
|
||||
onSuccess: () => {
|
||||
toast(t('Document duplicated successfully!'), VariantType.SUCCESS, {
|
||||
duration: 3000,
|
||||
});
|
||||
},
|
||||
onError: () => {
|
||||
toast(t('Failed to duplicate the document...'), VariantType.ERROR, {
|
||||
duration: 3000,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const handleDetachDoc = () => {
|
||||
if (!treeContext?.root) {
|
||||
@@ -89,6 +107,18 @@ export const DocTreeItemActions = ({
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
label: t('Duplicate'),
|
||||
icon: <Icon $variation="600" iconName="content_copy" />,
|
||||
isDisabled: !doc.abilities.duplicate,
|
||||
callback: () => {
|
||||
duplicateDoc({
|
||||
docId: doc.id,
|
||||
with_accesses: false,
|
||||
canSave: doc.abilities.partial_update,
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
label: t('Delete'),
|
||||
isDisabled: !doc.abilities.destroy,
|
||||
|
||||
@@ -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": "L’exportation 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.",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "impress",
|
||||
"version": "3.3.0",
|
||||
"version": "3.4.0",
|
||||
"private": true,
|
||||
"workspaces": {
|
||||
"packages": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "eslint-config-impress",
|
||||
"version": "3.3.0",
|
||||
"version": "3.4.0",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"lint": "eslint --ext .js ."
|
||||
|
||||
@@ -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,6 +1,6 @@
|
||||
{
|
||||
"name": "packages-i18n",
|
||||
"version": "3.3.0",
|
||||
"version": "3.4.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"extract-translation": "yarn extract-translation:impress",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
environments:
|
||||
dev:
|
||||
values:
|
||||
- version: 3.3.0
|
||||
- version: 3.4.0
|
||||
---
|
||||
repositories:
|
||||
- name: bitnami
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
apiVersion: v2
|
||||
type: application
|
||||
name: docs
|
||||
version: 3.4.0-beta.2
|
||||
version: 3.4.0
|
||||
appVersion: latest
|
||||
|
||||
@@ -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": {
|
||||
|
||||
Reference in New Issue
Block a user