Compare commits

...

28 Commits

Author SHA1 Message Date
Anthony LC 0d0e17c8d5 🔖(minor) release 3.10.0
Added:
- (frontend) enable ODT export for documents
- (frontend) improve mobile UX by showing subdocs count

Changed:
- ♻️(frontend) preserve @ character when esc is pressed
  after typing it
- ♻️(frontend) make summary button fixed to remain visible
  during scroll
- ♻️(frontend) pdf embed use full width

Fixed:
- (frontend) improve accessibility:
  - (frontend) improve ARIA in doc grid and editor
    for a11y
  - (frontend) improve accessibility and styling
    of summary table
  - (frontend) add focus trap and enter key support
    to remove doc modal
- 🐛(docx) fix image overflow by limiting width to
  600px during export
- 🐛(frontend) fix fallback translations with Trans
- 🐛(pdf) fix table cell alignment issue in exported
  documents
- 🐛(pdf) preserve image aspect ratio in PDF export
2025-11-19 14:48:08 +01:00
Anthony LC 257de6d068 📌(frontend) pin Blocknote to 0.42.3
We were pinned to a specific package registry
coming from a pull request, the new version is now
published to npm, so we can use the version from there.
2025-11-19 14:48:08 +01:00
AntoLC 5a4c02a978 🌐(i18n) update translated strings
Update translated files with new translations
2025-11-19 12:26:04 +01:00
Manuel Raynaud 0090ccc981 🔥(backend) remove api managing templates
A complete API was able to manage templates lifecycle, from the creation
to the deletion and managing accesses on them. This API is not used by
the frontend application, is not finished. A connected user can interact
with this API and lead to unwanted behavior in the interface. Refering
ot issue #1222 templates can maybe totaly remove in the future. While
it's here and used, we only keep list and retrive endpoints. The
template management can still be done in the admin interface.
2025-11-19 11:23:31 +00:00
Anthony LC d403878f8c 🐛(frontend) fix alignment of side menu
Recent refactoring moved the side menu position.
This commit fixes its alignment.
2025-11-19 10:39:35 +01:00
Anthony LC 191b046641 📌(frontend) pin Blocknote to fix mounting issue
We have a mounting issue with the Blocknote
version 0.42.1. To not wait for the next Blocknote
release, we pin the Blocknote packages
to a specific pull request version.
2025-11-19 10:05:25 +01:00
Anthony LC aeac49d760 🔥(frontend) remove fix about color in export
The last version of Blocknote fixed the issue
with colors in exports.
We can remove our workaround now.
2025-11-18 16:19:56 +01:00
Anthony LC b5dcbbb057 ⬆️(frontend) upgrade Blocknote to 0.42.1
We upgraded Blocknote to version 0.42.1 to benefit
from the latest features and bug fixes.
There is some compatibility issues with
tiptap extensions, so we had to pin tiptap
to "3.10.2" to ensure everything works smoothly.
There is as well some issues with prosemirror,
we pinned the problematic packages.
2025-11-18 16:19:56 +01:00
Cyril 2e64298ff4 (pdf) preserve image aspect ratio in PDF export
images were distorted in PDF exports; height is now computed to fix that

Signed-off-by: Cyril <c.gromoff@gmail.com>
2025-11-18 15:36:42 +01:00
Manuel Raynaud 8dad9ea6c4 🔧(helm) create a symlink between helmfile config
We want to have both helmfile.yaml and helmfile.gotmpl present.
helmfile.gotmpl is for now a symlink of helmfile.yaml
2025-11-18 11:10:03 +00:00
Anthony LC 3ae8046ffc 🐛(frontend) fix fallback translations with Trans
The fallback of the Trans component didn't work
anymore after upgrade to 16.2.3 of react-i18next.
Upgrading to 16.3.3 fixed the issue.
We added a test to cover this case.
2025-11-17 15:23:48 +01:00
Anthony LC a4e3168682 🐛(export) fix export unsupported colors
Some colors bind to a text style are not supported.
It comes often from a paste style, we don't
display them if they are not supported by the
editor.
2025-11-17 10:33:17 +01:00
renovate[bot] c8955133a4 ⬆️(dependencies) update python dependencies 2025-11-17 08:29:46 +00:00
Anthony LC b069310bf0 🛂(frontend) disabled role not allowed to be assigned
We disable roles that the current user is not allowed
to assign when sharing a document. This prevents
users from selecting roles they cannot actually
assign, improving the user experience and reducing
confusion.
2025-11-17 08:48:16 +01:00
Manuel Raynaud 1292c33a58 ♻️(backend) rely on set_role_to from DocumentAskForAccess abilities
Like in other abilities, we compute a set_role_to property on the
abilities. This set_role_to contains all the roles lower or equal than
the current user role. We rely on this propoerty to validate the accept
endpoint and it will be used by the front allpication to built the role
select list.
2025-11-17 08:48:16 +01:00
Manuel Raynaud bf68a5ae40 🔒️(backend) remove owner as valid role for ask_for_access serializer
When a ask_for_access creation is made, we explicitly remove the owner
role to prevent role escalation.
2025-11-17 08:47:15 +01:00
Manuel Raynaud 8799b4aa2f 🔒️(backend) role in ask_for_access must be lower than user role
We check that the role set in a ask_for_access is not higher than the
user's role accepting the request. We prevent case where ad min will
grant a user owner in order to take control of the document. Only owner
can accept an owner role.
2025-11-17 08:47:14 +01:00
Cyril d96abb1ccf (frontend) make summary button fixed to remain visible during scroll
ensures persistent access to table of contents by fixing button position

Signed-off-by: Cyril <c.gromoff@gmail.com>
2025-11-14 13:42:56 +01:00
Cyril dc12a99d4a (pdf) fix table cell alignment issue in exported documents
ensures correct horizontal alignment of text based on cell props

Signed-off-by: Cyril <c.gromoff@gmail.com>
2025-11-14 13:10:03 +01:00
Cyril 82a0c1a770 (frontend) add unit test for mobile rendering in docheaderinfo
ensures numchild count is displayed correctly on mobile interface

Signed-off-by: Cyril <c.gromoff@gmail.com>
2025-11-13 13:41:35 +01:00
Cyril a758254b60 (frontend) improve mobile UX by showing subdocs count
helps users notice root documents have children in mobile view

Signed-off-by: Cyril <c.gromoff@gmail.com>
2025-11-13 13:41:25 +01:00
Cyril 6314cb3a18 (frontend) add focus trap and enter key support to remove doc modal
improves a11y by enabling keyboard-triggered modal with proper focus trap

Signed-off-by: Cyril <c.gromoff@gmail.com>
2025-11-13 12:59:11 +01:00
Cyril 3e410e3519 (frontend) enable ODT export for documents
provides ODT export with support for callout, upload, interlinking and tests

Signed-off-by: Cyril <c.gromoff@gmail.com>

(frontend) add image and interlinking support for odt export

Added image mapping with SVG conversion and clickable document links.

Signed-off-by: Cyril <c.gromoff@gmail.com>

(e2e) add e2e tests for odt export and interlinking features

covers odt document export and cross-section interlinking use cases

Signed-off-by: Cyril <c.gromoff@gmail.com>

(odt) add generic helper and style callout block for odt export

create odtRegisterParagraphStyleForBlock and apply background/padding styles

Signed-off-by: Cyril <c.gromoff@gmail.com>
2025-11-13 11:14:09 +01:00
Cyril aba7959344 (frontend) fix pdf embed to use full width
Signed-off-by: Cyril <c.gromoff@gmail.com>
2025-11-12 11:28:10 +01:00
Cyril 3d45c7c215 (frontend) improve accessibility and styling of summary table
adds semantic structure, aria attributes, and token-based focus styling

Signed-off-by: Cyril <c.gromoff@gmail.com>
2025-11-12 10:02:05 +01:00
Cyril cdb26b480a (frontend) preserve @ character when esc is pressed after typing it
improves user experience by keeping @ symbol after cancelling mention trigger

Signed-off-by: Cyril <c.gromoff@gmail.com>
2025-11-12 09:24:14 +01:00
Cyril 23a0f2761f (docx) fix image overflow by limiting width to 600px during export
ensures all images keep proportions and stay within page bounds in docx export

Signed-off-by: Cyril <c.gromoff@gmail.com>
2025-11-12 08:40:05 +01:00
Cyril 0d596e338c (frontend) move editor button out of grid and fix roles/aria-label
improves accessibility and layout consistency of editor interface

Signed-off-by: Cyril <c.gromoff@gmail.com>
2025-11-10 18:13:33 +01:00
79 changed files with 1974 additions and 2301 deletions
+37 -1
View File
@@ -6,6 +6,41 @@ and this project adheres to
## [Unreleased]
## [3.10.0] - 2025-11-18
### Added
- ✨(export) enable ODT export for documents #1524
- ✨(frontend) improve mobile UX by showing subdocs count #1540
### Changed
- ♻️(frontend) preserve @ character when esc is pressed after typing it #1512
- ♻️(frontend) make summary button fixed to remain visible during scroll #1581
- ♻️(frontend) pdf embed use full width #1526
### Fixed
- ♿(frontend) improve accessibility:
- ♿(frontend) improve ARIA in doc grid and editor for a11y #1519
- ♿(frontend) improve accessibility and styling of summary table #1528
- ♿(frontend) add focus trap and enter key support to remove doc modal #1531
- 🐛(frontend) fix alignment of side menu #1597
- 🐛(frontend) fix fallback translations with Trans #1620
- 🐛(export) fix image overflow by limiting width to 600px during export #1525
- 🐛(export) fix table cell alignment issue in exported documents #1582
- 🐛(export) preserve image aspect ratio in PDF export #1622
- 🐛(export) Export fails when paste with style #1552
### Security
- mitigate role escalation in the ask_for_access viewset #1580
### Removed
- 🔥(backend) remove api managing templates
## [3.9.0] - 2025-11-10
### Added
@@ -843,7 +878,8 @@ and this project adheres to
- ✨(frontend) Coming Soon page (#67)
- 🚀 Impress, project to manage your documents easily and collaboratively.
[unreleased]: https://github.com/suitenumerique/docs/compare/v3.9.0...main
[unreleased]: https://github.com/suitenumerique/docs/compare/v3.10.0...main
[v3.10.0]: https://github.com/suitenumerique/docs/releases/v3.10.0
[v3.9.0]: https://github.com/suitenumerique/docs/releases/v3.9.0
[v3.8.2]: https://github.com/suitenumerique/docs/releases/v3.8.2
[v3.8.1]: https://github.com/suitenumerique/docs/releases/v3.8.1
+5 -3
View File
@@ -786,7 +786,9 @@ class DocumentAskForAccessCreateSerializer(serializers.Serializer):
"""Serializer for creating a document ask for access."""
role = serializers.ChoiceField(
choices=models.RoleChoices.choices,
choices=[
role for role in choices.RoleChoices if role != models.RoleChoices.OWNER
],
required=False,
default=models.RoleChoices.READER,
)
@@ -810,11 +812,11 @@ class DocumentAskForAccessSerializer(serializers.ModelSerializer):
]
read_only_fields = ["id", "document", "user", "role", "created_at", "abilities"]
def get_abilities(self, invitation) -> dict:
def get_abilities(self, instance) -> dict:
"""Return abilities of the logged-in user on the instance."""
request = self.context.get("request")
if request:
return invitation.get_abilities(request.user)
return instance.get_abilities(request.user)
return {}
+12 -98
View File
@@ -1831,10 +1831,7 @@ class DocumentAccessViewSet(
class TemplateViewSet(
drf.mixins.CreateModelMixin,
drf.mixins.DestroyModelMixin,
drf.mixins.RetrieveModelMixin,
drf.mixins.UpdateModelMixin,
viewsets.GenericViewSet,
):
"""Template ViewSet"""
@@ -1890,100 +1887,6 @@ class TemplateViewSet(
serializer = self.get_serializer(queryset, many=True)
return drf.response.Response(serializer.data)
@transaction.atomic
def perform_create(self, serializer):
"""Set the current user as owner of the newly created object."""
obj = serializer.save()
models.TemplateAccess.objects.create(
template=obj,
user=self.request.user,
role=models.RoleChoices.OWNER,
)
class TemplateAccessViewSet(
ResourceAccessViewsetMixin,
drf.mixins.CreateModelMixin,
drf.mixins.DestroyModelMixin,
drf.mixins.RetrieveModelMixin,
drf.mixins.UpdateModelMixin,
viewsets.GenericViewSet,
):
"""
API ViewSet for all interactions with template accesses.
GET /api/v1.0/templates/<template_id>/accesses/:<template_access_id>
Return list of all template accesses related to the logged-in user or one
template access if an id is provided.
POST /api/v1.0/templates/<template_id>/accesses/ with expected data:
- user: str
- role: str [administrator|editor|reader]
Return newly created template access
PUT /api/v1.0/templates/<template_id>/accesses/<template_access_id>/ with expected data:
- role: str [owner|admin|editor|reader]
Return updated template access
PATCH /api/v1.0/templates/<template_id>/accesses/<template_access_id>/ with expected data:
- role: str [owner|admin|editor|reader]
Return partially updated template access
DELETE /api/v1.0/templates/<template_id>/accesses/<template_access_id>/
Delete targeted template access
"""
lookup_field = "pk"
permission_classes = [permissions.ResourceAccessPermission]
throttle_scope = "template_access"
queryset = models.TemplateAccess.objects.select_related("user").all()
resource_field_name = "template"
serializer_class = serializers.TemplateAccessSerializer
@cached_property
def template(self):
"""Get related template from resource ID in url."""
try:
return models.Template.objects.get(pk=self.kwargs["resource_id"])
except models.Template.DoesNotExist as excpt:
raise drf.exceptions.NotFound() from excpt
def list(self, request, *args, **kwargs):
"""Restrict templates returned by the list endpoint"""
user = self.request.user
teams = user.teams
queryset = self.filter_queryset(self.get_queryset())
# Limit to resource access instances related to a resource THAT also has
# a resource access instance for the logged-in user (we don't want to list
# only the resource access instances pointing to the logged-in user)
queryset = queryset.filter(
db.Q(template__accesses__user=user)
| db.Q(template__accesses__team__in=teams),
).distinct()
serializer = self.get_serializer(queryset, many=True)
return drf.response.Response(serializer.data)
def perform_create(self, serializer):
"""
Actually create the new template access:
- Ensures the `template_id` is explicitly set from the URL.
- If the assigned role is `OWNER`, checks that the requesting user is an owner
of the document. This is the only permission check deferred until this step;
all other access checks are handled earlier in the permission lifecycle.
"""
role = serializer.validated_data.get("role")
if (
role == choices.RoleChoices.OWNER
and self.template.get_role(self.request.user) != choices.RoleChoices.OWNER
):
raise drf.exceptions.PermissionDenied(
"Only owners of a template can assign other users as owners."
)
serializer.save(template_id=self.kwargs["resource_id"])
class InvitationViewset(
drf.mixins.CreateModelMixin,
@@ -2162,7 +2065,18 @@ class DocumentAskForAccessViewSet(
serializer = serializers.RoleSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
document_ask_for_access.accept(role=serializer.validated_data.get("role"))
target_role = serializer.validated_data.get(
"role", document_ask_for_access.role
)
abilities = document_ask_for_access.get_abilities(request.user)
if target_role not in abilities["set_role_to"]:
return drf.response.Response(
{"detail": "You cannot accept a role higher than your own."},
status=drf.status.HTTP_400_BAD_REQUEST,
)
document_ask_for_access.accept(role=target_role)
return drf.response.Response(status=drf.status.HTTP_204_NO_CONTENT)
+8 -16
View File
@@ -1205,23 +1205,14 @@ class DocumentAskForAccess(BaseModel):
def get_abilities(self, user):
"""Compute and return abilities for a given user."""
roles = []
user_role = self.document.get_role(user)
is_admin_or_owner = user_role in PRIVILEGED_ROLES
if user.is_authenticated:
teams = user.teams
try:
roles = self.user_roles or []
except AttributeError:
try:
roles = self.document.accesses.filter(
models.Q(user=user) | models.Q(team__in=teams),
).values_list("role", flat=True)
except (self._meta.model.DoesNotExist, IndexError):
roles = []
is_admin_or_owner = bool(
set(roles).intersection({RoleChoices.OWNER, RoleChoices.ADMIN})
)
set_role_to = [
role
for role in RoleChoices.values
if RoleChoices.get_priority(role) <= RoleChoices.get_priority(user_role)
]
return {
"destroy": is_admin_or_owner,
@@ -1229,6 +1220,7 @@ class DocumentAskForAccess(BaseModel):
"partial_update": is_admin_or_owner,
"retrieve": is_admin_or_owner,
"accept": is_admin_or_owner,
"set_role_to": set_role_to,
}
def accept(self, role=None):
@@ -115,7 +115,10 @@ def test_api_documents_ask_for_access_create_authenticated_non_root_document():
assert response.status_code == 404
def test_api_documents_ask_for_access_create_authenticated_specific_role():
@pytest.mark.parametrize(
"role", [role for role in RoleChoices if role != RoleChoices.OWNER]
)
def test_api_documents_ask_for_access_create_authenticated_specific_role(role):
"""
Authenticated users should be able to create a document ask for access with a specific role.
"""
@@ -127,17 +130,35 @@ def test_api_documents_ask_for_access_create_authenticated_specific_role():
response = client.post(
f"/api/v1.0/documents/{document.id}/ask-for-access/",
data={"role": RoleChoices.EDITOR},
data={"role": role},
)
assert response.status_code == 201
assert DocumentAskForAccess.objects.filter(
document=document,
user=user,
role=RoleChoices.EDITOR,
role=role,
).exists()
def test_api_documents_ask_for_access_create_authenticated_owner_role():
"""
Authenticated users should not be able to create a document ask for access with the owner role.
"""
document = DocumentFactory()
user = UserFactory()
client = APIClient()
client.force_login(user)
response = client.post(
f"/api/v1.0/documents/{document.id}/ask-for-access/",
data={"role": RoleChoices.OWNER},
)
assert response.status_code == 400
assert response.json() == {"role": ['"owner" is not a valid choice.']}
def test_api_documents_ask_for_access_create_authenticated_already_has_access():
"""Authenticated users with existing access can ask for access with a different role."""
user = UserFactory()
@@ -266,6 +287,7 @@ def test_api_documents_ask_for_access_list_authenticated_own_request():
"update": False,
"partial_update": False,
"retrieve": False,
"set_role_to": [],
},
}
],
@@ -335,6 +357,15 @@ def test_api_documents_ask_for_access_list_owner_or_admin(role):
response = client.get(f"/api/v1.0/documents/{document.id}/ask-for-access/")
assert response.status_code == 200
expected_set_role_to = [
RoleChoices.READER,
RoleChoices.EDITOR,
RoleChoices.ADMIN,
]
if role == RoleChoices.OWNER:
expected_set_role_to.append(RoleChoices.OWNER)
assert response.json() == {
"count": 3,
"next": None,
@@ -354,6 +385,7 @@ def test_api_documents_ask_for_access_list_owner_or_admin(role):
"update": True,
"partial_update": True,
"retrieve": True,
"set_role_to": expected_set_role_to,
},
}
for document_ask_for_access in document_ask_for_accesses
@@ -446,6 +478,13 @@ def test_api_documents_ask_for_access_retrieve_owner_or_admin(role):
f"/api/v1.0/documents/{document.id}/ask-for-access/{document_ask_for_access.id}/"
)
assert response.status_code == 200
expected_set_role_to = [
RoleChoices.READER,
RoleChoices.EDITOR,
RoleChoices.ADMIN,
]
if role == RoleChoices.OWNER:
expected_set_role_to.append(RoleChoices.OWNER)
assert response.json() == {
"id": str(document_ask_for_access.id),
"document": str(document.id),
@@ -460,6 +499,7 @@ def test_api_documents_ask_for_access_retrieve_owner_or_admin(role):
"update": True,
"partial_update": True,
"retrieve": True,
"set_role_to": expected_set_role_to,
},
}
@@ -749,6 +789,53 @@ def test_api_documents_ask_for_access_accept_authenticated_owner_or_admin_update
assert document_access.role == RoleChoices.ADMIN
def test_api_documents_ask_for_access_accept_admin_cannot_accept_owner_role():
"""
Admin users should not be able to accept document ask for access with the owner role.
"""
user = UserFactory()
document = DocumentFactory(users=[(user, RoleChoices.ADMIN)])
document_ask_for_access = DocumentAskForAccessFactory(
document=document, role=RoleChoices.READER
)
client = APIClient()
client.force_login(user)
response = client.post(
f"/api/v1.0/documents/{document.id}/ask-for-access/{document_ask_for_access.id}/accept/",
data={"role": RoleChoices.OWNER},
)
assert response.status_code == 400
assert response.json() == {
"detail": "You cannot accept a role higher than your own."
}
def test_api_documents_ask_for_access_accept_owner_can_accept_owner_role():
"""
Owner users should be able to accept document ask for access with the owner role.
"""
user = UserFactory()
document = DocumentFactory(users=[(user, RoleChoices.OWNER)])
document_ask_for_access = DocumentAskForAccessFactory(
document=document, role=RoleChoices.READER
)
client = APIClient()
client.force_login(user)
response = client.post(
f"/api/v1.0/documents/{document.id}/ask-for-access/{document_ask_for_access.id}/accept/",
data={"role": RoleChoices.OWNER},
)
assert response.status_code == 204
assert not DocumentAskForAccess.objects.filter(
id=document_ask_for_access.id
).exists()
@pytest.mark.parametrize("role", [RoleChoices.OWNER, RoleChoices.ADMIN])
def test_api_documents_ask_for_access_accept_authenticated_non_root_document(role):
"""
@@ -1,799 +0,0 @@
"""
Test template accesses API endpoints for users in impress's core app.
"""
import random
from unittest import mock
from uuid import uuid4
import pytest
from rest_framework.test import APIClient
from core import factories, models
from core.api import serializers
from core.tests.conftest import TEAM, USER, VIA
pytestmark = pytest.mark.django_db
def test_api_template_accesses_list_anonymous():
"""Anonymous users should not be allowed to list template accesses."""
template = factories.TemplateFactory()
factories.UserTemplateAccessFactory.create_batch(2, template=template)
response = APIClient().get(f"/api/v1.0/templates/{template.id!s}/accesses/")
assert response.status_code == 401
assert response.json() == {
"detail": "Authentication credentials were not provided."
}
def test_api_template_accesses_list_authenticated_unrelated():
"""
Authenticated users should not be allowed to list template accesses for a template
to which they are not related.
"""
user = factories.UserFactory(with_owned_template=True)
client = APIClient()
client.force_login(user)
template = factories.TemplateFactory()
factories.UserTemplateAccessFactory.create_batch(3, template=template)
# Accesses for other templates to which the user is related should not be listed either
other_access = factories.UserTemplateAccessFactory(user=user)
factories.UserTemplateAccessFactory(template=other_access.template)
response = client.get(
f"/api/v1.0/templates/{template.id!s}/accesses/",
)
assert response.status_code == 200
assert response.json() == []
@pytest.mark.parametrize("via", VIA)
def test_api_template_accesses_list_authenticated_related(via, mock_user_teams):
"""
Authenticated users should be able to list template accesses for a template
to which they are directly related, whatever their role in the template.
"""
user = factories.UserFactory(with_owned_template=True)
client = APIClient()
client.force_login(user)
template = factories.TemplateFactory()
user_access = None
if via == USER:
user_access = models.TemplateAccess.objects.create(
template=template,
user=user,
role=random.choice(models.RoleChoices.values),
)
elif via == TEAM:
mock_user_teams.return_value = ["lasuite", "unknown"]
user_access = models.TemplateAccess.objects.create(
template=template,
team="lasuite",
role=random.choice(models.RoleChoices.values),
)
access1 = factories.TeamTemplateAccessFactory(template=template)
access2 = factories.UserTemplateAccessFactory(template=template)
# Accesses for other templates to which the user is related should not be listed either
other_access = factories.UserTemplateAccessFactory(user=user)
factories.UserTemplateAccessFactory(template=other_access.template)
response = client.get(
f"/api/v1.0/templates/{template.id!s}/accesses/",
)
assert response.status_code == 200
content = response.json()
assert len(content) == 3
assert sorted(content, key=lambda x: x["id"]) == sorted(
[
{
"id": str(user_access.id),
"user": str(user.id) if via == "user" else None,
"team": "lasuite" if via == "team" else "",
"role": user_access.role,
"abilities": user_access.get_abilities(user),
},
{
"id": str(access1.id),
"user": None,
"team": access1.team,
"role": access1.role,
"abilities": access1.get_abilities(user),
},
{
"id": str(access2.id),
"user": str(access2.user.id),
"team": "",
"role": access2.role,
"abilities": access2.get_abilities(user),
},
],
key=lambda x: x["id"],
)
def test_api_template_accesses_retrieve_anonymous():
"""
Anonymous users should not be allowed to retrieve a template access.
"""
access = factories.UserTemplateAccessFactory()
response = APIClient().get(
f"/api/v1.0/templates/{access.template_id!s}/accesses/{access.id!s}/",
)
assert response.status_code == 401
assert response.json() == {
"detail": "Authentication credentials were not provided."
}
def test_api_template_accesses_retrieve_authenticated_unrelated():
"""
Authenticated users should not be allowed to retrieve a template access for
a template to which they are not related.
"""
user = factories.UserFactory(with_owned_template=True)
client = APIClient()
client.force_login(user)
template = factories.TemplateFactory()
access = factories.UserTemplateAccessFactory(template=template)
response = client.get(
f"/api/v1.0/templates/{template.id!s}/accesses/{access.id!s}/",
)
assert response.status_code == 403
assert response.json() == {
"detail": "You do not have permission to perform this action."
}
# Accesses related to another template should be excluded even if the user is related to it
for access in [
factories.UserTemplateAccessFactory(),
factories.UserTemplateAccessFactory(user=user),
]:
response = client.get(
f"/api/v1.0/templates/{template.id!s}/accesses/{access.id!s}/",
)
assert response.status_code == 404
assert response.json() == {
"detail": "No TemplateAccess matches the given query."
}
@pytest.mark.parametrize("via", VIA)
def test_api_template_accesses_retrieve_authenticated_related(via, mock_user_teams):
"""
A user who is related to a template should be allowed to retrieve the
associated template user accesses.
"""
user = factories.UserFactory(with_owned_template=True)
client = APIClient()
client.force_login(user)
template = factories.TemplateFactory()
if via == USER:
factories.UserTemplateAccessFactory(template=template, user=user)
elif via == TEAM:
mock_user_teams.return_value = ["lasuite", "unknown"]
factories.TeamTemplateAccessFactory(template=template, team="lasuite")
access = factories.UserTemplateAccessFactory(template=template)
response = client.get(
f"/api/v1.0/templates/{template.id!s}/accesses/{access.id!s}/",
)
assert response.status_code == 200
assert response.json() == {
"id": str(access.id),
"user": str(access.user.id),
"team": "",
"role": access.role,
"abilities": access.get_abilities(user),
}
def test_api_template_accesses_update_anonymous():
"""Anonymous users should not be allowed to update a template access."""
access = factories.UserTemplateAccessFactory()
old_values = serializers.TemplateAccessSerializer(instance=access).data
new_values = {
"id": uuid4(),
"user": factories.UserFactory().id,
"role": random.choice(models.RoleChoices.values),
}
api_client = APIClient()
for field, value in new_values.items():
response = api_client.put(
f"/api/v1.0/templates/{access.template_id!s}/accesses/{access.id!s}/",
{**old_values, field: value},
format="json",
)
assert response.status_code == 401
access.refresh_from_db()
updated_values = serializers.TemplateAccessSerializer(instance=access).data
assert updated_values == old_values
def test_api_template_accesses_update_authenticated_unrelated():
"""
Authenticated users should not be allowed to update a template access for a template to which
they are not related.
"""
user = factories.UserFactory(with_owned_template=True)
client = APIClient()
client.force_login(user)
access = factories.UserTemplateAccessFactory()
old_values = serializers.TemplateAccessSerializer(instance=access).data
new_values = {
"id": uuid4(),
"user": factories.UserFactory().id,
"role": random.choice(models.RoleChoices.values),
}
for field, value in new_values.items():
response = client.put(
f"/api/v1.0/templates/{access.template_id!s}/accesses/{access.id!s}/",
{**old_values, field: value},
format="json",
)
assert response.status_code == 403
access.refresh_from_db()
updated_values = serializers.TemplateAccessSerializer(instance=access).data
assert updated_values == old_values
@pytest.mark.parametrize("role", ["reader", "editor"])
@pytest.mark.parametrize("via", VIA)
def test_api_template_accesses_update_authenticated_editor_or_reader(
via, role, mock_user_teams
):
"""Editors or readers of a template should not be allowed to update its accesses."""
user = factories.UserFactory(with_owned_template=True)
client = APIClient()
client.force_login(user)
template = factories.TemplateFactory()
if via == USER:
factories.UserTemplateAccessFactory(template=template, user=user, role=role)
elif via == TEAM:
mock_user_teams.return_value = ["lasuite", "unknown"]
factories.TeamTemplateAccessFactory(
template=template, team="lasuite", role=role
)
access = factories.UserTemplateAccessFactory(template=template)
old_values = serializers.TemplateAccessSerializer(instance=access).data
new_values = {
"id": uuid4(),
"user": factories.UserFactory().id,
"role": random.choice(models.RoleChoices.values),
}
for field, value in new_values.items():
response = client.put(
f"/api/v1.0/templates/{access.template_id!s}/accesses/{access.id!s}/",
{**old_values, field: value},
format="json",
)
assert response.status_code == 403
access.refresh_from_db()
updated_values = serializers.TemplateAccessSerializer(instance=access).data
assert updated_values == old_values
@pytest.mark.parametrize("via", VIA)
def test_api_template_accesses_update_administrator_except_owner(via, mock_user_teams):
"""
A user who is a direct administrator in a template should be allowed to update a user
access for this template, as long as they don't try to set the role to owner.
"""
user = factories.UserFactory(with_owned_template=True)
client = APIClient()
client.force_login(user)
template = factories.TemplateFactory()
if via == USER:
factories.UserTemplateAccessFactory(
template=template, user=user, role="administrator"
)
elif via == TEAM:
mock_user_teams.return_value = ["lasuite", "unknown"]
factories.TeamTemplateAccessFactory(
template=template, team="lasuite", role="administrator"
)
access = factories.UserTemplateAccessFactory(
template=template,
role=random.choice(["administrator", "editor", "reader"]),
)
old_values = serializers.TemplateAccessSerializer(instance=access).data
new_values = {
"id": uuid4(),
"user_id": factories.UserFactory().id,
"role": random.choice(["administrator", "editor", "reader"]),
}
for field, value in new_values.items():
new_data = {**old_values, field: value}
response = client.put(
f"/api/v1.0/templates/{template.id!s}/accesses/{access.id!s}/",
data=new_data,
format="json",
)
if (
new_data["role"] == old_values["role"]
): # we are not really updating the role
assert response.status_code == 403
else:
assert response.status_code == 200
access.refresh_from_db()
updated_values = serializers.TemplateAccessSerializer(instance=access).data
if field == "role":
assert updated_values == {**old_values, "role": new_values["role"]}
else:
assert updated_values == old_values
@pytest.mark.parametrize("via", VIA)
def test_api_template_accesses_update_administrator_from_owner(via, mock_user_teams):
"""
A user who is an administrator in a template, should not be allowed to update
the user access of an "owner" for this template.
"""
user = factories.UserFactory(with_owned_template=True)
client = APIClient()
client.force_login(user)
template = factories.TemplateFactory()
if via == USER:
factories.UserTemplateAccessFactory(
template=template, user=user, role="administrator"
)
elif via == TEAM:
mock_user_teams.return_value = ["lasuite", "unknown"]
factories.TeamTemplateAccessFactory(
template=template, team="lasuite", role="administrator"
)
other_user = factories.UserFactory()
access = factories.UserTemplateAccessFactory(
template=template, user=other_user, role="owner"
)
old_values = serializers.TemplateAccessSerializer(instance=access).data
new_values = {
"id": uuid4(),
"user_id": factories.UserFactory().id,
"role": random.choice(models.RoleChoices.values),
}
for field, value in new_values.items():
response = client.put(
f"/api/v1.0/templates/{template.id!s}/accesses/{access.id!s}/",
data={**old_values, field: value},
format="json",
)
assert response.status_code == 403
access.refresh_from_db()
updated_values = serializers.TemplateAccessSerializer(instance=access).data
assert updated_values == old_values
@pytest.mark.parametrize("via", VIA)
def test_api_template_accesses_update_administrator_to_owner(via, mock_user_teams):
"""
A user who is an administrator in a template, should not be allowed to update
the user access of another user to grant template ownership.
"""
user = factories.UserFactory(with_owned_template=True)
client = APIClient()
client.force_login(user)
template = factories.TemplateFactory()
if via == USER:
factories.UserTemplateAccessFactory(
template=template, user=user, role="administrator"
)
elif via == TEAM:
mock_user_teams.return_value = ["lasuite", "unknown"]
factories.TeamTemplateAccessFactory(
template=template, team="lasuite", role="administrator"
)
other_user = factories.UserFactory()
access = factories.UserTemplateAccessFactory(
template=template,
user=other_user,
role=random.choice(["administrator", "editor", "reader"]),
)
old_values = serializers.TemplateAccessSerializer(instance=access).data
new_values = {
"id": uuid4(),
"user_id": factories.UserFactory().id,
"role": "owner",
}
for field, value in new_values.items():
new_data = {**old_values, field: value}
response = client.put(
f"/api/v1.0/templates/{template.id!s}/accesses/{access.id!s}/",
data=new_data,
format="json",
)
# We are not allowed or not really updating the role
if field == "role" or new_data["role"] == old_values["role"]:
assert response.status_code == 403
else:
assert response.status_code == 200
access.refresh_from_db()
updated_values = serializers.TemplateAccessSerializer(instance=access).data
assert updated_values == old_values
@pytest.mark.parametrize("via", VIA)
def test_api_template_accesses_update_owner(via, mock_user_teams):
"""
A user who is an owner in a template should be allowed to update
a user access for this template whatever the role.
"""
user = factories.UserFactory(with_owned_template=True)
client = APIClient()
client.force_login(user)
template = factories.TemplateFactory()
if via == USER:
factories.UserTemplateAccessFactory(template=template, user=user, role="owner")
elif via == TEAM:
mock_user_teams.return_value = ["lasuite", "unknown"]
factories.TeamTemplateAccessFactory(
template=template, team="lasuite", role="owner"
)
factories.UserFactory()
access = factories.UserTemplateAccessFactory(
template=template,
)
old_values = serializers.TemplateAccessSerializer(instance=access).data
new_values = {
"id": uuid4(),
"user_id": factories.UserFactory().id,
"role": random.choice(models.RoleChoices.values),
}
for field, value in new_values.items():
new_data = {**old_values, field: value}
response = client.put(
f"/api/v1.0/templates/{template.id!s}/accesses/{access.id!s}/",
data=new_data,
format="json",
)
if (
new_data["role"] == old_values["role"]
): # we are not really updating the role
assert response.status_code == 403
else:
assert response.status_code == 200
access.refresh_from_db()
updated_values = serializers.TemplateAccessSerializer(instance=access).data
if field == "role":
assert updated_values == {**old_values, "role": new_values["role"]}
else:
assert updated_values == old_values
@pytest.mark.parametrize("via", VIA)
def test_api_template_accesses_update_owner_self(via, mock_user_teams):
"""
A user who is owner of a template should be allowed to update
their own user access provided there are other owners in the template.
"""
user = factories.UserFactory(with_owned_template=True)
client = APIClient()
client.force_login(user)
template = factories.TemplateFactory()
if via == TEAM:
mock_user_teams.return_value = ["lasuite", "unknown"]
access = factories.TeamTemplateAccessFactory(
template=template, team="lasuite", role="owner"
)
else:
access = factories.UserTemplateAccessFactory(
template=template, user=user, role="owner"
)
old_values = serializers.TemplateAccessSerializer(instance=access).data
new_role = random.choice(["administrator", "editor", "reader"])
response = client.put(
f"/api/v1.0/templates/{template.id!s}/accesses/{access.id!s}/",
data={**old_values, "role": new_role},
format="json",
)
assert response.status_code == 403
access.refresh_from_db()
assert access.role == "owner"
# Add another owner and it should now work
factories.UserTemplateAccessFactory(template=template, role="owner")
response = client.put(
f"/api/v1.0/templates/{template.id!s}/accesses/{access.id!s}/",
data={**old_values, "role": new_role},
format="json",
)
assert response.status_code == 200
access.refresh_from_db()
assert access.role == new_role
# Delete
def test_api_template_accesses_delete_anonymous():
"""Anonymous users should not be allowed to destroy a template access."""
access = factories.UserTemplateAccessFactory()
response = APIClient().delete(
f"/api/v1.0/templates/{access.template_id!s}/accesses/{access.id!s}/",
)
assert response.status_code == 401
assert models.TemplateAccess.objects.count() == 1
def test_api_template_accesses_delete_authenticated():
"""
Authenticated users should not be allowed to delete a template access for a
template to which they are not related.
"""
user = factories.UserFactory(with_owned_template=True)
client = APIClient()
client.force_login(user)
access = factories.UserTemplateAccessFactory()
response = client.delete(
f"/api/v1.0/templates/{access.template_id!s}/accesses/{access.id!s}/",
)
assert response.status_code == 403
assert models.TemplateAccess.objects.count() == 2
@pytest.mark.parametrize("role", ["reader", "editor"])
@pytest.mark.parametrize("via", VIA)
def test_api_template_accesses_delete_editor_or_reader(via, role, mock_user_teams):
"""
Authenticated users should not be allowed to delete a template access for a
template in which they are a simple editor or reader.
"""
user = factories.UserFactory(with_owned_template=True)
client = APIClient()
client.force_login(user)
template = factories.TemplateFactory()
if via == USER:
factories.UserTemplateAccessFactory(template=template, user=user, role=role)
elif via == TEAM:
mock_user_teams.return_value = ["lasuite", "unknown"]
factories.TeamTemplateAccessFactory(
template=template, team="lasuite", role=role
)
access = factories.UserTemplateAccessFactory(template=template)
assert models.TemplateAccess.objects.count() == 3
assert models.TemplateAccess.objects.filter(user=access.user).exists()
response = client.delete(
f"/api/v1.0/templates/{template.id!s}/accesses/{access.id!s}/",
)
assert response.status_code == 403
assert models.TemplateAccess.objects.count() == 3
@pytest.mark.parametrize("via", VIA)
def test_api_template_accesses_delete_administrators_except_owners(
via, mock_user_teams
):
"""
Users who are administrators in a template should be allowed to delete an access
from the template provided it is not ownership.
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
template = factories.TemplateFactory()
if via == USER:
factories.UserTemplateAccessFactory(
template=template, user=user, role="administrator"
)
elif via == TEAM:
mock_user_teams.return_value = ["lasuite", "unknown"]
factories.TeamTemplateAccessFactory(
template=template, team="lasuite", role="administrator"
)
access = factories.UserTemplateAccessFactory(
template=template, role=random.choice(["reader", "editor", "administrator"])
)
assert models.TemplateAccess.objects.count() == 2
assert models.TemplateAccess.objects.filter(user=access.user).exists()
response = client.delete(
f"/api/v1.0/templates/{template.id!s}/accesses/{access.id!s}/",
)
assert response.status_code == 204
assert models.TemplateAccess.objects.count() == 1
@pytest.mark.parametrize("via", VIA)
def test_api_template_accesses_delete_administrator_on_owners(via, mock_user_teams):
"""
Users who are administrators in a template should not be allowed to delete an ownership
access from the template.
"""
user = factories.UserFactory(with_owned_template=True)
client = APIClient()
client.force_login(user)
template = factories.TemplateFactory()
if via == USER:
factories.UserTemplateAccessFactory(
template=template, user=user, role="administrator"
)
elif via == TEAM:
mock_user_teams.return_value = ["lasuite", "unknown"]
factories.TeamTemplateAccessFactory(
template=template, team="lasuite", role="administrator"
)
access = factories.UserTemplateAccessFactory(template=template, role="owner")
assert models.TemplateAccess.objects.count() == 3
assert models.TemplateAccess.objects.filter(user=access.user).exists()
response = client.delete(
f"/api/v1.0/templates/{template.id!s}/accesses/{access.id!s}/",
)
assert response.status_code == 403
assert models.TemplateAccess.objects.count() == 3
@pytest.mark.parametrize("via", VIA)
def test_api_template_accesses_delete_owners(via, mock_user_teams):
"""
Users should be able to delete the template access of another user
for a template of which they are owner.
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
template = factories.TemplateFactory()
if via == USER:
factories.UserTemplateAccessFactory(template=template, user=user, role="owner")
elif via == TEAM:
mock_user_teams.return_value = ["lasuite", "unknown"]
factories.TeamTemplateAccessFactory(
template=template, team="lasuite", role="owner"
)
access = factories.UserTemplateAccessFactory(template=template)
assert models.TemplateAccess.objects.count() == 2
assert models.TemplateAccess.objects.filter(user=access.user).exists()
response = client.delete(
f"/api/v1.0/templates/{template.id!s}/accesses/{access.id!s}/",
)
assert response.status_code == 204
assert models.TemplateAccess.objects.count() == 1
@pytest.mark.parametrize("via", VIA)
def test_api_template_accesses_delete_owners_last_owner(via, mock_user_teams):
"""
It should not be possible to delete the last owner access from a template
"""
user = factories.UserFactory(with_owned_template=True)
client = APIClient()
client.force_login(user)
template = factories.TemplateFactory()
access = None
if via == USER:
access = factories.UserTemplateAccessFactory(
template=template, user=user, role="owner"
)
elif via == TEAM:
mock_user_teams.return_value = ["lasuite", "unknown"]
access = factories.TeamTemplateAccessFactory(
template=template, team="lasuite", role="owner"
)
assert models.TemplateAccess.objects.count() == 2
response = client.delete(
f"/api/v1.0/templates/{template.id!s}/accesses/{access.id!s}/",
)
assert response.status_code == 403
assert models.TemplateAccess.objects.count() == 2
def test_api_template_accesses_throttling(settings):
"""Test api template accesses throttling."""
current_rate = settings.REST_FRAMEWORK["DEFAULT_THROTTLE_RATES"]["template_access"]
settings.REST_FRAMEWORK["DEFAULT_THROTTLE_RATES"]["template_access"] = "2/minute"
template = factories.TemplateFactory()
user = factories.UserFactory()
factories.UserTemplateAccessFactory(
template=template, user=user, role="administrator"
)
client = APIClient()
client.force_login(user)
for _i in range(2):
response = client.get(f"/api/v1.0/templates/{template.id!s}/accesses/")
assert response.status_code == 200
with mock.patch("core.api.throttling.capture_message") as mock_capture_message:
response = client.get(f"/api/v1.0/templates/{template.id!s}/accesses/")
assert response.status_code == 429
mock_capture_message.assert_called_once_with(
"Rate limit exceeded for scope template_access", "warning"
)
settings.REST_FRAMEWORK["DEFAULT_THROTTLE_RATES"]["template_access"] = current_rate
@@ -1,206 +0,0 @@
"""
Test template accesses create API endpoint for users in impress's core app.
"""
import random
import pytest
from rest_framework.test import APIClient
from core import factories, models
from core.tests.conftest import TEAM, USER, VIA
pytestmark = pytest.mark.django_db
def test_api_template_accesses_create_anonymous():
"""Anonymous users should not be allowed to create template accesses."""
template = factories.TemplateFactory()
other_user = factories.UserFactory()
response = APIClient().post(
f"/api/v1.0/templates/{template.id!s}/accesses/",
{
"user": str(other_user.id),
"template": str(template.id),
"role": random.choice(models.RoleChoices.values),
},
format="json",
)
assert response.status_code == 401
assert response.json() == {
"detail": "Authentication credentials were not provided."
}
assert models.TemplateAccess.objects.exists() is False
def test_api_template_accesses_create_authenticated_unrelated():
"""
Authenticated users should not be allowed to create template accesses for a template to
which they are not related.
"""
user = factories.UserFactory(with_owned_template=True)
client = APIClient()
client.force_login(user)
other_user = factories.UserFactory()
template = factories.TemplateFactory()
response = client.post(
f"/api/v1.0/templates/{template.id!s}/accesses/",
{
"user": str(other_user.id),
},
format="json",
)
assert response.status_code == 403
assert not models.TemplateAccess.objects.filter(user=other_user).exists()
@pytest.mark.parametrize("role", ["reader", "editor"])
@pytest.mark.parametrize("via", VIA)
def test_api_template_accesses_create_authenticated_editor_or_reader(
via, role, mock_user_teams
):
"""Editors or readers of a template should not be allowed to create template accesses."""
user = factories.UserFactory(with_owned_template=True)
client = APIClient()
client.force_login(user)
template = factories.TemplateFactory()
if via == USER:
factories.UserTemplateAccessFactory(template=template, user=user, role=role)
elif via == TEAM:
mock_user_teams.return_value = ["lasuite", "unknown"]
factories.TeamTemplateAccessFactory(
template=template, team="lasuite", role=role
)
other_user = factories.UserFactory()
for new_role in [role[0] for role in models.RoleChoices.choices]:
response = client.post(
f"/api/v1.0/templates/{template.id!s}/accesses/",
{
"user": str(other_user.id),
"role": new_role,
},
format="json",
)
assert response.status_code == 403
assert not models.TemplateAccess.objects.filter(user=other_user).exists()
@pytest.mark.parametrize("via", VIA)
def test_api_template_accesses_create_authenticated_administrator(via, mock_user_teams):
"""
Administrators of a template should be able to create template accesses
except for the "owner" role.
"""
user = factories.UserFactory(with_owned_template=True)
client = APIClient()
client.force_login(user)
template = factories.TemplateFactory()
if via == USER:
factories.UserTemplateAccessFactory(
template=template, user=user, role="administrator"
)
elif via == TEAM:
mock_user_teams.return_value = ["lasuite", "unknown"]
factories.TeamTemplateAccessFactory(
template=template, team="lasuite", role="administrator"
)
other_user = factories.UserFactory()
# It should not be allowed to create an owner access
response = client.post(
f"/api/v1.0/templates/{template.id!s}/accesses/",
{
"user": str(other_user.id),
"role": "owner",
},
format="json",
)
assert response.status_code == 403
assert response.json() == {
"detail": "Only owners of a template can assign other users as owners."
}
# It should be allowed to create a lower access
role = random.choice(
[role[0] for role in models.RoleChoices.choices if role[0] != "owner"]
)
response = client.post(
f"/api/v1.0/templates/{template.id!s}/accesses/",
{
"user": str(other_user.id),
"role": role,
},
format="json",
)
assert response.status_code == 201
assert models.TemplateAccess.objects.filter(user=other_user).count() == 1
new_template_access = models.TemplateAccess.objects.filter(user=other_user).get()
assert response.json() == {
"abilities": new_template_access.get_abilities(user),
"id": str(new_template_access.id),
"team": "",
"role": role,
"user": str(other_user.id),
}
@pytest.mark.parametrize("via", VIA)
def test_api_template_accesses_create_authenticated_owner(via, mock_user_teams):
"""
Owners of a template should be able to create template accesses whatever the role.
"""
user = factories.UserFactory(with_owned_template=True)
client = APIClient()
client.force_login(user)
template = factories.TemplateFactory()
if via == USER:
factories.UserTemplateAccessFactory(template=template, user=user, role="owner")
elif via == TEAM:
mock_user_teams.return_value = ["lasuite", "unknown"]
factories.TeamTemplateAccessFactory(
template=template, team="lasuite", role="owner"
)
other_user = factories.UserFactory()
role = random.choice([role[0] for role in models.RoleChoices.choices])
response = client.post(
f"/api/v1.0/templates/{template.id!s}/accesses/",
{
"user": str(other_user.id),
"role": role,
},
format="json",
)
assert response.status_code == 201
assert models.TemplateAccess.objects.filter(user=other_user).count() == 1
new_template_access = models.TemplateAccess.objects.filter(user=other_user).get()
assert response.json() == {
"id": str(new_template_access.id),
"user": str(other_user.id),
"team": "",
"role": role,
"abilities": new_template_access.get_abilities(user),
}
@@ -42,7 +42,5 @@ def test_api_templates_create_authenticated():
format="json",
)
assert response.status_code == 201
template = Template.objects.get()
assert template.title == "my template"
assert template.accesses.filter(role="owner", user=user).exists()
assert response.status_code == 405
assert not Template.objects.exists()
@@ -8,7 +8,6 @@ import pytest
from rest_framework.test import APIClient
from core import factories, models
from core.tests.conftest import TEAM, USER, VIA
pytestmark = pytest.mark.django_db
@@ -25,7 +24,7 @@ def test_api_templates_delete_anonymous():
assert models.Template.objects.count() == 1
def test_api_templates_delete_authenticated_unrelated():
def test_api_templates_delete_not_implemented():
"""
Authenticated users should not be allowed to delete a template to which they are not
related.
@@ -36,72 +35,11 @@ def test_api_templates_delete_authenticated_unrelated():
client.force_login(user)
is_public = random.choice([True, False])
template = factories.TemplateFactory(is_public=is_public)
template = factories.TemplateFactory(is_public=is_public, users=[(user, "owner")])
response = client.delete(
f"/api/v1.0/templates/{template.id!s}/",
)
assert response.status_code == 403 if is_public else 404
assert response.status_code == 405
assert models.Template.objects.count() == 1
@pytest.mark.parametrize("role", ["reader", "editor", "administrator"])
@pytest.mark.parametrize("via", VIA)
def test_api_templates_delete_authenticated_member_or_administrator(
via, role, mock_user_teams
):
"""
Authenticated users should not be allowed to delete a template for which they are
only a member or administrator.
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
template = factories.TemplateFactory()
if via == USER:
factories.UserTemplateAccessFactory(template=template, user=user, role=role)
elif via == TEAM:
mock_user_teams.return_value = ["lasuite", "unknown"]
factories.TeamTemplateAccessFactory(
template=template, team="lasuite", role=role
)
response = client.delete(
f"/api/v1.0/templates/{template.id}/",
)
assert response.status_code == 403
assert response.json() == {
"detail": "You do not have permission to perform this action."
}
assert models.Template.objects.count() == 1
@pytest.mark.parametrize("via", VIA)
def test_api_templates_delete_authenticated_owner(via, mock_user_teams):
"""
Authenticated users should be able to delete a template they own.
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
template = factories.TemplateFactory()
if via == USER:
factories.UserTemplateAccessFactory(template=template, user=user, role="owner")
elif via == TEAM:
mock_user_teams.return_value = ["lasuite", "unknown"]
factories.TeamTemplateAccessFactory(
template=template, team="lasuite", role="owner"
)
response = client.delete(
f"/api/v1.0/templates/{template.id}/",
)
assert response.status_code == 204
assert models.Template.objects.exists() is False
@@ -2,14 +2,11 @@
Tests for Templates API endpoint in impress's core app: update
"""
import random
import pytest
from rest_framework.test import APIClient
from core import factories
from core.api import serializers
from core.tests.conftest import TEAM, USER, VIA
pytestmark = pytest.mark.django_db
@@ -17,7 +14,6 @@ pytestmark = pytest.mark.django_db
def test_api_templates_update_anonymous():
"""Anonymous users should not be allowed to update a template."""
template = factories.TemplateFactory()
old_template_values = serializers.TemplateSerializer(instance=template).data
new_template_values = serializers.TemplateSerializer(
instance=factories.TemplateFactory()
@@ -28,145 +24,18 @@ def test_api_templates_update_anonymous():
format="json",
)
assert response.status_code == 401
assert response.json() == {
"detail": "Authentication credentials were not provided."
}
template.refresh_from_db()
template_values = serializers.TemplateSerializer(instance=template).data
assert template_values == old_template_values
def test_api_templates_update_authenticated_unrelated():
def test_api_templates_update_not_implemented():
"""
Authenticated users should not be allowed to update a template to which they are not related.
Authenticated users should not be allowed to update a template.
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
template = factories.TemplateFactory(is_public=False)
old_template_values = serializers.TemplateSerializer(instance=template).data
new_template_values = serializers.TemplateSerializer(
instance=factories.TemplateFactory()
).data
response = client.put(
f"/api/v1.0/templates/{template.id!s}/",
new_template_values,
format="json",
)
assert response.status_code == 403
assert response.json() == {
"detail": "You do not have permission to perform this action."
}
template.refresh_from_db()
template_values = serializers.TemplateSerializer(instance=template).data
assert template_values == old_template_values
@pytest.mark.parametrize("via", VIA)
def test_api_templates_update_authenticated_readers(via, mock_user_teams):
"""
Users who are readers of a template should not be allowed to update it.
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
template = factories.TemplateFactory()
if via == USER:
factories.UserTemplateAccessFactory(template=template, user=user, role="reader")
elif via == TEAM:
mock_user_teams.return_value = ["lasuite", "unknown"]
factories.TeamTemplateAccessFactory(
template=template, team="lasuite", role="reader"
)
old_template_values = serializers.TemplateSerializer(instance=template).data
new_template_values = serializers.TemplateSerializer(
instance=factories.TemplateFactory()
).data
response = client.put(
f"/api/v1.0/templates/{template.id!s}/",
new_template_values,
format="json",
)
assert response.status_code == 403
assert response.json() == {
"detail": "You do not have permission to perform this action."
}
template.refresh_from_db()
template_values = serializers.TemplateSerializer(instance=template).data
assert template_values == old_template_values
@pytest.mark.parametrize("role", ["editor", "administrator", "owner"])
@pytest.mark.parametrize("via", VIA)
def test_api_templates_update_authenticated_editor_or_administrator_or_owner(
via, role, mock_user_teams
):
"""Administrator or owner of a template should be allowed to update it."""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
template = factories.TemplateFactory()
if via == USER:
factories.UserTemplateAccessFactory(template=template, user=user, role=role)
elif via == TEAM:
mock_user_teams.return_value = ["lasuite", "unknown"]
factories.TeamTemplateAccessFactory(
template=template, team="lasuite", role=role
)
old_template_values = serializers.TemplateSerializer(instance=template).data
new_template_values = serializers.TemplateSerializer(
instance=factories.TemplateFactory()
).data
response = client.put(
f"/api/v1.0/templates/{template.id!s}/",
new_template_values,
format="json",
)
assert response.status_code == 200
template.refresh_from_db()
template_values = serializers.TemplateSerializer(instance=template).data
for key, value in template_values.items():
if key in ["id", "accesses"]:
assert value == old_template_values[key]
else:
assert value == new_template_values[key]
@pytest.mark.parametrize("via", VIA)
def test_api_templates_update_authenticated_owners(via, mock_user_teams):
"""Administrators of a template should be allowed to update it."""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
template = factories.TemplateFactory()
if via == USER:
factories.UserTemplateAccessFactory(template=template, user=user, role="owner")
elif via == TEAM:
mock_user_teams.return_value = ["lasuite", "unknown"]
factories.TeamTemplateAccessFactory(
template=template, team="lasuite", role="owner"
)
old_template_values = serializers.TemplateSerializer(instance=template).data
template = factories.TemplateFactory(users=[(user, "owner")])
new_template_values = serializers.TemplateSerializer(
instance=factories.TemplateFactory()
@@ -176,55 +45,10 @@ def test_api_templates_update_authenticated_owners(via, mock_user_teams):
f"/api/v1.0/templates/{template.id!s}/", new_template_values, format="json"
)
assert response.status_code == 200
template.refresh_from_db()
template_values = serializers.TemplateSerializer(instance=template).data
for key, value in template_values.items():
if key in ["id", "accesses"]:
assert value == old_template_values[key]
else:
assert value == new_template_values[key]
assert response.status_code == 405
@pytest.mark.parametrize("via", VIA)
def test_api_templates_update_administrator_or_owner_of_another(via, mock_user_teams):
"""
Being administrator or owner of a template should not grant authorization to update
another template.
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
template = factories.TemplateFactory()
if via == USER:
factories.UserTemplateAccessFactory(
template=template, user=user, role=random.choice(["administrator", "owner"])
)
elif via == TEAM:
mock_user_teams.return_value = ["lasuite", "unknown"]
factories.TeamTemplateAccessFactory(
template=template,
team="lasuite",
role=random.choice(["administrator", "owner"]),
)
is_public = random.choice([True, False])
template = factories.TemplateFactory(title="Old title", is_public=is_public)
old_template_values = serializers.TemplateSerializer(instance=template).data
new_template_values = serializers.TemplateSerializer(
instance=factories.TemplateFactory()
).data
response = client.put(
f"/api/v1.0/templates/{template.id!s}/",
new_template_values,
format="json",
response = client.patch(
f"/api/v1.0/templates/{template.id!s}/", new_template_values, format="json"
)
assert response.status_code == 403 if is_public else 404
template.refresh_from_db()
template_values = serializers.TemplateSerializer(instance=template).data
assert template_values == old_template_values
assert response.status_code == 405
-13
View File
@@ -34,15 +34,6 @@ document_related_router.register(
)
# - Routes nested under a template
template_related_router = DefaultRouter()
template_related_router.register(
"accesses",
viewsets.TemplateAccessViewSet,
basename="template_accesses",
)
urlpatterns = [
path(
f"api/{settings.API_VERSION}/",
@@ -54,10 +45,6 @@ urlpatterns = [
r"^documents/(?P<resource_id>[0-9a-z-]*)/",
include(document_related_router.urls),
),
re_path(
r"^templates/(?P<resource_id>[0-9a-z-]*)/",
include(template_related_router.urls),
),
]
),
),
+24 -24
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-10-23 11:01+0000\n"
"PO-Revision-Date: 2025-11-10 09:54\n"
"POT-Creation-Date: 2025-11-17 08:04+0000\n"
"PO-Revision-Date: 2025-11-19 10:13\n"
"Last-Translator: \n"
"Language-Team: Breton\n"
"Language: br_FR\n"
@@ -234,8 +234,8 @@ msgstr "implijer"
msgid "users"
msgstr "implijerien"
#: build/lib/core/models.py:361 build/lib/core/models.py:1284
#: core/models.py:361 core/models.py:1284
#: build/lib/core/models.py:361 build/lib/core/models.py:1276
#: core/models.py:361 core/models.py:1276
msgid "title"
msgstr "titl"
@@ -311,8 +311,8 @@ msgstr "An implijer-mañ a zo dija er restr-mañ."
msgid "This team is already in this document."
msgstr "Ar skipailh-mañ a zo dija en restr-mañ."
#: build/lib/core/models.py:1045 build/lib/core/models.py:1370
#: core/models.py:1045 core/models.py:1370
#: build/lib/core/models.py:1045 build/lib/core/models.py:1362
#: core/models.py:1045 core/models.py:1362
msgid "Either user or team must be set, not both."
msgstr "An implijer pe ar skipailh a rank bezañ termenet, ket an daou avat."
@@ -328,78 +328,78 @@ msgstr "Goulennoù tizhout ar restr"
msgid "This user has already asked for access to this document."
msgstr "An implijer en deus goulennet tizhout ar restr-mañ."
#: build/lib/core/models.py:1263 core/models.py:1263
#: build/lib/core/models.py:1255 core/models.py:1255
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr "{name} en defe c'hoant da dizhout ar restr-mañ!"
#: build/lib/core/models.py:1267 core/models.py:1267
#: build/lib/core/models.py:1259 core/models.py:1259
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr "{name} en defe c'hoant da dizhout ar restr da-heul:"
#: build/lib/core/models.py:1273 core/models.py:1273
#: build/lib/core/models.py:1265 core/models.py:1265
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr "{name} en defe c'hoant da dizhout ar restr: {title}"
#: build/lib/core/models.py:1285 core/models.py:1285
#: build/lib/core/models.py:1277 core/models.py:1277
msgid "description"
msgstr "deskrivadur"
#: build/lib/core/models.py:1286 core/models.py:1286
#: build/lib/core/models.py:1278 core/models.py:1278
msgid "code"
msgstr "kod"
#: build/lib/core/models.py:1287 core/models.py:1287
#: build/lib/core/models.py:1279 core/models.py:1279
msgid "css"
msgstr "css"
#: build/lib/core/models.py:1289 core/models.py:1289
#: build/lib/core/models.py:1281 core/models.py:1281
msgid "public"
msgstr "publik"
#: build/lib/core/models.py:1291 core/models.py:1291
#: build/lib/core/models.py:1283 core/models.py:1283
msgid "Whether this template is public for anyone to use."
msgstr "M'eo foran ar patrom-mañ hag implijus gant n'eus forzh piv."
#: build/lib/core/models.py:1297 core/models.py:1297
#: build/lib/core/models.py:1289 core/models.py:1289
msgid "Template"
msgstr "Patrom"
#: build/lib/core/models.py:1298 core/models.py:1298
#: build/lib/core/models.py:1290 core/models.py:1290
msgid "Templates"
msgstr "Patromoù"
#: build/lib/core/models.py:1351 core/models.py:1351
#: build/lib/core/models.py:1343 core/models.py:1343
msgid "Template/user relation"
msgstr "Liamm patrom/implijer"
#: build/lib/core/models.py:1352 core/models.py:1352
#: build/lib/core/models.py:1344 core/models.py:1344
msgid "Template/user relations"
msgstr "Liammoù patrom/implijer"
#: build/lib/core/models.py:1358 core/models.py:1358
#: build/lib/core/models.py:1350 core/models.py:1350
msgid "This user is already in this template."
msgstr "An implijer-mañ a zo dija er patrom-mañ."
#: build/lib/core/models.py:1364 core/models.py:1364
#: build/lib/core/models.py:1356 core/models.py:1356
msgid "This team is already in this template."
msgstr "Ar skipailh-mañ a zo dija er patrom-mañ."
#: build/lib/core/models.py:1441 core/models.py:1441
#: build/lib/core/models.py:1433 core/models.py:1433
msgid "email address"
msgstr "postel"
#: build/lib/core/models.py:1460 core/models.py:1460
#: build/lib/core/models.py:1452 core/models.py:1452
msgid "Document invitation"
msgstr "Pedadenn d'ur restr"
#: build/lib/core/models.py:1461 core/models.py:1461
#: build/lib/core/models.py:1453 core/models.py:1453
msgid "Document invitations"
msgstr "Pedadennoù d'ur restr"
#: build/lib/core/models.py:1481 core/models.py:1481
#: build/lib/core/models.py:1473 core/models.py:1473
msgid "This email is already associated to a registered user."
msgstr "Ar postel-mañ a zo liammet ouzh un implijer enskrivet."
+24 -24
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-10-23 11:01+0000\n"
"PO-Revision-Date: 2025-11-10 09:54\n"
"POT-Creation-Date: 2025-11-17 08:04+0000\n"
"PO-Revision-Date: 2025-11-19 10:13\n"
"Last-Translator: \n"
"Language-Team: German\n"
"Language: de_DE\n"
@@ -234,8 +234,8 @@ msgstr "Benutzer"
msgid "users"
msgstr "Benutzer"
#: build/lib/core/models.py:361 build/lib/core/models.py:1284
#: core/models.py:361 core/models.py:1284
#: build/lib/core/models.py:361 build/lib/core/models.py:1276
#: core/models.py:361 core/models.py:1276
msgid "title"
msgstr "Titel"
@@ -311,8 +311,8 @@ msgstr "Dieser Benutzer befindet sich bereits in diesem Dokument."
msgid "This team is already in this document."
msgstr "Dieses Team befindet sich bereits in diesem Dokument."
#: build/lib/core/models.py:1045 build/lib/core/models.py:1370
#: core/models.py:1045 core/models.py:1370
#: build/lib/core/models.py:1045 build/lib/core/models.py:1362
#: core/models.py:1045 core/models.py:1362
msgid "Either user or team must be set, not both."
msgstr "Benutzer oder Team müssen gesetzt werden, nicht beides."
@@ -328,78 +328,78 @@ msgstr ""
msgid "This user has already asked for access to this document."
msgstr ""
#: build/lib/core/models.py:1263 core/models.py:1263
#: build/lib/core/models.py:1255 core/models.py:1255
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr ""
#: build/lib/core/models.py:1267 core/models.py:1267
#: build/lib/core/models.py:1259 core/models.py:1259
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr ""
#: build/lib/core/models.py:1273 core/models.py:1273
#: build/lib/core/models.py:1265 core/models.py:1265
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr ""
#: build/lib/core/models.py:1285 core/models.py:1285
#: build/lib/core/models.py:1277 core/models.py:1277
msgid "description"
msgstr "Beschreibung"
#: build/lib/core/models.py:1286 core/models.py:1286
#: build/lib/core/models.py:1278 core/models.py:1278
msgid "code"
msgstr "Code"
#: build/lib/core/models.py:1287 core/models.py:1287
#: build/lib/core/models.py:1279 core/models.py:1279
msgid "css"
msgstr "CSS"
#: build/lib/core/models.py:1289 core/models.py:1289
#: build/lib/core/models.py:1281 core/models.py:1281
msgid "public"
msgstr "öffentlich"
#: build/lib/core/models.py:1291 core/models.py:1291
#: build/lib/core/models.py:1283 core/models.py:1283
msgid "Whether this template is public for anyone to use."
msgstr "Ob diese Vorlage für jedermann öffentlich ist."
#: build/lib/core/models.py:1297 core/models.py:1297
#: build/lib/core/models.py:1289 core/models.py:1289
msgid "Template"
msgstr "Vorlage"
#: build/lib/core/models.py:1298 core/models.py:1298
#: build/lib/core/models.py:1290 core/models.py:1290
msgid "Templates"
msgstr "Vorlagen"
#: build/lib/core/models.py:1351 core/models.py:1351
#: build/lib/core/models.py:1343 core/models.py:1343
msgid "Template/user relation"
msgstr "Vorlage/Benutzer-Beziehung"
#: build/lib/core/models.py:1352 core/models.py:1352
#: build/lib/core/models.py:1344 core/models.py:1344
msgid "Template/user relations"
msgstr "Vorlage/Benutzerbeziehungen"
#: build/lib/core/models.py:1358 core/models.py:1358
#: build/lib/core/models.py:1350 core/models.py:1350
msgid "This user is already in this template."
msgstr "Dieser Benutzer ist bereits in dieser Vorlage."
#: build/lib/core/models.py:1364 core/models.py:1364
#: build/lib/core/models.py:1356 core/models.py:1356
msgid "This team is already in this template."
msgstr "Dieses Team ist bereits in diesem Template."
#: build/lib/core/models.py:1441 core/models.py:1441
#: build/lib/core/models.py:1433 core/models.py:1433
msgid "email address"
msgstr "E-Mail-Adresse"
#: build/lib/core/models.py:1460 core/models.py:1460
#: build/lib/core/models.py:1452 core/models.py:1452
msgid "Document invitation"
msgstr "Einladung zum Dokument"
#: build/lib/core/models.py:1461 core/models.py:1461
#: build/lib/core/models.py:1453 core/models.py:1453
msgid "Document invitations"
msgstr "Dokumenteinladungen"
#: build/lib/core/models.py:1481 core/models.py:1481
#: build/lib/core/models.py:1473 core/models.py:1473
msgid "This email is already associated to a registered user."
msgstr "Diese E-Mail ist bereits einem registrierten Benutzer zugeordnet."
+24 -24
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-10-23 11:01+0000\n"
"PO-Revision-Date: 2025-11-10 09:54\n"
"POT-Creation-Date: 2025-11-17 08:04+0000\n"
"PO-Revision-Date: 2025-11-19 10:13\n"
"Last-Translator: \n"
"Language-Team: English\n"
"Language: en_US\n"
@@ -234,8 +234,8 @@ msgstr ""
msgid "users"
msgstr ""
#: build/lib/core/models.py:361 build/lib/core/models.py:1284
#: core/models.py:361 core/models.py:1284
#: build/lib/core/models.py:361 build/lib/core/models.py:1276
#: core/models.py:361 core/models.py:1276
msgid "title"
msgstr ""
@@ -311,8 +311,8 @@ msgstr ""
msgid "This team is already in this document."
msgstr ""
#: build/lib/core/models.py:1045 build/lib/core/models.py:1370
#: core/models.py:1045 core/models.py:1370
#: build/lib/core/models.py:1045 build/lib/core/models.py:1362
#: core/models.py:1045 core/models.py:1362
msgid "Either user or team must be set, not both."
msgstr ""
@@ -328,78 +328,78 @@ msgstr ""
msgid "This user has already asked for access to this document."
msgstr ""
#: build/lib/core/models.py:1263 core/models.py:1263
#: build/lib/core/models.py:1255 core/models.py:1255
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr ""
#: build/lib/core/models.py:1267 core/models.py:1267
#: build/lib/core/models.py:1259 core/models.py:1259
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr ""
#: build/lib/core/models.py:1273 core/models.py:1273
#: build/lib/core/models.py:1265 core/models.py:1265
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr ""
#: build/lib/core/models.py:1285 core/models.py:1285
#: build/lib/core/models.py:1277 core/models.py:1277
msgid "description"
msgstr ""
#: build/lib/core/models.py:1286 core/models.py:1286
#: build/lib/core/models.py:1278 core/models.py:1278
msgid "code"
msgstr ""
#: build/lib/core/models.py:1287 core/models.py:1287
#: build/lib/core/models.py:1279 core/models.py:1279
msgid "css"
msgstr ""
#: build/lib/core/models.py:1289 core/models.py:1289
#: build/lib/core/models.py:1281 core/models.py:1281
msgid "public"
msgstr ""
#: build/lib/core/models.py:1291 core/models.py:1291
#: build/lib/core/models.py:1283 core/models.py:1283
msgid "Whether this template is public for anyone to use."
msgstr ""
#: build/lib/core/models.py:1297 core/models.py:1297
#: build/lib/core/models.py:1289 core/models.py:1289
msgid "Template"
msgstr ""
#: build/lib/core/models.py:1298 core/models.py:1298
#: build/lib/core/models.py:1290 core/models.py:1290
msgid "Templates"
msgstr ""
#: build/lib/core/models.py:1351 core/models.py:1351
#: build/lib/core/models.py:1343 core/models.py:1343
msgid "Template/user relation"
msgstr ""
#: build/lib/core/models.py:1352 core/models.py:1352
#: build/lib/core/models.py:1344 core/models.py:1344
msgid "Template/user relations"
msgstr ""
#: build/lib/core/models.py:1358 core/models.py:1358
#: build/lib/core/models.py:1350 core/models.py:1350
msgid "This user is already in this template."
msgstr ""
#: build/lib/core/models.py:1364 core/models.py:1364
#: build/lib/core/models.py:1356 core/models.py:1356
msgid "This team is already in this template."
msgstr ""
#: build/lib/core/models.py:1441 core/models.py:1441
#: build/lib/core/models.py:1433 core/models.py:1433
msgid "email address"
msgstr ""
#: build/lib/core/models.py:1460 core/models.py:1460
#: build/lib/core/models.py:1452 core/models.py:1452
msgid "Document invitation"
msgstr ""
#: build/lib/core/models.py:1461 core/models.py:1461
#: build/lib/core/models.py:1453 core/models.py:1453
msgid "Document invitations"
msgstr ""
#: build/lib/core/models.py:1481 core/models.py:1481
#: build/lib/core/models.py:1473 core/models.py:1473
msgid "This email is already associated to a registered user."
msgstr ""
+24 -24
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-10-23 11:01+0000\n"
"PO-Revision-Date: 2025-11-10 09:54\n"
"POT-Creation-Date: 2025-11-17 08:04+0000\n"
"PO-Revision-Date: 2025-11-19 10:13\n"
"Last-Translator: \n"
"Language-Team: Spanish\n"
"Language: es_ES\n"
@@ -234,8 +234,8 @@ msgstr "usuario"
msgid "users"
msgstr "usuarios"
#: build/lib/core/models.py:361 build/lib/core/models.py:1284
#: core/models.py:361 core/models.py:1284
#: build/lib/core/models.py:361 build/lib/core/models.py:1276
#: core/models.py:361 core/models.py:1276
msgid "title"
msgstr "título"
@@ -311,8 +311,8 @@ msgstr "Este usuario ya forma parte del documento."
msgid "This team is already in this document."
msgstr "Este equipo ya forma parte del documento."
#: build/lib/core/models.py:1045 build/lib/core/models.py:1370
#: core/models.py:1045 core/models.py:1370
#: build/lib/core/models.py:1045 build/lib/core/models.py:1362
#: core/models.py:1045 core/models.py:1362
msgid "Either user or team must be set, not both."
msgstr "Debe establecerse un usuario o un equipo, no ambos."
@@ -328,78 +328,78 @@ msgstr "Solicitud de accesos"
msgid "This user has already asked for access to this document."
msgstr "Este usuario ya ha solicitado acceso a este documento."
#: build/lib/core/models.py:1263 core/models.py:1263
#: build/lib/core/models.py:1255 core/models.py:1255
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr "¡{name} desea acceder a un documento!"
#: build/lib/core/models.py:1267 core/models.py:1267
#: build/lib/core/models.py:1259 core/models.py:1259
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr "{name} desea acceso al siguiente documento:"
#: build/lib/core/models.py:1273 core/models.py:1273
#: build/lib/core/models.py:1265 core/models.py:1265
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr "{name} está pidiendo acceso al documento: {title}"
#: build/lib/core/models.py:1285 core/models.py:1285
#: build/lib/core/models.py:1277 core/models.py:1277
msgid "description"
msgstr "descripción"
#: build/lib/core/models.py:1286 core/models.py:1286
#: build/lib/core/models.py:1278 core/models.py:1278
msgid "code"
msgstr "código"
#: build/lib/core/models.py:1287 core/models.py:1287
#: build/lib/core/models.py:1279 core/models.py:1279
msgid "css"
msgstr "css"
#: build/lib/core/models.py:1289 core/models.py:1289
#: build/lib/core/models.py:1281 core/models.py:1281
msgid "public"
msgstr "público"
#: build/lib/core/models.py:1291 core/models.py:1291
#: build/lib/core/models.py:1283 core/models.py:1283
msgid "Whether this template is public for anyone to use."
msgstr "Si esta plantilla es pública para que cualquiera la utilice."
#: build/lib/core/models.py:1297 core/models.py:1297
#: build/lib/core/models.py:1289 core/models.py:1289
msgid "Template"
msgstr "Plantilla"
#: build/lib/core/models.py:1298 core/models.py:1298
#: build/lib/core/models.py:1290 core/models.py:1290
msgid "Templates"
msgstr "Plantillas"
#: build/lib/core/models.py:1351 core/models.py:1351
#: build/lib/core/models.py:1343 core/models.py:1343
msgid "Template/user relation"
msgstr "Relación plantilla/usuario"
#: build/lib/core/models.py:1352 core/models.py:1352
#: build/lib/core/models.py:1344 core/models.py:1344
msgid "Template/user relations"
msgstr "Relaciones plantilla/usuario"
#: build/lib/core/models.py:1358 core/models.py:1358
#: build/lib/core/models.py:1350 core/models.py:1350
msgid "This user is already in this template."
msgstr "Este usuario ya forma parte de la plantilla."
#: build/lib/core/models.py:1364 core/models.py:1364
#: build/lib/core/models.py:1356 core/models.py:1356
msgid "This team is already in this template."
msgstr "Este equipo ya se encuentra en esta plantilla."
#: build/lib/core/models.py:1441 core/models.py:1441
#: build/lib/core/models.py:1433 core/models.py:1433
msgid "email address"
msgstr "dirección de correo electrónico"
#: build/lib/core/models.py:1460 core/models.py:1460
#: build/lib/core/models.py:1452 core/models.py:1452
msgid "Document invitation"
msgstr "Invitación al documento"
#: build/lib/core/models.py:1461 core/models.py:1461
#: build/lib/core/models.py:1453 core/models.py:1453
msgid "Document invitations"
msgstr "Invitaciones a documentos"
#: build/lib/core/models.py:1481 core/models.py:1481
#: build/lib/core/models.py:1473 core/models.py:1473
msgid "This email is already associated to a registered user."
msgstr "Este correo electrónico está asociado a un usuario registrado."
+24 -24
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-10-23 11:01+0000\n"
"PO-Revision-Date: 2025-11-10 09:54\n"
"POT-Creation-Date: 2025-11-17 08:04+0000\n"
"PO-Revision-Date: 2025-11-19 10:13\n"
"Last-Translator: \n"
"Language-Team: French\n"
"Language: fr_FR\n"
@@ -234,8 +234,8 @@ msgstr "utilisateur"
msgid "users"
msgstr "utilisateurs"
#: build/lib/core/models.py:361 build/lib/core/models.py:1284
#: core/models.py:361 core/models.py:1284
#: build/lib/core/models.py:361 build/lib/core/models.py:1276
#: core/models.py:361 core/models.py:1276
msgid "title"
msgstr "titre"
@@ -311,8 +311,8 @@ msgstr "Cet utilisateur est déjà dans ce document."
msgid "This team is already in this document."
msgstr "Cette équipe est déjà dans ce document."
#: build/lib/core/models.py:1045 build/lib/core/models.py:1370
#: core/models.py:1045 core/models.py:1370
#: build/lib/core/models.py:1045 build/lib/core/models.py:1362
#: core/models.py:1045 core/models.py:1362
msgid "Either user or team must be set, not both."
msgstr "L'utilisateur ou l'équipe doivent être définis, pas les deux."
@@ -328,78 +328,78 @@ msgstr "Demande d'accès au document"
msgid "This user has already asked for access to this document."
msgstr "Cet utilisateur a déjà demandé l'accès à ce document."
#: build/lib/core/models.py:1263 core/models.py:1263
#: build/lib/core/models.py:1255 core/models.py:1255
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr "{name} souhaiterait accéder au document suivant !"
#: build/lib/core/models.py:1267 core/models.py:1267
#: build/lib/core/models.py:1259 core/models.py:1259
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr "{name} souhaiterait accéder au document suivant :"
#: build/lib/core/models.py:1273 core/models.py:1273
#: build/lib/core/models.py:1265 core/models.py:1265
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr "{name} demande l'accès au document : {title}"
#: build/lib/core/models.py:1285 core/models.py:1285
#: build/lib/core/models.py:1277 core/models.py:1277
msgid "description"
msgstr "description"
#: build/lib/core/models.py:1286 core/models.py:1286
#: build/lib/core/models.py:1278 core/models.py:1278
msgid "code"
msgstr "code"
#: build/lib/core/models.py:1287 core/models.py:1287
#: build/lib/core/models.py:1279 core/models.py:1279
msgid "css"
msgstr "CSS"
#: build/lib/core/models.py:1289 core/models.py:1289
#: build/lib/core/models.py:1281 core/models.py:1281
msgid "public"
msgstr "public"
#: build/lib/core/models.py:1291 core/models.py:1291
#: build/lib/core/models.py:1283 core/models.py:1283
msgid "Whether this template is public for anyone to use."
msgstr "Si ce modèle est public, utilisable par n'importe qui."
#: build/lib/core/models.py:1297 core/models.py:1297
#: build/lib/core/models.py:1289 core/models.py:1289
msgid "Template"
msgstr "Modèle"
#: build/lib/core/models.py:1298 core/models.py:1298
#: build/lib/core/models.py:1290 core/models.py:1290
msgid "Templates"
msgstr "Modèles"
#: build/lib/core/models.py:1351 core/models.py:1351
#: build/lib/core/models.py:1343 core/models.py:1343
msgid "Template/user relation"
msgstr "Relation modèle/utilisateur"
#: build/lib/core/models.py:1352 core/models.py:1352
#: build/lib/core/models.py:1344 core/models.py:1344
msgid "Template/user relations"
msgstr "Relations modèle/utilisateur"
#: build/lib/core/models.py:1358 core/models.py:1358
#: build/lib/core/models.py:1350 core/models.py:1350
msgid "This user is already in this template."
msgstr "Cet utilisateur est déjà dans ce modèle."
#: build/lib/core/models.py:1364 core/models.py:1364
#: build/lib/core/models.py:1356 core/models.py:1356
msgid "This team is already in this template."
msgstr "Cette équipe est déjà modèle."
#: build/lib/core/models.py:1441 core/models.py:1441
#: build/lib/core/models.py:1433 core/models.py:1433
msgid "email address"
msgstr "adresse e-mail"
#: build/lib/core/models.py:1460 core/models.py:1460
#: build/lib/core/models.py:1452 core/models.py:1452
msgid "Document invitation"
msgstr "Invitation à un document"
#: build/lib/core/models.py:1461 core/models.py:1461
#: build/lib/core/models.py:1453 core/models.py:1453
msgid "Document invitations"
msgstr "Invitations à un document"
#: build/lib/core/models.py:1481 core/models.py:1481
#: build/lib/core/models.py:1473 core/models.py:1473
msgid "This email is already associated to a registered user."
msgstr "Cette adresse email est déjà associée à un utilisateur inscrit."
+24 -24
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-10-23 11:01+0000\n"
"PO-Revision-Date: 2025-11-10 09:54\n"
"POT-Creation-Date: 2025-11-17 08:04+0000\n"
"PO-Revision-Date: 2025-11-19 10:13\n"
"Last-Translator: \n"
"Language-Team: Italian\n"
"Language: it_IT\n"
@@ -234,8 +234,8 @@ msgstr "utente"
msgid "users"
msgstr "utenti"
#: build/lib/core/models.py:361 build/lib/core/models.py:1284
#: core/models.py:361 core/models.py:1284
#: build/lib/core/models.py:361 build/lib/core/models.py:1276
#: core/models.py:361 core/models.py:1276
msgid "title"
msgstr "titolo"
@@ -311,8 +311,8 @@ msgstr "Questo utente è già presente in questo documento."
msgid "This team is already in this document."
msgstr "Questo team è già presente in questo documento."
#: build/lib/core/models.py:1045 build/lib/core/models.py:1370
#: core/models.py:1045 core/models.py:1370
#: build/lib/core/models.py:1045 build/lib/core/models.py:1362
#: core/models.py:1045 core/models.py:1362
msgid "Either user or team must be set, not both."
msgstr ""
@@ -328,78 +328,78 @@ msgstr ""
msgid "This user has already asked for access to this document."
msgstr ""
#: build/lib/core/models.py:1263 core/models.py:1263
#: build/lib/core/models.py:1255 core/models.py:1255
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr ""
#: build/lib/core/models.py:1267 core/models.py:1267
#: build/lib/core/models.py:1259 core/models.py:1259
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr ""
#: build/lib/core/models.py:1273 core/models.py:1273
#: build/lib/core/models.py:1265 core/models.py:1265
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr ""
#: build/lib/core/models.py:1285 core/models.py:1285
#: build/lib/core/models.py:1277 core/models.py:1277
msgid "description"
msgstr "descrizione"
#: build/lib/core/models.py:1286 core/models.py:1286
#: build/lib/core/models.py:1278 core/models.py:1278
msgid "code"
msgstr "code"
#: build/lib/core/models.py:1287 core/models.py:1287
#: build/lib/core/models.py:1279 core/models.py:1279
msgid "css"
msgstr "css"
#: build/lib/core/models.py:1289 core/models.py:1289
#: build/lib/core/models.py:1281 core/models.py:1281
msgid "public"
msgstr "pubblico"
#: build/lib/core/models.py:1291 core/models.py:1291
#: build/lib/core/models.py:1283 core/models.py:1283
msgid "Whether this template is public for anyone to use."
msgstr "Indica se questo modello è pubblico per chiunque."
#: build/lib/core/models.py:1297 core/models.py:1297
#: build/lib/core/models.py:1289 core/models.py:1289
msgid "Template"
msgstr "Modello"
#: build/lib/core/models.py:1298 core/models.py:1298
#: build/lib/core/models.py:1290 core/models.py:1290
msgid "Templates"
msgstr "Modelli"
#: build/lib/core/models.py:1351 core/models.py:1351
#: build/lib/core/models.py:1343 core/models.py:1343
msgid "Template/user relation"
msgstr ""
#: build/lib/core/models.py:1352 core/models.py:1352
#: build/lib/core/models.py:1344 core/models.py:1344
msgid "Template/user relations"
msgstr ""
#: build/lib/core/models.py:1358 core/models.py:1358
#: build/lib/core/models.py:1350 core/models.py:1350
msgid "This user is already in this template."
msgstr "Questo utente è già in questo modello."
#: build/lib/core/models.py:1364 core/models.py:1364
#: build/lib/core/models.py:1356 core/models.py:1356
msgid "This team is already in this template."
msgstr "Questo team è già in questo modello."
#: build/lib/core/models.py:1441 core/models.py:1441
#: build/lib/core/models.py:1433 core/models.py:1433
msgid "email address"
msgstr "indirizzo e-mail"
#: build/lib/core/models.py:1460 core/models.py:1460
#: build/lib/core/models.py:1452 core/models.py:1452
msgid "Document invitation"
msgstr "Invito al documento"
#: build/lib/core/models.py:1461 core/models.py:1461
#: build/lib/core/models.py:1453 core/models.py:1453
msgid "Document invitations"
msgstr "Inviti al documento"
#: build/lib/core/models.py:1481 core/models.py:1481
#: build/lib/core/models.py:1473 core/models.py:1473
msgid "This email is already associated to a registered user."
msgstr "Questa email è già associata a un utente registrato."
+24 -24
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-10-23 11:01+0000\n"
"PO-Revision-Date: 2025-11-10 09:54\n"
"POT-Creation-Date: 2025-11-17 08:04+0000\n"
"PO-Revision-Date: 2025-11-19 10:13\n"
"Last-Translator: \n"
"Language-Team: Dutch\n"
"Language: nl_NL\n"
@@ -234,8 +234,8 @@ msgstr "gebruiker"
msgid "users"
msgstr "gebruikers"
#: build/lib/core/models.py:361 build/lib/core/models.py:1284
#: core/models.py:361 core/models.py:1284
#: build/lib/core/models.py:361 build/lib/core/models.py:1276
#: core/models.py:361 core/models.py:1276
msgid "title"
msgstr "titel"
@@ -311,8 +311,8 @@ msgstr "De gebruiker bestaat al in dit document."
msgid "This team is already in this document."
msgstr "Dit team bestaat al in dit document."
#: build/lib/core/models.py:1045 build/lib/core/models.py:1370
#: core/models.py:1045 core/models.py:1370
#: build/lib/core/models.py:1045 build/lib/core/models.py:1362
#: core/models.py:1045 core/models.py:1362
msgid "Either user or team must be set, not both."
msgstr "Een gebruiker of team moet gekozen worden, maar niet beide."
@@ -328,78 +328,78 @@ msgstr "Document verzoekt om toegangen"
msgid "This user has already asked for access to this document."
msgstr "Deze gebruiker heeft al om toegang tot dit document gevraagd."
#: build/lib/core/models.py:1263 core/models.py:1263
#: build/lib/core/models.py:1255 core/models.py:1255
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr "{name} verzoekt toegang tot een document!"
#: build/lib/core/models.py:1267 core/models.py:1267
#: build/lib/core/models.py:1259 core/models.py:1259
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr "{name} verzoekt toegang tot het volgende document:"
#: build/lib/core/models.py:1273 core/models.py:1273
#: build/lib/core/models.py:1265 core/models.py:1265
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr "{name} verzoekt toegang tot het document: {title}"
#: build/lib/core/models.py:1285 core/models.py:1285
#: build/lib/core/models.py:1277 core/models.py:1277
msgid "description"
msgstr "omschrijving"
#: build/lib/core/models.py:1286 core/models.py:1286
#: build/lib/core/models.py:1278 core/models.py:1278
msgid "code"
msgstr "code"
#: build/lib/core/models.py:1287 core/models.py:1287
#: build/lib/core/models.py:1279 core/models.py:1279
msgid "css"
msgstr "css"
#: build/lib/core/models.py:1289 core/models.py:1289
#: build/lib/core/models.py:1281 core/models.py:1281
msgid "public"
msgstr "publiek"
#: build/lib/core/models.py:1291 core/models.py:1291
#: build/lib/core/models.py:1283 core/models.py:1283
msgid "Whether this template is public for anyone to use."
msgstr "Of dit sjabloon door iedereen publiekelijk te gebruiken is."
#: build/lib/core/models.py:1297 core/models.py:1297
#: build/lib/core/models.py:1289 core/models.py:1289
msgid "Template"
msgstr "Sjabloon"
#: build/lib/core/models.py:1298 core/models.py:1298
#: build/lib/core/models.py:1290 core/models.py:1290
msgid "Templates"
msgstr "Sjabloon"
#: build/lib/core/models.py:1351 core/models.py:1351
#: build/lib/core/models.py:1343 core/models.py:1343
msgid "Template/user relation"
msgstr "Sjabloon/gebruiker relatie"
#: build/lib/core/models.py:1352 core/models.py:1352
#: build/lib/core/models.py:1344 core/models.py:1344
msgid "Template/user relations"
msgstr "Sjabloon/gebruiker relaties"
#: build/lib/core/models.py:1358 core/models.py:1358
#: build/lib/core/models.py:1350 core/models.py:1350
msgid "This user is already in this template."
msgstr "De gebruiker bestaat al in dit sjabloon."
#: build/lib/core/models.py:1364 core/models.py:1364
#: build/lib/core/models.py:1356 core/models.py:1356
msgid "This team is already in this template."
msgstr "Het team bestaat al in dit sjabloon."
#: build/lib/core/models.py:1441 core/models.py:1441
#: build/lib/core/models.py:1433 core/models.py:1433
msgid "email address"
msgstr "e-mailadres"
#: build/lib/core/models.py:1460 core/models.py:1460
#: build/lib/core/models.py:1452 core/models.py:1452
msgid "Document invitation"
msgstr "Document uitnodiging"
#: build/lib/core/models.py:1461 core/models.py:1461
#: build/lib/core/models.py:1453 core/models.py:1453
msgid "Document invitations"
msgstr "Document uitnodigingen"
#: build/lib/core/models.py:1481 core/models.py:1481
#: build/lib/core/models.py:1473 core/models.py:1473
msgid "This email is already associated to a registered user."
msgstr "Deze email is al geassocieerd met een geregistreerde gebruiker."
+24 -24
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-10-23 11:01+0000\n"
"PO-Revision-Date: 2025-11-10 09:54\n"
"POT-Creation-Date: 2025-11-17 08:04+0000\n"
"PO-Revision-Date: 2025-11-19 10:13\n"
"Last-Translator: \n"
"Language-Team: Portuguese\n"
"Language: pt_PT\n"
@@ -234,8 +234,8 @@ msgstr ""
msgid "users"
msgstr ""
#: build/lib/core/models.py:361 build/lib/core/models.py:1284
#: core/models.py:361 core/models.py:1284
#: build/lib/core/models.py:361 build/lib/core/models.py:1276
#: core/models.py:361 core/models.py:1276
msgid "title"
msgstr ""
@@ -311,8 +311,8 @@ msgstr ""
msgid "This team is already in this document."
msgstr ""
#: build/lib/core/models.py:1045 build/lib/core/models.py:1370
#: core/models.py:1045 core/models.py:1370
#: build/lib/core/models.py:1045 build/lib/core/models.py:1362
#: core/models.py:1045 core/models.py:1362
msgid "Either user or team must be set, not both."
msgstr ""
@@ -328,78 +328,78 @@ msgstr ""
msgid "This user has already asked for access to this document."
msgstr ""
#: build/lib/core/models.py:1263 core/models.py:1263
#: build/lib/core/models.py:1255 core/models.py:1255
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr ""
#: build/lib/core/models.py:1267 core/models.py:1267
#: build/lib/core/models.py:1259 core/models.py:1259
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr ""
#: build/lib/core/models.py:1273 core/models.py:1273
#: build/lib/core/models.py:1265 core/models.py:1265
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr ""
#: build/lib/core/models.py:1285 core/models.py:1285
#: build/lib/core/models.py:1277 core/models.py:1277
msgid "description"
msgstr ""
#: build/lib/core/models.py:1286 core/models.py:1286
#: build/lib/core/models.py:1278 core/models.py:1278
msgid "code"
msgstr ""
#: build/lib/core/models.py:1287 core/models.py:1287
#: build/lib/core/models.py:1279 core/models.py:1279
msgid "css"
msgstr ""
#: build/lib/core/models.py:1289 core/models.py:1289
#: build/lib/core/models.py:1281 core/models.py:1281
msgid "public"
msgstr ""
#: build/lib/core/models.py:1291 core/models.py:1291
#: build/lib/core/models.py:1283 core/models.py:1283
msgid "Whether this template is public for anyone to use."
msgstr ""
#: build/lib/core/models.py:1297 core/models.py:1297
#: build/lib/core/models.py:1289 core/models.py:1289
msgid "Template"
msgstr ""
#: build/lib/core/models.py:1298 core/models.py:1298
#: build/lib/core/models.py:1290 core/models.py:1290
msgid "Templates"
msgstr ""
#: build/lib/core/models.py:1351 core/models.py:1351
#: build/lib/core/models.py:1343 core/models.py:1343
msgid "Template/user relation"
msgstr ""
#: build/lib/core/models.py:1352 core/models.py:1352
#: build/lib/core/models.py:1344 core/models.py:1344
msgid "Template/user relations"
msgstr ""
#: build/lib/core/models.py:1358 core/models.py:1358
#: build/lib/core/models.py:1350 core/models.py:1350
msgid "This user is already in this template."
msgstr ""
#: build/lib/core/models.py:1364 core/models.py:1364
#: build/lib/core/models.py:1356 core/models.py:1356
msgid "This team is already in this template."
msgstr ""
#: build/lib/core/models.py:1441 core/models.py:1441
#: build/lib/core/models.py:1433 core/models.py:1433
msgid "email address"
msgstr ""
#: build/lib/core/models.py:1460 core/models.py:1460
#: build/lib/core/models.py:1452 core/models.py:1452
msgid "Document invitation"
msgstr ""
#: build/lib/core/models.py:1461 core/models.py:1461
#: build/lib/core/models.py:1453 core/models.py:1453
msgid "Document invitations"
msgstr ""
#: build/lib/core/models.py:1481 core/models.py:1481
#: build/lib/core/models.py:1473 core/models.py:1473
msgid "This email is already associated to a registered user."
msgstr ""
+24 -24
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-10-23 11:01+0000\n"
"PO-Revision-Date: 2025-11-10 09:54\n"
"POT-Creation-Date: 2025-11-17 08:04+0000\n"
"PO-Revision-Date: 2025-11-19 10:13\n"
"Last-Translator: \n"
"Language-Team: Russian\n"
"Language: ru_RU\n"
@@ -234,8 +234,8 @@ msgstr "пользователь"
msgid "users"
msgstr "пользователи"
#: build/lib/core/models.py:361 build/lib/core/models.py:1284
#: core/models.py:361 core/models.py:1284
#: build/lib/core/models.py:361 build/lib/core/models.py:1276
#: core/models.py:361 core/models.py:1276
msgid "title"
msgstr "заголовок"
@@ -311,8 +311,8 @@ msgstr "Этот пользователь уже имеет доступ к эт
msgid "This team is already in this document."
msgstr "Эта команда уже имеет доступ к этому документу."
#: build/lib/core/models.py:1045 build/lib/core/models.py:1370
#: core/models.py:1045 core/models.py:1370
#: build/lib/core/models.py:1045 build/lib/core/models.py:1362
#: core/models.py:1045 core/models.py:1362
msgid "Either user or team must be set, not both."
msgstr "Может быть выбран либо пользователь, либо команда, но не оба варианта сразу."
@@ -328,78 +328,78 @@ msgstr "Документ запрашивает доступы"
msgid "This user has already asked for access to this document."
msgstr "Этот пользователь уже запросил доступ к этому документу."
#: build/lib/core/models.py:1263 core/models.py:1263
#: build/lib/core/models.py:1255 core/models.py:1255
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr "{name} хочет получить доступ к документу!"
#: build/lib/core/models.py:1267 core/models.py:1267
#: build/lib/core/models.py:1259 core/models.py:1259
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr "{name} хочет получить доступ к следующему документу:"
#: build/lib/core/models.py:1273 core/models.py:1273
#: build/lib/core/models.py:1265 core/models.py:1265
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr "{name} запрашивает доступ к документу: {title}"
#: build/lib/core/models.py:1285 core/models.py:1285
#: build/lib/core/models.py:1277 core/models.py:1277
msgid "description"
msgstr "описание"
#: build/lib/core/models.py:1286 core/models.py:1286
#: build/lib/core/models.py:1278 core/models.py:1278
msgid "code"
msgstr "код"
#: build/lib/core/models.py:1287 core/models.py:1287
#: build/lib/core/models.py:1279 core/models.py:1279
msgid "css"
msgstr "css"
#: build/lib/core/models.py:1289 core/models.py:1289
#: build/lib/core/models.py:1281 core/models.py:1281
msgid "public"
msgstr "доступно всем"
#: build/lib/core/models.py:1291 core/models.py:1291
#: build/lib/core/models.py:1283 core/models.py:1283
msgid "Whether this template is public for anyone to use."
msgstr "Этот шаблон доступен всем пользователям."
#: build/lib/core/models.py:1297 core/models.py:1297
#: build/lib/core/models.py:1289 core/models.py:1289
msgid "Template"
msgstr "Шаблон"
#: build/lib/core/models.py:1298 core/models.py:1298
#: build/lib/core/models.py:1290 core/models.py:1290
msgid "Templates"
msgstr "Шаблоны"
#: build/lib/core/models.py:1351 core/models.py:1351
#: build/lib/core/models.py:1343 core/models.py:1343
msgid "Template/user relation"
msgstr "Отношение шаблон/пользователь"
#: build/lib/core/models.py:1352 core/models.py:1352
#: build/lib/core/models.py:1344 core/models.py:1344
msgid "Template/user relations"
msgstr "Отношения шаблон/пользователь"
#: build/lib/core/models.py:1358 core/models.py:1358
#: build/lib/core/models.py:1350 core/models.py:1350
msgid "This user is already in this template."
msgstr "Этот пользователь уже указан в этом шаблоне."
#: build/lib/core/models.py:1364 core/models.py:1364
#: build/lib/core/models.py:1356 core/models.py:1356
msgid "This team is already in this template."
msgstr "Эта команда уже указана в этом шаблоне."
#: build/lib/core/models.py:1441 core/models.py:1441
#: build/lib/core/models.py:1433 core/models.py:1433
msgid "email address"
msgstr "адрес электронной почты"
#: build/lib/core/models.py:1460 core/models.py:1460
#: build/lib/core/models.py:1452 core/models.py:1452
msgid "Document invitation"
msgstr "Приглашение для документа"
#: build/lib/core/models.py:1461 core/models.py:1461
#: build/lib/core/models.py:1453 core/models.py:1453
msgid "Document invitations"
msgstr "Приглашения для документов"
#: build/lib/core/models.py:1481 core/models.py:1481
#: build/lib/core/models.py:1473 core/models.py:1473
msgid "This email is already associated to a registered user."
msgstr "Этот адрес уже связан с зарегистрированным пользователем."
+24 -24
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-10-23 11:01+0000\n"
"PO-Revision-Date: 2025-11-10 09:54\n"
"POT-Creation-Date: 2025-11-17 08:04+0000\n"
"PO-Revision-Date: 2025-11-19 10:13\n"
"Last-Translator: \n"
"Language-Team: Slovenian\n"
"Language: sl_SI\n"
@@ -234,8 +234,8 @@ msgstr "uporabnik"
msgid "users"
msgstr "uporabniki"
#: build/lib/core/models.py:361 build/lib/core/models.py:1284
#: core/models.py:361 core/models.py:1284
#: build/lib/core/models.py:361 build/lib/core/models.py:1276
#: core/models.py:361 core/models.py:1276
msgid "title"
msgstr "naslov"
@@ -311,8 +311,8 @@ msgstr "Ta uporabnik je že v tem dokumentu."
msgid "This team is already in this document."
msgstr "Ta ekipa je že v tem dokumentu."
#: build/lib/core/models.py:1045 build/lib/core/models.py:1370
#: core/models.py:1045 core/models.py:1370
#: build/lib/core/models.py:1045 build/lib/core/models.py:1362
#: core/models.py:1045 core/models.py:1362
msgid "Either user or team must be set, not both."
msgstr "Nastaviti je treba bodisi uporabnika ali ekipo, a ne obojega."
@@ -328,78 +328,78 @@ msgstr ""
msgid "This user has already asked for access to this document."
msgstr ""
#: build/lib/core/models.py:1263 core/models.py:1263
#: build/lib/core/models.py:1255 core/models.py:1255
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr ""
#: build/lib/core/models.py:1267 core/models.py:1267
#: build/lib/core/models.py:1259 core/models.py:1259
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr ""
#: build/lib/core/models.py:1273 core/models.py:1273
#: build/lib/core/models.py:1265 core/models.py:1265
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr ""
#: build/lib/core/models.py:1285 core/models.py:1285
#: build/lib/core/models.py:1277 core/models.py:1277
msgid "description"
msgstr "opis"
#: build/lib/core/models.py:1286 core/models.py:1286
#: build/lib/core/models.py:1278 core/models.py:1278
msgid "code"
msgstr "koda"
#: build/lib/core/models.py:1287 core/models.py:1287
#: build/lib/core/models.py:1279 core/models.py:1279
msgid "css"
msgstr "css"
#: build/lib/core/models.py:1289 core/models.py:1289
#: build/lib/core/models.py:1281 core/models.py:1281
msgid "public"
msgstr "javno"
#: build/lib/core/models.py:1291 core/models.py:1291
#: build/lib/core/models.py:1283 core/models.py:1283
msgid "Whether this template is public for anyone to use."
msgstr "Ali je ta predloga javna za uporabo."
#: build/lib/core/models.py:1297 core/models.py:1297
#: build/lib/core/models.py:1289 core/models.py:1289
msgid "Template"
msgstr "Predloga"
#: build/lib/core/models.py:1298 core/models.py:1298
#: build/lib/core/models.py:1290 core/models.py:1290
msgid "Templates"
msgstr "Predloge"
#: build/lib/core/models.py:1351 core/models.py:1351
#: build/lib/core/models.py:1343 core/models.py:1343
msgid "Template/user relation"
msgstr "Odnos predloga/uporabnik"
#: build/lib/core/models.py:1352 core/models.py:1352
#: build/lib/core/models.py:1344 core/models.py:1344
msgid "Template/user relations"
msgstr "Odnosi med predlogo in uporabnikom"
#: build/lib/core/models.py:1358 core/models.py:1358
#: build/lib/core/models.py:1350 core/models.py:1350
msgid "This user is already in this template."
msgstr "Ta uporabnik je že v tej predlogi."
#: build/lib/core/models.py:1364 core/models.py:1364
#: build/lib/core/models.py:1356 core/models.py:1356
msgid "This team is already in this template."
msgstr "Ta ekipa je že v tej predlogi."
#: build/lib/core/models.py:1441 core/models.py:1441
#: build/lib/core/models.py:1433 core/models.py:1433
msgid "email address"
msgstr "elektronski naslov"
#: build/lib/core/models.py:1460 core/models.py:1460
#: build/lib/core/models.py:1452 core/models.py:1452
msgid "Document invitation"
msgstr "Vabilo na dokument"
#: build/lib/core/models.py:1461 core/models.py:1461
#: build/lib/core/models.py:1453 core/models.py:1453
msgid "Document invitations"
msgstr "Vabila na dokument"
#: build/lib/core/models.py:1481 core/models.py:1481
#: build/lib/core/models.py:1473 core/models.py:1473
msgid "This email is already associated to a registered user."
msgstr "Ta e-poštni naslov je že povezan z registriranim uporabnikom."
+24 -24
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-10-23 11:01+0000\n"
"PO-Revision-Date: 2025-11-10 09:54\n"
"POT-Creation-Date: 2025-11-17 08:04+0000\n"
"PO-Revision-Date: 2025-11-19 10:13\n"
"Last-Translator: \n"
"Language-Team: Swedish\n"
"Language: sv_SE\n"
@@ -234,8 +234,8 @@ msgstr ""
msgid "users"
msgstr ""
#: build/lib/core/models.py:361 build/lib/core/models.py:1284
#: core/models.py:361 core/models.py:1284
#: build/lib/core/models.py:361 build/lib/core/models.py:1276
#: core/models.py:361 core/models.py:1276
msgid "title"
msgstr ""
@@ -311,8 +311,8 @@ msgstr ""
msgid "This team is already in this document."
msgstr ""
#: build/lib/core/models.py:1045 build/lib/core/models.py:1370
#: core/models.py:1045 core/models.py:1370
#: build/lib/core/models.py:1045 build/lib/core/models.py:1362
#: core/models.py:1045 core/models.py:1362
msgid "Either user or team must be set, not both."
msgstr ""
@@ -328,78 +328,78 @@ msgstr ""
msgid "This user has already asked for access to this document."
msgstr ""
#: build/lib/core/models.py:1263 core/models.py:1263
#: build/lib/core/models.py:1255 core/models.py:1255
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr ""
#: build/lib/core/models.py:1267 core/models.py:1267
#: build/lib/core/models.py:1259 core/models.py:1259
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr ""
#: build/lib/core/models.py:1273 core/models.py:1273
#: build/lib/core/models.py:1265 core/models.py:1265
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr ""
#: build/lib/core/models.py:1285 core/models.py:1285
#: build/lib/core/models.py:1277 core/models.py:1277
msgid "description"
msgstr ""
#: build/lib/core/models.py:1286 core/models.py:1286
#: build/lib/core/models.py:1278 core/models.py:1278
msgid "code"
msgstr ""
#: build/lib/core/models.py:1287 core/models.py:1287
#: build/lib/core/models.py:1279 core/models.py:1279
msgid "css"
msgstr ""
#: build/lib/core/models.py:1289 core/models.py:1289
#: build/lib/core/models.py:1281 core/models.py:1281
msgid "public"
msgstr ""
#: build/lib/core/models.py:1291 core/models.py:1291
#: build/lib/core/models.py:1283 core/models.py:1283
msgid "Whether this template is public for anyone to use."
msgstr ""
#: build/lib/core/models.py:1297 core/models.py:1297
#: build/lib/core/models.py:1289 core/models.py:1289
msgid "Template"
msgstr ""
#: build/lib/core/models.py:1298 core/models.py:1298
#: build/lib/core/models.py:1290 core/models.py:1290
msgid "Templates"
msgstr ""
#: build/lib/core/models.py:1351 core/models.py:1351
#: build/lib/core/models.py:1343 core/models.py:1343
msgid "Template/user relation"
msgstr ""
#: build/lib/core/models.py:1352 core/models.py:1352
#: build/lib/core/models.py:1344 core/models.py:1344
msgid "Template/user relations"
msgstr ""
#: build/lib/core/models.py:1358 core/models.py:1358
#: build/lib/core/models.py:1350 core/models.py:1350
msgid "This user is already in this template."
msgstr ""
#: build/lib/core/models.py:1364 core/models.py:1364
#: build/lib/core/models.py:1356 core/models.py:1356
msgid "This team is already in this template."
msgstr ""
#: build/lib/core/models.py:1441 core/models.py:1441
#: build/lib/core/models.py:1433 core/models.py:1433
msgid "email address"
msgstr "e-postadress"
#: build/lib/core/models.py:1460 core/models.py:1460
#: build/lib/core/models.py:1452 core/models.py:1452
msgid "Document invitation"
msgstr "Bjud in dokument"
#: build/lib/core/models.py:1461 core/models.py:1461
#: build/lib/core/models.py:1453 core/models.py:1453
msgid "Document invitations"
msgstr "Inbjudningar dokument"
#: build/lib/core/models.py:1481 core/models.py:1481
#: build/lib/core/models.py:1473 core/models.py:1473
msgid "This email is already associated to a registered user."
msgstr "Denna e-postadress är redan associerad med en registrerad användare."
+24 -24
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-10-23 11:01+0000\n"
"PO-Revision-Date: 2025-11-10 09:54\n"
"POT-Creation-Date: 2025-11-17 08:04+0000\n"
"PO-Revision-Date: 2025-11-19 10:13\n"
"Last-Translator: \n"
"Language-Team: Turkish\n"
"Language: tr_TR\n"
@@ -234,8 +234,8 @@ msgstr ""
msgid "users"
msgstr ""
#: build/lib/core/models.py:361 build/lib/core/models.py:1284
#: core/models.py:361 core/models.py:1284
#: build/lib/core/models.py:361 build/lib/core/models.py:1276
#: core/models.py:361 core/models.py:1276
msgid "title"
msgstr ""
@@ -311,8 +311,8 @@ msgstr ""
msgid "This team is already in this document."
msgstr ""
#: build/lib/core/models.py:1045 build/lib/core/models.py:1370
#: core/models.py:1045 core/models.py:1370
#: build/lib/core/models.py:1045 build/lib/core/models.py:1362
#: core/models.py:1045 core/models.py:1362
msgid "Either user or team must be set, not both."
msgstr ""
@@ -328,78 +328,78 @@ msgstr ""
msgid "This user has already asked for access to this document."
msgstr ""
#: build/lib/core/models.py:1263 core/models.py:1263
#: build/lib/core/models.py:1255 core/models.py:1255
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr ""
#: build/lib/core/models.py:1267 core/models.py:1267
#: build/lib/core/models.py:1259 core/models.py:1259
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr ""
#: build/lib/core/models.py:1273 core/models.py:1273
#: build/lib/core/models.py:1265 core/models.py:1265
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr ""
#: build/lib/core/models.py:1285 core/models.py:1285
#: build/lib/core/models.py:1277 core/models.py:1277
msgid "description"
msgstr ""
#: build/lib/core/models.py:1286 core/models.py:1286
#: build/lib/core/models.py:1278 core/models.py:1278
msgid "code"
msgstr ""
#: build/lib/core/models.py:1287 core/models.py:1287
#: build/lib/core/models.py:1279 core/models.py:1279
msgid "css"
msgstr ""
#: build/lib/core/models.py:1289 core/models.py:1289
#: build/lib/core/models.py:1281 core/models.py:1281
msgid "public"
msgstr ""
#: build/lib/core/models.py:1291 core/models.py:1291
#: build/lib/core/models.py:1283 core/models.py:1283
msgid "Whether this template is public for anyone to use."
msgstr ""
#: build/lib/core/models.py:1297 core/models.py:1297
#: build/lib/core/models.py:1289 core/models.py:1289
msgid "Template"
msgstr ""
#: build/lib/core/models.py:1298 core/models.py:1298
#: build/lib/core/models.py:1290 core/models.py:1290
msgid "Templates"
msgstr ""
#: build/lib/core/models.py:1351 core/models.py:1351
#: build/lib/core/models.py:1343 core/models.py:1343
msgid "Template/user relation"
msgstr ""
#: build/lib/core/models.py:1352 core/models.py:1352
#: build/lib/core/models.py:1344 core/models.py:1344
msgid "Template/user relations"
msgstr ""
#: build/lib/core/models.py:1358 core/models.py:1358
#: build/lib/core/models.py:1350 core/models.py:1350
msgid "This user is already in this template."
msgstr ""
#: build/lib/core/models.py:1364 core/models.py:1364
#: build/lib/core/models.py:1356 core/models.py:1356
msgid "This team is already in this template."
msgstr ""
#: build/lib/core/models.py:1441 core/models.py:1441
#: build/lib/core/models.py:1433 core/models.py:1433
msgid "email address"
msgstr ""
#: build/lib/core/models.py:1460 core/models.py:1460
#: build/lib/core/models.py:1452 core/models.py:1452
msgid "Document invitation"
msgstr ""
#: build/lib/core/models.py:1461 core/models.py:1461
#: build/lib/core/models.py:1453 core/models.py:1453
msgid "Document invitations"
msgstr ""
#: build/lib/core/models.py:1481 core/models.py:1481
#: build/lib/core/models.py:1473 core/models.py:1473
msgid "This email is already associated to a registered user."
msgstr ""
+24 -24
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-10-23 11:01+0000\n"
"PO-Revision-Date: 2025-11-10 09:54\n"
"POT-Creation-Date: 2025-11-17 08:04+0000\n"
"PO-Revision-Date: 2025-11-19 10:13\n"
"Last-Translator: \n"
"Language-Team: Ukrainian\n"
"Language: uk_UA\n"
@@ -234,8 +234,8 @@ msgstr "користувач"
msgid "users"
msgstr "користувачі"
#: build/lib/core/models.py:361 build/lib/core/models.py:1284
#: core/models.py:361 core/models.py:1284
#: build/lib/core/models.py:361 build/lib/core/models.py:1276
#: core/models.py:361 core/models.py:1276
msgid "title"
msgstr "заголовок"
@@ -311,8 +311,8 @@ msgstr "Цей користувач вже має доступ до цього
msgid "This team is already in this document."
msgstr "Ця команда вже має доступ до цього документа."
#: build/lib/core/models.py:1045 build/lib/core/models.py:1370
#: core/models.py:1045 core/models.py:1370
#: build/lib/core/models.py:1045 build/lib/core/models.py:1362
#: core/models.py:1045 core/models.py:1362
msgid "Either user or team must be set, not both."
msgstr "Вкажіть користувача або команду, а не обох."
@@ -328,78 +328,78 @@ msgstr "Запит доступу для документа"
msgid "This user has already asked for access to this document."
msgstr "Цей користувач вже попросив доступ до цього документа."
#: build/lib/core/models.py:1263 core/models.py:1263
#: build/lib/core/models.py:1255 core/models.py:1255
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr "{name} хоче отримати доступ до документа!"
#: build/lib/core/models.py:1267 core/models.py:1267
#: build/lib/core/models.py:1259 core/models.py:1259
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr "{name} бажає отримати доступ до наступного документа:"
#: build/lib/core/models.py:1273 core/models.py:1273
#: build/lib/core/models.py:1265 core/models.py:1265
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr "{name} запитує доступ до документа: {title}"
#: build/lib/core/models.py:1285 core/models.py:1285
#: build/lib/core/models.py:1277 core/models.py:1277
msgid "description"
msgstr "опис"
#: build/lib/core/models.py:1286 core/models.py:1286
#: build/lib/core/models.py:1278 core/models.py:1278
msgid "code"
msgstr "код"
#: build/lib/core/models.py:1287 core/models.py:1287
#: build/lib/core/models.py:1279 core/models.py:1279
msgid "css"
msgstr "css"
#: build/lib/core/models.py:1289 core/models.py:1289
#: build/lib/core/models.py:1281 core/models.py:1281
msgid "public"
msgstr "публічне"
#: build/lib/core/models.py:1291 core/models.py:1291
#: build/lib/core/models.py:1283 core/models.py:1283
msgid "Whether this template is public for anyone to use."
msgstr "Чи є цей шаблон публічним для будь-кого користувача."
#: build/lib/core/models.py:1297 core/models.py:1297
#: build/lib/core/models.py:1289 core/models.py:1289
msgid "Template"
msgstr "Шаблон"
#: build/lib/core/models.py:1298 core/models.py:1298
#: build/lib/core/models.py:1290 core/models.py:1290
msgid "Templates"
msgstr "Шаблони"
#: build/lib/core/models.py:1351 core/models.py:1351
#: build/lib/core/models.py:1343 core/models.py:1343
msgid "Template/user relation"
msgstr "Відношення шаблон/користувач"
#: build/lib/core/models.py:1352 core/models.py:1352
#: build/lib/core/models.py:1344 core/models.py:1344
msgid "Template/user relations"
msgstr "Відношення шаблон/користувач"
#: build/lib/core/models.py:1358 core/models.py:1358
#: build/lib/core/models.py:1350 core/models.py:1350
msgid "This user is already in this template."
msgstr "Цей користувач вже має доступ до цього шаблону."
#: build/lib/core/models.py:1364 core/models.py:1364
#: build/lib/core/models.py:1356 core/models.py:1356
msgid "This team is already in this template."
msgstr "Ця команда вже має доступ до цього шаблону."
#: build/lib/core/models.py:1441 core/models.py:1441
#: build/lib/core/models.py:1433 core/models.py:1433
msgid "email address"
msgstr "електронна адреса"
#: build/lib/core/models.py:1460 core/models.py:1460
#: build/lib/core/models.py:1452 core/models.py:1452
msgid "Document invitation"
msgstr "Запрошення до редагування документа"
#: build/lib/core/models.py:1461 core/models.py:1461
#: build/lib/core/models.py:1453 core/models.py:1453
msgid "Document invitations"
msgstr "Запрошення до редагування документів"
#: build/lib/core/models.py:1481 core/models.py:1481
#: build/lib/core/models.py:1473 core/models.py:1473
msgid "This email is already associated to a registered user."
msgstr "Ця електронна пошта вже пов'язана з зареєстрованим користувачем."
+24 -24
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-10-23 11:01+0000\n"
"PO-Revision-Date: 2025-11-10 09:54\n"
"POT-Creation-Date: 2025-11-17 08:04+0000\n"
"PO-Revision-Date: 2025-11-19 10:13\n"
"Last-Translator: \n"
"Language-Team: Chinese Simplified\n"
"Language: zh_CN\n"
@@ -234,8 +234,8 @@ msgstr "用户"
msgid "users"
msgstr "个用户"
#: build/lib/core/models.py:361 build/lib/core/models.py:1284
#: core/models.py:361 core/models.py:1284
#: build/lib/core/models.py:361 build/lib/core/models.py:1276
#: core/models.py:361 core/models.py:1276
msgid "title"
msgstr "标题"
@@ -311,8 +311,8 @@ msgstr "该用户已在此文档中。"
msgid "This team is already in this document."
msgstr "该团队已在此文档中。"
#: build/lib/core/models.py:1045 build/lib/core/models.py:1370
#: core/models.py:1045 core/models.py:1370
#: build/lib/core/models.py:1045 build/lib/core/models.py:1362
#: core/models.py:1045 core/models.py:1362
msgid "Either user or team must be set, not both."
msgstr "必须设置用户或团队之一,不能同时设置两者。"
@@ -328,78 +328,78 @@ msgstr "文档需要访问权限"
msgid "This user has already asked for access to this document."
msgstr "用户已申请该文档的访问权限。"
#: build/lib/core/models.py:1263 core/models.py:1263
#: build/lib/core/models.py:1255 core/models.py:1255
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr "{name} 申请访问文档!"
#: build/lib/core/models.py:1267 core/models.py:1267
#: build/lib/core/models.py:1259 core/models.py:1259
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr "{name} 申请访问以下文档:"
#: build/lib/core/models.py:1273 core/models.py:1273
#: build/lib/core/models.py:1265 core/models.py:1265
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr "{name}申请文档:{title}的访问权限"
#: build/lib/core/models.py:1285 core/models.py:1285
#: build/lib/core/models.py:1277 core/models.py:1277
msgid "description"
msgstr "说明"
#: build/lib/core/models.py:1286 core/models.py:1286
#: build/lib/core/models.py:1278 core/models.py:1278
msgid "code"
msgstr "代码"
#: build/lib/core/models.py:1287 core/models.py:1287
#: build/lib/core/models.py:1279 core/models.py:1279
msgid "css"
msgstr "css"
#: build/lib/core/models.py:1289 core/models.py:1289
#: build/lib/core/models.py:1281 core/models.py:1281
msgid "public"
msgstr "公开"
#: build/lib/core/models.py:1291 core/models.py:1291
#: build/lib/core/models.py:1283 core/models.py:1283
msgid "Whether this template is public for anyone to use."
msgstr "该模板是否公开供任何人使用。"
#: build/lib/core/models.py:1297 core/models.py:1297
#: build/lib/core/models.py:1289 core/models.py:1289
msgid "Template"
msgstr "模板"
#: build/lib/core/models.py:1298 core/models.py:1298
#: build/lib/core/models.py:1290 core/models.py:1290
msgid "Templates"
msgstr "模板"
#: build/lib/core/models.py:1351 core/models.py:1351
#: build/lib/core/models.py:1343 core/models.py:1343
msgid "Template/user relation"
msgstr "模板/用户关系"
#: build/lib/core/models.py:1352 core/models.py:1352
#: build/lib/core/models.py:1344 core/models.py:1344
msgid "Template/user relations"
msgstr "模板/用户关系集"
#: build/lib/core/models.py:1358 core/models.py:1358
#: build/lib/core/models.py:1350 core/models.py:1350
msgid "This user is already in this template."
msgstr "该用户已在此模板中。"
#: build/lib/core/models.py:1364 core/models.py:1364
#: build/lib/core/models.py:1356 core/models.py:1356
msgid "This team is already in this template."
msgstr "该团队已在此模板中。"
#: build/lib/core/models.py:1441 core/models.py:1441
#: build/lib/core/models.py:1433 core/models.py:1433
msgid "email address"
msgstr "电子邮件地址"
#: build/lib/core/models.py:1460 core/models.py:1460
#: build/lib/core/models.py:1452 core/models.py:1452
msgid "Document invitation"
msgstr "文档邀请"
#: build/lib/core/models.py:1461 core/models.py:1461
#: build/lib/core/models.py:1453 core/models.py:1453
msgid "Document invitations"
msgstr "文档邀请"
#: build/lib/core/models.py:1481 core/models.py:1481
#: build/lib/core/models.py:1473 core/models.py:1473
msgid "This email is already associated to a registered user."
msgstr "此电子邮件已经与现有注册用户关联。"
+13 -13
View File
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "impress"
version = "3.9.0"
version = "3.10.0"
authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }]
classifiers = [
"Development Status :: 5 - Production/Stable",
@@ -26,15 +26,15 @@ readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"beautifulsoup4==4.14.2",
"boto3==1.40.59",
"boto3==1.40.74",
"Brotli==1.2.0",
"celery[redis]==5.5.3",
"django-configurations==2.5.1",
"django-cors-headers==4.9.0",
"django-countries==7.6.1",
"django-countries==8.1.0",
"django-csp==4.0",
"django-filter==25.2",
"django-lasuite[all]==0.0.16",
"django-lasuite[all]==0.0.18",
"django-parler==2.3",
"django-redis==6.0.0",
"django-storages[s3]==1.14.6",
@@ -42,24 +42,24 @@ dependencies = [
"django==5.2.8",
"django-treebeard==4.7.1",
"djangorestframework==3.16.1",
"drf_spectacular==0.28.0",
"drf_spectacular==0.29.0",
"dockerflow==2024.4.2",
"easy_thumbnails==2.10.1",
"factory_boy==3.3.3",
"gunicorn==23.0.0",
"jsonschema==4.25.1",
"lxml==6.0.2",
"markdown==3.9",
"markdown==3.10",
"mozilla-django-oidc==4.0.1",
"nested-multipart-parser==1.6.0",
"openai==2.6.1",
"openai==2.8.0",
"psycopg[binary]==3.2.12",
"pycrdt==0.12.42",
"pycrdt==0.12.43",
"PyJWT==2.10.1",
"python-magic==0.4.27",
"redis<6.0.0",
"requests==2.32.5",
"sentry-sdk==2.42.1",
"sentry-sdk==2.44.0",
"whitenoise==6.11.0",
]
@@ -76,17 +76,17 @@ dev = [
"drf-spectacular-sidecar==2025.10.1",
"freezegun==1.5.5",
"ipdb==0.13.13",
"ipython==9.6.0",
"pyfakefs==5.10.0",
"ipython==9.7.0",
"pyfakefs==5.10.2",
"pylint-django==2.6.1",
"pylint<4.0.0",
"pytest-cov==7.0.0",
"pytest-django==4.11.1",
"pytest==8.4.2",
"pytest==9.0.1",
"pytest-icdiff==0.9",
"pytest-xdist==3.8.0",
"responses==0.25.8",
"ruff==0.14.2",
"ruff==0.14.5",
"types-requests==2.32.4.20250913",
]
@@ -806,6 +806,12 @@ test.describe('Doc Editor', () => {
});
await expect(interlinkChild1).toBeVisible({ timeout: 10000 });
await expect(interlinkChild1.locator('svg').first()).toBeVisible();
await page.keyboard.press('@');
await page.keyboard.press('Escape');
await expect(editor.getByText('@')).toBeVisible();
});
test('it checks multiple big doc scroll to the top', async ({
@@ -31,7 +31,7 @@ test.describe('Doc Export', () => {
await expect(page.getByTestId('modal-export-title')).toBeVisible();
await expect(
page.getByText('Download your document in a .docx or .pdf format.'),
page.getByText('Download your document in a .docx, .odt or .pdf format.'),
).toBeVisible();
await expect(
page.getByRole('combobox', { name: 'Template' }),
@@ -142,6 +142,51 @@ test.describe('Doc Export', () => {
expect(download.suggestedFilename()).toBe(`${randomDoc}.docx`);
});
test('it exports the doc to odt', async ({ page, browserName }) => {
const [randomDoc] = await createDoc(page, 'doc-editor-odt', browserName, 1);
await verifyDocName(page, randomDoc);
await page.locator('.ProseMirror.bn-editor').click();
await page.locator('.ProseMirror.bn-editor').fill('Hello World ODT');
await page.keyboard.press('Enter');
await page.locator('.bn-block-outer').last().fill('/');
await page.getByText('Resizable image with caption').click();
const fileChooserPromise = page.waitForEvent('filechooser');
await page.getByText('Upload image').click();
const fileChooser = await fileChooserPromise;
await fileChooser.setFiles(path.join(__dirname, 'assets/test.svg'));
const image = page
.locator('.--docs--editor-container img.bn-visual-media')
.first();
await expect(image).toBeVisible();
await page
.getByRole('button', {
name: 'Export the document',
})
.click();
await page.getByRole('combobox', { name: 'Format' }).click();
await page.getByRole('option', { name: 'Odt' }).click();
await expect(page.getByTestId('doc-export-download-button')).toBeVisible();
const downloadPromise = page.waitForEvent('download', (download) => {
return download.suggestedFilename().includes(`${randomDoc}.odt`);
});
void page.getByTestId('doc-export-download-button').click();
const download = await downloadPromise;
expect(download.suggestedFilename()).toBe(`${randomDoc}.odt`);
});
/**
* This test tell us that the export to pdf is working with images
* but it does not tell us if the images are being displayed correctly
@@ -442,4 +487,68 @@ test.describe('Doc Export', () => {
const pdfText = await pdfParse.getText();
expect(pdfText.text).toContain(randomDoc);
});
test('it exports the doc with interlinking to odt', async ({
page,
browserName,
}) => {
const [randomDoc] = await createDoc(
page,
'export-interlinking-odt',
browserName,
1,
);
await verifyDocName(page, randomDoc);
const { name: docChild } = await createRootSubPage(
page,
browserName,
'export-interlink-child-odt',
);
await verifyDocName(page, docChild);
await page.locator('.bn-block-outer').last().fill('/');
await page.getByText('Link a doc').first().click();
const input = page.locator(
"span[data-inline-content-type='interlinkingSearchInline'] input",
);
const searchContainer = page.locator('.quick-search-container');
await input.fill('export-interlink');
await expect(searchContainer).toBeVisible();
await expect(searchContainer.getByText(randomDoc)).toBeVisible();
// We are in docChild, we want to create a link to randomDoc (parent)
await searchContainer.getByText(randomDoc).click();
// Search the interlinking link in the editor (not in the document tree)
const editor = page.locator('.ProseMirror.bn-editor');
const interlink = editor.getByRole('button', {
name: randomDoc,
});
await expect(interlink).toBeVisible();
await page
.getByRole('button', {
name: 'Export the document',
})
.click();
await page.getByRole('combobox', { name: 'Format' }).click();
await page.getByRole('option', { name: 'Odt' }).click();
const downloadPromise = page.waitForEvent('download', (download) => {
return download.suggestedFilename().includes(`${docChild}.odt`);
});
void page.getByTestId('doc-export-download-button').click();
const download = await downloadPromise;
expect(download.suggestedFilename()).toBe(`${docChild}.odt`);
});
});
@@ -160,6 +160,9 @@ test.describe('Document list members', () => {
`You are the sole owner of this group, make another member the group owner before you can change your own role or be removed from your document.`,
);
await expect(soloOwner).toBeVisible();
await expect(
page.getByRole('menuitem', { name: 'Administrator' }),
).toBeDisabled();
await list.click({
force: true, // Force click to close the dropdown
@@ -186,9 +189,17 @@ test.describe('Document list members', () => {
await list.click();
await expect(currentUserRole).toBeVisible();
await newUserRoles.click();
await expect(page.getByRole('menuitem', { name: 'Owner' })).toBeDisabled();
await list.click({
force: true, // Force click to close the dropdown
});
await currentUserRole.click();
await page.getByRole('menuitem', { name: 'Reader' }).click();
await list.click();
await list.click({
force: true, // Force click to close the dropdown
});
await expect(currentUserRole).toBeHidden();
});
@@ -27,6 +27,33 @@ test.describe('Home page', () => {
// Check the titles
const h2 = page.locator('h2');
await expect(h2.getByText('Govs ❤️ Open Source.')).toBeVisible();
await expect(page.getByText('Docs is built on top of')).toBeVisible();
await expect(
page.getByRole('link', {
name: 'Django Rest Framework',
}),
).toHaveAttribute('href', 'https://www.django-rest-framework.org/');
await expect(page.getByText('You can easily self-host Docs')).toBeVisible();
await expect(
page
.getByRole('link', {
name: 'licence',
})
.first(),
).toHaveAttribute(
'href',
'https://github.com/suitenumerique/docs/blob/main/LICENSE',
);
await expect(
page.getByText('Docs is the result of a joint effort lead by the French'),
).toBeVisible();
await expect(
page
.getByRole('link', {
name: 'Zendis',
})
.first(),
).toHaveAttribute('href', 'https://zendis.de/');
await expect(
h2.getByText('Collaborative writing, Simplified.'),
).toBeVisible();
@@ -7,14 +7,14 @@ import {
verifyDocName,
} from './utils-common';
export type Role = 'Administrator' | 'Owner' | 'Member' | 'Editor' | 'Reader';
export type Role = 'Administrator' | 'Owner' | 'Editor' | 'Reader';
export type LinkReach = 'Private' | 'Connected' | 'Public';
export type LinkRole = 'Reading' | 'Editing';
export const addNewMember = async (
page: Page,
index: number,
role: 'Administrator' | 'Owner' | 'Editor' | 'Reader',
role: Role,
fillText = 'user.test',
) => {
const responsePromiseSearchUser = page.waitForResponse(
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "app-e2e",
"version": "3.9.0",
"version": "3.10.0",
"repository": "https://github.com/suitenumerique/docs",
"author": "DINUM",
"license": "MIT",
+11 -10
View File
@@ -1,6 +1,6 @@
{
"name": "app-impress",
"version": "3.9.0",
"version": "3.10.0",
"repository": "https://github.com/suitenumerique/docs",
"author": "DINUM",
"license": "MIT",
@@ -19,13 +19,14 @@
},
"dependencies": {
"@ag-media/react-pdf-table": "2.0.3",
"@blocknote/code-block": "0.41.1",
"@blocknote/core": "0.41.1",
"@blocknote/mantine": "0.41.1",
"@blocknote/react": "0.41.1",
"@blocknote/xl-docx-exporter": "0.41.1",
"@blocknote/xl-multi-column": "0.41.1",
"@blocknote/xl-pdf-exporter": "0.41.1",
"@blocknote/code-block": "0.42.3",
"@blocknote/core": "0.42.3",
"@blocknote/mantine": "0.42.3",
"@blocknote/react": "0.42.3",
"@blocknote/xl-docx-exporter": "0.42.3",
"@blocknote/xl-multi-column": "0.42.3",
"@blocknote/xl-odt-exporter": "0.42.3",
"@blocknote/xl-pdf-exporter": "0.42.3",
"@dnd-kit/core": "6.3.1",
"@dnd-kit/modifiers": "9.0.0",
"@emoji-mart/data": "1.2.1",
@@ -42,7 +43,7 @@
"@react-pdf/renderer": "4.3.1",
"@sentry/nextjs": "10.22.0",
"@tanstack/react-query": "5.90.6",
"@tiptap/extensions": "3.10.1",
"@tiptap/extensions": "*",
"canvg": "4.0.3",
"clsx": "2.1.1",
"cmdk": "1.1.1",
@@ -61,7 +62,7 @@
"react": "*",
"react-aria-components": "1.13.0",
"react-dom": "*",
"react-i18next": "16.2.3",
"react-i18next": "16.3.3",
"react-intersection-observer": "10.0.0",
"react-resizable-panels": "3.0.6",
"react-select": "5.10.2",
@@ -12,6 +12,7 @@ import { css } from 'styled-components';
import { Box, BoxButton, BoxProps, DropButton, Icon, Text } from '@/components';
import { useCunninghamTheme } from '@/cunningham';
import { useKeyboardAction } from '@/hooks';
import { useDropdownKeyboardNav } from './hook/useDropdownKeyboardNav';
@@ -57,6 +58,7 @@ export const DropdownMenu = ({
testId,
}: PropsWithChildren<DropdownMenuProps>) => {
const { spacingsTokens, colorsTokens } = useCunninghamTheme();
const keyboardAction = useKeyboardAction();
const [isOpen, setIsOpen] = useState(opened ?? false);
const [focusedIndex, setFocusedIndex] = useState(-1);
const blockButtonRef = useRef<HTMLDivElement>(null);
@@ -93,6 +95,14 @@ export const DropdownMenu = ({
}
}, [isOpen, options]);
const triggerOption = useCallback(
(option: DropdownMenuOption) => {
onOpenChange?.(false);
void option.callback?.();
},
[onOpenChange],
);
if (disabled) {
return children;
}
@@ -170,9 +180,9 @@ export const DropdownMenu = ({
onClick={(event) => {
event.preventDefault();
event.stopPropagation();
onOpenChange?.(false);
void option.callback?.();
triggerOption(option);
}}
onKeyDown={keyboardAction(() => triggerOption(option))}
key={option.label}
$align="center"
$justify="space-between"
@@ -186,6 +186,7 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
formattingToolbar={false}
slashMenu={false}
theme="light"
aria-label={t('Document editor')}
>
<BlockNoteSuggestionMenu />
<BlockNoteToolbar />
@@ -200,6 +201,7 @@ interface BlockNoteReaderProps {
export const BlockNoteReader = ({ initialContent }: BlockNoteReaderProps) => {
const { setEditor } = useEditorStore();
const { t } = useTranslation();
const editor = useCreateBlockNote(
{
collaboration: {
@@ -231,6 +233,7 @@ export const BlockNoteReader = ({ initialContent }: BlockNoteReaderProps) => {
editor={editor}
editable={false}
theme="light"
aria-label={t('Document version viewer')}
formattingToolbar={false}
slashMenu={false}
/>
@@ -99,6 +99,7 @@ export const DocEditor = ({ doc }: DocEditorProps) => {
<>
{isDesktop && (
<Box
$height="100vh"
$position="absolute"
$css={css`
top: 72px;
@@ -173,5 +173,4 @@ export const getCalloutFormattingToolbarItems = (
name: t('Callout'),
type: 'callout',
icon: () => <Icon iconName="lightbulb" $size="16px" />,
isSelected: (block) => block.type === 'callout',
});
@@ -22,8 +22,8 @@ import { Box, Icon } from '@/components';
import { DocsBlockNoteEditor } from '../../types';
const PDFBlockStyle = createGlobalStyle`
.bn-block-content[data-content-type="pdf"] {
width: fit-content;
.bn-block-content[data-content-type="pdf"] .bn-file-block-content-wrapper[style*="fit-content"] {
width: 100% !important;
}
`;
@@ -3,6 +3,7 @@ import {
StyleSchema,
} from '@blocknote/core';
import { useBlockNoteEditor } from '@blocknote/react';
import type { KeyboardEvent } from 'react';
import { useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { css } from 'styled-components';
@@ -99,6 +100,55 @@ export const SearchPage = ({
}, 100);
}, [inputRef]);
const closeSearch = (insertContent: string) => {
updateInlineContent({
type: 'interlinkingSearchInline',
props: {
disabled: true,
trigger,
},
});
contentRef(null);
editor.focus();
editor.insertInlineContent([insertContent]);
};
const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Escape') {
e.preventDefault();
// Keep the trigger character ('@' or '/') in the editor when closing with Escape
closeSearch(trigger);
} else if (e.key === 'Backspace' && search.length === 0) {
e.preventDefault();
closeSearch('');
} else if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
// Allow arrow keys to be handled by the command menu for navigation
const commandList = e.currentTarget
.closest('.inline-content')
?.nextElementSibling?.querySelector('[cmdk-list]');
// Create a synthetic keyboard event for the command menu
const syntheticEvent = new KeyboardEvent('keydown', {
key: e.key,
bubbles: true,
cancelable: true,
});
commandList?.dispatchEvent(syntheticEvent);
e.preventDefault();
} else if (e.key === 'Enter') {
// Handle Enter key to select the currently highlighted item
const selectedItem = e.currentTarget
.closest('.inline-content')
?.nextElementSibling?.querySelector(
'[cmdk-item][data-selected="true"]',
) as HTMLElement;
selectedItem?.click();
e.preventDefault();
}
};
return (
<Box as="span" $position="relative">
<Box
@@ -124,50 +174,7 @@ export const SearchPage = ({
const value = (e.target as HTMLInputElement).value;
setSearch(value);
}}
onKeyDown={(e) => {
if (
(e.key === 'Backspace' && search.length === 0) ||
e.key === 'Escape'
) {
e.preventDefault();
updateInlineContent({
type: 'interlinkingSearchInline',
props: {
disabled: true,
trigger,
},
});
contentRef(null);
editor.focus();
editor.insertInlineContent(['']);
} else if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
// Allow arrow keys to be handled by the command menu for navigation
const commandList = e.currentTarget
.closest('.inline-content')
?.nextElementSibling?.querySelector('[cmdk-list]');
// Create a synthetic keyboard event for the command menu
const syntheticEvent = new KeyboardEvent('keydown', {
key: e.key,
bubbles: true,
cancelable: true,
});
commandList?.dispatchEvent(syntheticEvent);
e.preventDefault();
} else if (e.key === 'Enter') {
// Handle Enter key to select the currently highlighted item
const selectedItem = e.currentTarget
.closest('.inline-content')
?.nextElementSibling?.querySelector(
'[cmdk-item][data-selected="true"]',
) as HTMLElement;
selectedItem?.click();
e.preventDefault();
}
}}
onKeyDown={handleKeyDown}
/>
</Box>
<Box
@@ -224,6 +231,8 @@ export const SearchPage = ({
},
});
contentRef(null);
editor.insertInlineContent([
{
type: 'interlinkingLinkInline',
@@ -7,153 +7,151 @@ export const cssEditor = css`
height: 100%;
}
& .ProseMirror {
/**
* WCAG Accessibility contrast fixes for BlockNote editor
*/
.bn-block-content[data-is-empty-and-focused][data-content-type='paragraph']
.bn-inline-content:has(> .ProseMirror-trailingBreak:only-child)::before {
color: #767676 !important;
font-weight: 400;
}
/**
* Ensure long placeholder text is truncated with ellipsis
*/
.bn-block-content[data-is-empty-and-focused][data-content-type='paragraph']
.bn-inline-content:has(> .ProseMirror-trailingBreak:only-child)::before {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
width: inherit;
height: inherit;
}
.bn-block-content[data-is-empty-and-focused][data-content-type='paragraph']
.bn-inline-content:has(> .ProseMirror-trailingBreak:only-child) {
position: relative;
}
/**
* Ensure images with unsafe URLs are not interactive
*/
img.bn-visual-media[src*='-unsafe'] {
pointer-events: none;
}
/**
* Collaboration cursor styles
*/
.collaboration-cursor-custom__base {
position: relative;
}
.collaboration-cursor-custom__caret {
position: absolute;
height: 100%;
width: 2px;
bottom: 4%;
left: -1px;
}
.collaboration-cursor-custom__label {
color: #0d0d0d;
font-size: 12px;
font-weight: 600;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
position: absolute;
top: -17px;
left: 0px;
padding: 0px 6px;
border-radius: 0px;
white-space: nowrap;
transition: clip-path 0.3s ease-in-out;
border-radius: 4px 4px 4px 0;
box-shadow: inset -2px 2px 6px #ffffff00;
clip-path: polygon(0 85%, 4% 85%, 4% 100%, 0% 100%);
}
.collaboration-cursor-custom__base[data-active]
.collaboration-cursor-custom__label {
pointer-events: none;
box-shadow: inset -2px 2px 6px #ffffff88;
clip-path: polygon(0 0, 100% 0%, 100% 100%, 0% 100%);
}
/**
* Side menu
*/
.bn-side-menu[data-block-type='heading'][data-level='1'] {
height: 50px;
}
.bn-side-menu[data-block-type='heading'][data-level='2'] {
height: 43px;
}
.bn-side-menu[data-block-type='heading'][data-level='3'] {
height: 35px;
}
.bn-side-menu[data-block-type='divider'] {
height: 38px;
}
.bn-side-menu .mantine-UnstyledButton-root svg {
color: #767676 !important;
}
/**
* Callout, Paragraph and Heading blocks
*/
.bn-block {
border-radius: var(--c--theme--spacings--3xs);
}
.bn-block-outer {
border-radius: var(--c--theme--spacings--3xs);
}
.bn-block > .bn-block-content[data-background-color] {
padding: var(--c--theme--spacings--3xs) var(--c--theme--spacings--3xs);
border-radius: var(--c--theme--spacings--3xs);
}
.bn-block-content[data-content-type='checkListItem'][data-checked='true']
.bn-inline-content {
text-decoration: none;
}
h1 {
font-size: 1.875rem;
}
h2 {
font-size: 1.5rem;
}
h3 {
font-size: 1.25rem;
}
a {
color: var(--c--theme--colors--greyscale-600);
cursor: pointer;
}
.bn-block-group
.bn-block-group
.bn-block-outer:not([data-prev-depth-changed]):before {
border-left: none;
}
}
& .bn-editor {
color: var(--c--theme--colors--greyscale-700);
}
/**
* Quotes
*/
blockquote {
border-left: 4px solid var(--c--theme--colors--greyscale-300);
font-style: italic;
}
/**
* WCAG Accessibility contrast fixes for BlockNote editor
*/
.bn-block-content[data-is-empty-and-focused][data-content-type='paragraph']
.bn-inline-content:has(> .ProseMirror-trailingBreak:only-child)::before {
color: #767676 !important;
font-weight: 400;
}
/**
* Divider
*/
[data-content-type='divider'] hr {
background: #d3d2cf;
margin: 1rem 0;
width: 100%;
border: 1px solid #d3d2cf;
}
/**
* Ensure long placeholder text is truncated with ellipsis
*/
.bn-block-content[data-is-empty-and-focused][data-content-type='paragraph']
.bn-inline-content:has(> .ProseMirror-trailingBreak:only-child)::before {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
width: inherit;
height: inherit;
}
.bn-block-content[data-is-empty-and-focused][data-content-type='paragraph']
.bn-inline-content:has(> .ProseMirror-trailingBreak:only-child) {
position: relative;
}
/**
* Ensure images with unsafe URLs are not interactive
*/
img.bn-visual-media[src*='-unsafe'] {
pointer-events: none;
}
/**
* Collaboration cursor styles
*/
.collaboration-cursor-custom__base {
position: relative;
}
.collaboration-cursor-custom__caret {
position: absolute;
height: 100%;
width: 2px;
bottom: 4%;
left: -1px;
}
.collaboration-cursor-custom__label {
color: #0d0d0d;
font-size: 12px;
font-weight: 600;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
position: absolute;
top: -17px;
left: 0px;
padding: 0px 6px;
border-radius: 0px;
white-space: nowrap;
transition: clip-path 0.3s ease-in-out;
border-radius: 4px 4px 4px 0;
box-shadow: inset -2px 2px 6px #ffffff00;
clip-path: polygon(0 85%, 4% 85%, 4% 100%, 0% 100%);
}
.collaboration-cursor-custom__base[data-active]
.collaboration-cursor-custom__label {
pointer-events: none;
box-shadow: inset -2px 2px 6px #ffffff88;
clip-path: polygon(0 0, 100% 0%, 100% 100%, 0% 100%);
}
/**
* Side menu
*/
.bn-side-menu[data-block-type='heading'][data-level='1'] {
height: 54px;
}
.bn-side-menu[data-block-type='heading'][data-level='2'] {
height: 43px;
}
.bn-side-menu[data-block-type='heading'][data-level='3'] {
height: 35px;
}
.bn-side-menu[data-block-type='divider'] {
height: 38px;
}
.bn-side-menu .mantine-UnstyledButton-root svg {
color: #767676 !important;
}
/**
* Callout, Paragraph and Heading blocks
*/
.bn-block {
border-radius: var(--c--theme--spacings--3xs);
}
.bn-block-outer {
border-radius: var(--c--theme--spacings--3xs);
}
.bn-block > .bn-block-content[data-background-color] {
padding: var(--c--theme--spacings--3xs) var(--c--theme--spacings--3xs);
border-radius: var(--c--theme--spacings--3xs);
}
.bn-block-content[data-content-type='checkListItem'][data-checked='true']
.bn-inline-content {
text-decoration: none;
}
h1 {
font-size: 1.875rem;
}
h2 {
font-size: 1.5rem;
}
h3 {
font-size: 1.25rem;
}
a {
color: var(--c--theme--colors--greyscale-600);
cursor: pointer;
}
.bn-block-group
.bn-block-group
.bn-block-outer:not([data-prev-depth-changed]):before {
border-left: none;
}
/**
* Quotes
*/
blockquote {
border-left: 4px solid var(--c--theme--colors--greyscale-300);
font-style: italic;
}
/**
* Divider
*/
[data-content-type='divider'] hr {
background: #d3d2cf;
margin: 1rem 0;
width: 100%;
border: 1px solid #d3d2cf;
}
& .bn-block-outer:not(:first-child) {
@@ -0,0 +1,32 @@
import React from 'react';
import { DocsExporterODT } from '../types';
import { odtRegisterParagraphStyleForBlock } from '../utils';
export const blockMappingCalloutODT: DocsExporterODT['mappings']['blockMapping']['callout'] =
(block, exporter) => {
// Map callout to paragraph with emoji prefix
const emoji = block.props.emoji || '💡';
// Transform inline content (text, bold, links, etc.)
const inlineContent = exporter.transformInlineContent(block.content);
// Resolve background and alignment → create a dedicated paragraph style
const styleName = odtRegisterParagraphStyleForBlock(
exporter,
{
backgroundColor: block.props.backgroundColor,
textAlignment: block.props.textAlignment,
},
{ paddingCm: 0.42 },
);
return React.createElement(
'text:p',
{
'text:style-name': styleName,
},
`${emoji} `,
...inlineContent,
);
};
@@ -31,15 +31,10 @@ export const blockMappingImageDocx: DocsExporterDocx['mappings']['blockMapping']
const svgText = await blob.text();
const FALLBACK_SIZE = 536;
previewWidth = previewWidth || blob.size || FALLBACK_SIZE;
pngConverted = await convertSvgToPng(svgText, previewWidth);
const img = new Image();
img.src = pngConverted;
await new Promise((resolve) => {
img.onload = () => {
dimensions = { width: img.width, height: img.height };
resolve(null);
};
});
const result = await convertSvgToPng(svgText, previewWidth);
pngConverted = result.png;
dimensions = { width: result.width, height: result.height };
} else {
dimensions = await getImageDimensions(blob);
}
@@ -50,9 +45,9 @@ export const blockMappingImageDocx: DocsExporterDocx['mappings']['blockMapping']
const { width, height } = dimensions;
if (previewWidth && previewWidth > MAX_WIDTH) {
previewWidth = MAX_WIDTH;
}
// Ensure the final width never exceeds MAX_WIDTH to prevent images
// from overflowing the page width in the exported document
const finalWidth = Math.min(previewWidth || width, MAX_WIDTH);
return [
new Paragraph({
@@ -71,8 +66,8 @@ export const blockMappingImageDocx: DocsExporterDocx['mappings']['blockMapping']
}
: undefined,
transformation: {
width: previewWidth || width,
height: ((previewWidth || width) / width) * height,
width: finalWidth,
height: (finalWidth / width) * height,
},
}),
],
@@ -0,0 +1,135 @@
import React from 'react';
import { DocsExporterODT } from '../types';
import { convertSvgToPng, odtRegisterParagraphStyleForBlock } from '../utils';
const MAX_WIDTH = 600;
export const blockMappingImageODT: DocsExporterODT['mappings']['blockMapping']['image'] =
async (block, exporter) => {
try {
const blob = await exporter.resolveFile(block.props.url);
if (!blob || !blob.type) {
console.warn(`Failed to resolve image: ${block.props.url}`);
return null;
}
let pngConverted: string | undefined;
let dimensions: { width: number; height: number } | undefined;
let previewWidth = block.props.previewWidth || undefined;
if (!blob.type.includes('image')) {
console.warn(`Not an image type: ${blob.type}`);
return null;
}
if (blob.type.includes('svg')) {
const svgText = await blob.text();
const FALLBACK_SIZE = 536;
previewWidth = previewWidth || blob.size || FALLBACK_SIZE;
const result = await convertSvgToPng(svgText, previewWidth);
pngConverted = result.png;
dimensions = { width: result.width, height: result.height };
} else {
dimensions = await getImageDimensions(blob);
}
if (!dimensions) {
return null;
}
const { width, height } = dimensions;
if (previewWidth && previewWidth > MAX_WIDTH) {
previewWidth = MAX_WIDTH;
}
// Convert image to base64 for ODT embedding
const arrayBuffer = pngConverted
? await (await fetch(pngConverted)).arrayBuffer()
: await blob.arrayBuffer();
const base64 = btoa(
Array.from(new Uint8Array(arrayBuffer))
.map((byte) => String.fromCharCode(byte))
.join(''),
);
const finalWidth = previewWidth || width;
const finalHeight = ((previewWidth || width) / width) * height;
const baseParagraphProps = {
backgroundColor: block.props.backgroundColor,
textAlignment: block.props.textAlignment,
};
const paragraphStyleName = odtRegisterParagraphStyleForBlock(
exporter,
baseParagraphProps,
{ paddingCm: 0 },
);
// Convert pixels to cm (ODT uses cm for dimensions)
const widthCm = finalWidth / 37.795275591;
const heightCm = finalHeight / 37.795275591;
// Create ODT image structure using React.createElement
const frame = React.createElement(
'text:p',
{
'text:style-name': paragraphStyleName,
},
React.createElement(
'draw:frame',
{
'draw:name': `Image${Date.now()}`,
'text:anchor-type': 'as-char',
'svg:width': `${widthCm}cm`,
'svg:height': `${heightCm}cm`,
},
React.createElement(
'draw:image',
{
xlinkType: 'simple',
xlinkShow: 'embed',
xlinkActuate: 'onLoad',
},
React.createElement('office:binary-data', {}, base64),
),
),
);
// Add caption if present
if (block.props.caption) {
const captionStyleName = odtRegisterParagraphStyleForBlock(
exporter,
baseParagraphProps,
{ paddingCm: 0, parentStyleName: 'Caption' },
);
return [
frame,
React.createElement(
'text:p',
{ 'text:style-name': captionStyleName },
block.props.caption,
),
];
}
return frame;
} catch (error) {
console.error(`Error processing image for ODT export:`, error);
return null;
}
};
async function getImageDimensions(blob: Blob) {
if (typeof window !== 'undefined') {
const bmp = await createImageBitmap(blob);
const { width, height } = bmp;
bmp.close();
return { width, height };
}
}
@@ -6,12 +6,14 @@ import { convertSvgToPng } from '../utils';
const PIXELS_PER_POINT = 0.75;
const FONT_SIZE = 16;
const MAX_WIDTH = 600;
export const blockMappingImagePDF: DocsExporterPDF['mappings']['blockMapping']['image'] =
async (block, exporter) => {
const blob = await exporter.resolveFile(block.props.url);
let pngConverted: string | undefined;
let width = block.props.previewWidth || undefined;
let dimensions: { width: number; height: number } | undefined;
let previewWidth = block.props.previewWidth || undefined;
if (!blob.type.includes('image')) {
return <View wrap={false} />;
@@ -20,16 +22,33 @@ export const blockMappingImagePDF: DocsExporterPDF['mappings']['blockMapping']['
if (blob.type.includes('svg')) {
const svgText = await blob.text();
const FALLBACK_SIZE = 536;
width = width || blob.size || FALLBACK_SIZE;
pngConverted = await convertSvgToPng(svgText, width);
previewWidth = previewWidth || FALLBACK_SIZE;
const result = await convertSvgToPng(svgText, previewWidth);
pngConverted = result.png;
dimensions = { width: result.width, height: result.height };
} else {
dimensions = await getImageDimensions(blob);
}
if (!dimensions) {
return <View wrap={false} />;
}
const { width, height } = dimensions;
// Ensure the final width never exceeds MAX_WIDTH to prevent images
// from overflowing the page width in the exported document
const finalWidth = Math.min(previewWidth || width, MAX_WIDTH);
const finalHeight = (finalWidth / width) * height;
return (
<View wrap={false}>
<Image
src={pngConverted || blob}
style={{
width: width ? width * PIXELS_PER_POINT : undefined,
width: finalWidth * PIXELS_PER_POINT,
height: finalHeight * PIXELS_PER_POINT,
maxWidth: '100%',
}}
/>
@@ -38,6 +57,21 @@ export const blockMappingImagePDF: DocsExporterPDF['mappings']['blockMapping']['
);
};
async function getImageDimensions(blob: Blob) {
if (typeof window !== 'undefined') {
const url = URL.createObjectURL(blob);
const img = document.createElement('img');
img.src = url;
return new Promise<{ width: number; height: number }>((resolve) => {
img.onload = () => {
URL.revokeObjectURL(url);
resolve({ width: img.naturalWidth, height: img.naturalHeight });
};
});
}
}
function caption(
props: Partial<DefaultProps & { caption: string; previewWidth: number }>,
) {
@@ -1,11 +1,14 @@
export * from './calloutDocx';
export * from './calloutODT';
export * from './calloutPDF';
export * from './headingPDF';
export * from './imageDocx';
export * from './imageODT';
export * from './imagePDF';
export * from './paragraphPDF';
export * from './quoteDocx';
export * from './quotePDF';
export * from './tablePDF';
export * from './uploadLoaderPDF';
export * from './uploadLoaderDocx';
export * from './uploadLoaderODT';
export * from './uploadLoaderPDF';
@@ -23,8 +23,9 @@ export const blockMappingParagraphPDF: DocsExporterPDF['mappings']['blockMapping
});
}
}
return (
<Text key={block.id}>
<Text key={'paragraph' + block.id}>
{exporter.transformInlineContent(block.content)}
</Text>
);
@@ -2,7 +2,7 @@
* We use mainly the Blocknotes code, mixed with @ag-media/react-pdf-table
* to have a better Table support.
* See:
* https://github.com/TypeCellOS/BlockNote/blob/004c0bf720fe1415c497ad56449015c5f4dd7ba0/packages/xl-pdf-exporter/src/pdf/util/table/Table.tsx
* https://github.com/TypeCellOS/BlockNote/blob/main/packages/xl-pdf-exporter/src/pdf/util/table/Table.tsx
*
* We succeeded to manage the colspan, but rowspan is not supported yet.
*/
@@ -92,18 +92,27 @@ export const blockMappingTablePDF: DocsExporterPDF['mappings']['blockMapping']['
color:
cellProps.textColor === 'default'
? undefined
: options.colors[cellProps.textColor].text,
: options.colors?.[cellProps.textColor]?.text,
backgroundColor:
cellProps.backgroundColor === 'default'
? undefined
: options.colors[cellProps.backgroundColor].background,
: options.colors?.[cellProps.backgroundColor]
?.background,
textAlign: cellProps.textAlignment,
},
];
return (
<TD key={colIndex} style={arrayStyle}>
<Text style={styles.cell}>
<Text
style={[
styles.cell,
{
width: '100%',
textAlign: cellProps.textAlignment ?? 'left',
},
]}
>
{exporter.transformInlineContent(cell)}
</Text>
</TD>
@@ -0,0 +1,17 @@
import React from 'react';
import { DocsExporterODT } from '../types';
export const blockMappingUploadLoaderODT: DocsExporterODT['mappings']['blockMapping']['uploadLoader'] =
(block) => {
// Map uploadLoader to paragraph with information text
const information = block.props.information || '';
const type = block.props.type || 'loading';
const prefix = type === 'warning' ? '⚠️ ' : '⏳ ';
return React.createElement(
'text:p',
{ 'text:style-name': 'Text_20_body' },
`${prefix}${information}`,
);
};
@@ -1,4 +1,5 @@
import { DOCXExporter } from '@blocknote/xl-docx-exporter';
import { ODTExporter } from '@blocknote/xl-odt-exporter';
import { PDFExporter } from '@blocknote/xl-pdf-exporter';
import {
Button,
@@ -23,12 +24,14 @@ import { Doc, useTrans } from '@/docs/doc-management';
import { exportCorsResolveFileUrl } from '../api/exportResolveFileUrl';
import { TemplatesOrdering, useTemplates } from '../api/useTemplates';
import { docxDocsSchemaMappings } from '../mappingDocx';
import { odtDocsSchemaMappings } from '../mappingODT';
import { pdfDocsSchemaMappings } from '../mappingPDF';
import { downloadFile } from '../utils';
enum DocDownloadFormat {
PDF = 'pdf',
DOCX = 'docx',
ODT = 'odt',
}
interface ModalExportProps {
@@ -124,7 +127,7 @@ export const ModalExport = ({ onClose, doc }: ModalExportProps) => {
: rawPdfDocument;
blobExport = await pdf(pdfDocument).toBlob();
} else {
} else if (format === DocDownloadFormat.DOCX) {
const exporter = new DOCXExporter(editor.schema, docxDocsSchemaMappings, {
resolveFileUrl: async (url) => exportCorsResolveFileUrl(doc.id, url),
});
@@ -133,6 +136,16 @@ export const ModalExport = ({ onClose, doc }: ModalExportProps) => {
documentOptions: { title: documentTitle },
sectionOptions: {},
});
} else if (format === DocDownloadFormat.ODT) {
const exporter = new ODTExporter(editor.schema, odtDocsSchemaMappings, {
resolveFileUrl: async (url) => exportCorsResolveFileUrl(doc.id, url),
});
blobExport = await exporter.toODTDocument(exportDocument);
} else {
toast(t('The export failed'), VariantType.ERROR);
setIsExporting(false);
return;
}
downloadFile(blobExport, `${filename}.${format}`);
@@ -213,7 +226,7 @@ export const ModalExport = ({ onClose, doc }: ModalExportProps) => {
className="--docs--modal-export-content"
>
<Text $variation="600" $size="sm" as="p">
{t('Download your document in a .docx or .pdf format.')}
{t('Download your document in a .docx, .odt or .pdf format.')}
</Text>
<Select
clearable={false}
@@ -231,6 +244,7 @@ export const ModalExport = ({ onClose, doc }: ModalExportProps) => {
label={t('Format')}
options={[
{ label: t('Docx'), value: DocDownloadFormat.DOCX },
{ label: t('ODT'), value: DocDownloadFormat.ODT },
{ label: t('PDF'), value: DocDownloadFormat.PDF },
]}
value={format}
@@ -1,2 +1,3 @@
export * from './interlinkingLinkPDF';
export * from './interlinkingLinkDocx';
export * from './interlinkingLinkODT';
@@ -0,0 +1,23 @@
import React from 'react';
import { DocsExporterODT } from '../types';
export const inlineContentMappingInterlinkingLinkODT: DocsExporterODT['mappings']['inlineContentMapping']['interlinkingLinkInline'] =
(inline) => {
const url = window.location.origin + inline.props.url;
const title = inline.props.title;
// Create ODT hyperlink using React.createElement to avoid TypeScript JSX namespace issues
// Uses the same structure as BlockNote's default link mapping
return React.createElement(
'text:a',
{
xlinkType: 'simple',
'text:style-name': 'Internet_20_link',
'office:target-frame-name': '_top',
xlinkShow: 'replace',
xlinkHref: url,
},
`📄${title}`,
);
};
@@ -0,0 +1,33 @@
import { odtDefaultSchemaMappings } from '@blocknote/xl-odt-exporter';
import {
blockMappingCalloutODT,
blockMappingImageODT,
blockMappingUploadLoaderODT,
} from './blocks-mapping';
import { inlineContentMappingInterlinkingLinkODT } from './inline-content-mapping';
import { DocsExporterODT } from './types';
// Align default inline mappings to our editor inline schema without using `any`
const baseInlineMappings =
odtDefaultSchemaMappings.inlineContentMapping as unknown as DocsExporterODT['mappings']['inlineContentMapping'];
export const odtDocsSchemaMappings: DocsExporterODT['mappings'] = {
...odtDefaultSchemaMappings,
blockMapping: {
...odtDefaultSchemaMappings.blockMapping,
callout: blockMappingCalloutODT,
image: blockMappingImageODT,
// We're reusing the file block mapping for PDF blocks
// The types don't match exactly but the implementation is compatible
// eslint-disable-next-line @typescript-eslint/no-explicit-any
pdf: odtDefaultSchemaMappings.blockMapping.file as any,
uploadLoader: blockMappingUploadLoaderODT,
},
inlineContentMapping: {
...baseInlineMappings,
interlinkingSearchInline: () => null,
interlinkingLinkInline: inlineContentMappingInterlinkingLinkODT,
},
};
@@ -51,3 +51,13 @@ export type DocsExporterDocx = Exporter<
IRunPropertiesOptions,
TextRun
>;
export type DocsExporterODT = Exporter<
DocsBlockSchema,
DocsInlineContentSchema,
DocsStyleSchema,
React.ReactNode,
React.ReactNode,
Record<string, string>,
React.ReactNode
>;
@@ -5,6 +5,7 @@ import {
} from '@blocknote/core';
import { Canvg } from 'canvg';
import { IParagraphOptions, ShadingType } from 'docx';
import React from 'react';
export function downloadFile(blob: Blob, filename: string) {
const url = window.URL.createObjectURL(blob);
@@ -19,18 +20,21 @@ export function downloadFile(blob: Blob, filename: string) {
}
/**
* Converts an SVG string into a PNG image and returns it as a data URL.
* Converts an SVG string into a PNG image and returns it as a data URL with dimensions.
*
* This function creates a canvas, parses the SVG, calculates the appropriate height
* to preserve the aspect ratio, and renders the SVG onto the canvas using Canvg.
*
* @param {string} svgText - The raw SVG markup to convert.
* @param {number} width - The desired width of the output PNG (height is auto-calculated to preserve aspect ratio).
* @returns {Promise<string>} A Promise that resolves to a PNG image encoded as a base64 data URL.
* @returns {Promise<{ png: string; width: number; height: number }>} A Promise that resolves to an object containing the PNG data URL and its dimensions.
*
* @throws Will throw an error if the canvas context cannot be initialized.
*/
export async function convertSvgToPng(svgText: string, width: number) {
export async function convertSvgToPng(
svgText: string,
width: number,
): Promise<{ png: string; width: number; height: number }> {
// Create a canvas and render the SVG onto it
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d', {
@@ -63,7 +67,11 @@ export async function convertSvgToPng(svgText: string, width: number) {
svg.resize(width, height, true);
await svg.render();
return canvas.toDataURL('image/png');
return {
png: canvas.toDataURL('image/png'),
width,
height: height || width,
};
}
export function docxBlockPropsToStyles(
@@ -98,3 +106,76 @@ export function docxBlockPropsToStyles(
})(),
};
}
// ODT helpers
type OdtExporterLike = {
options?: { colors?: typeof COLORS_DEFAULT };
registerStyle: (fn: (name: string) => React.ReactNode) => string;
};
function isOdtExporterLike(value: unknown): value is OdtExporterLike {
return (
!!value &&
typeof (value as { registerStyle?: unknown }).registerStyle === 'function'
);
}
export function odtRegisterParagraphStyleForBlock(
exporter: unknown,
props: Partial<DefaultProps>,
options?: { paddingCm?: number; parentStyleName?: string },
) {
if (!isOdtExporterLike(exporter)) {
throw new Error('Invalid ODT exporter: missing registerStyle');
}
const colors = exporter.options?.colors;
const bgColorHex =
props.backgroundColor && props.backgroundColor !== 'default' && colors
? colors[props.backgroundColor].background
: undefined;
const textColorHex =
props.textColor && props.textColor !== 'default' && colors
? colors[props.textColor].text
: undefined;
const foTextAlign =
!props.textAlignment || props.textAlignment === 'left'
? 'start'
: props.textAlignment === 'center'
? 'center'
: props.textAlignment === 'right'
? 'end'
: 'justify';
const paddingCm = options?.paddingCm ?? 0.42; // ~1rem (16px)
const parentStyleName = options?.parentStyleName;
// registerStyle is available on ODT exporter; call through with React elements
const styleName = exporter.registerStyle((name: string) =>
React.createElement(
'style:style',
{
'style:name': name,
'style:family': 'paragraph',
...(parentStyleName
? { 'style:parent-style-name': parentStyleName }
: {}),
},
React.createElement('style:paragraph-properties', {
'fo:text-align': foTextAlign,
'fo:padding': `${paddingCm}cm`,
...(bgColorHex ? { 'fo:background-color': bgColorHex } : {}),
}),
textColorHex
? React.createElement('style:text-properties', {
'fo:color': textColorHex,
})
: undefined,
),
);
return styleName;
}
@@ -0,0 +1,46 @@
import { render, screen } from '@testing-library/react';
import React from 'react';
import { describe, expect, test, vi } from 'vitest';
import { AppWrapper } from '@/tests/utils';
// Force mobile layout so the children count is rendered
vi.mock('@/stores', () => ({
useResponsiveStore: () => ({ isDesktop: false }),
}));
// Provide stable mocks for hooks used by the component
vi.mock('../../doc-management', async () => {
const actual = await vi.importActual<any>('../../doc-management');
return {
...actual,
useTrans: () => ({ transRole: vi.fn((r) => String(r)) }),
useIsCollaborativeEditable: () => ({ isEditable: true }),
};
});
vi.mock('@/core', () => ({
useConfig: () => ({ data: {} }),
}));
vi.mock('@/hook', () => ({
useDate: () => ({
relativeDate: () => 'yesterday',
calculateDaysLeft: () => 5,
}),
}));
import { DocHeaderInfo } from '../components/DocHeaderInfo';
describe('DocHeaderInfo', () => {
test('renders the number of sub-documents when numchild is provided (mobile layout)', () => {
const doc = {
numchild: 3,
updated_at: new Date().toISOString(),
} as any;
render(<DocHeaderInfo doc={doc} />, { wrapper: AppWrapper });
expect(screen.getByText(/Contains 3 sub-documents/i)).toBeInTheDocument();
});
});
@@ -44,7 +44,7 @@ describe('DocToolBox - Licence', () => {
await userEvent.click(optionsButton);
expect(
await screen.findByText(
'Download your document in a .docx or .pdf format.',
'Download your document in a .docx, .odt or .pdf format.',
),
).toBeInTheDocument();
}, 10000);
@@ -1,23 +1,20 @@
import { useTranslation } from 'react-i18next';
import { Box, HorizontalSeparator, Text } from '@/components';
import { useConfig } from '@/core';
import { Box, HorizontalSeparator } from '@/components';
import { useCunninghamTheme } from '@/cunningham';
import {
Doc,
LinkReach,
Role,
getDocLinkReach,
useIsCollaborativeEditable,
useTrans,
} from '@/docs/doc-management';
import { useDate } from '@/hook';
import { useResponsiveStore } from '@/stores';
import { AlertNetwork } from './AlertNetwork';
import { AlertPublic } from './AlertPublic';
import { AlertRestore } from './AlertRestore';
import { BoutonShare } from './BoutonShare';
import { DocHeaderInfo } from './DocHeaderInfo';
import { DocTitle } from './DocTitle';
import { DocToolBox } from './DocToolBox';
@@ -29,27 +26,11 @@ export const DocHeader = ({ doc }: DocHeaderProps) => {
const { spacingsTokens } = useCunninghamTheme();
const { isDesktop } = useResponsiveStore();
const { t } = useTranslation();
const { transRole } = useTrans();
const { isEditable } = useIsCollaborativeEditable(doc);
const docIsPublic = getDocLinkReach(doc) === LinkReach.PUBLIC;
const docIsAuth = getDocLinkReach(doc) === LinkReach.AUTHENTICATED;
const { relativeDate, calculateDaysLeft } = useDate();
const { data: config } = useConfig();
const isDeletedDoc = !!doc.deleted_at;
let dateToDisplay = t('Last update: {{update}}', {
update: relativeDate(doc.updated_at),
});
if (config?.TRASHBIN_CUTOFF_DAYS && doc.deleted_at) {
const daysLeft = calculateDaysLeft(
doc.deleted_at,
config.TRASHBIN_CUTOFF_DAYS,
);
dateToDisplay = `${t('Days remaining:')} ${daysLeft} ${t('days', { count: daysLeft })}`;
}
return (
<>
<Box
@@ -80,33 +61,8 @@ export const DocHeader = ({ doc }: DocHeaderProps) => {
>
<Box $gap={spacingsTokens['3xs']} $overflow="auto">
<DocTitle doc={doc} />
<Box $direction="row">
{isDesktop && (
<>
<Text
$variation="600"
$size="s"
$weight="bold"
$theme={isEditable ? 'greyscale' : 'warning'}
>
{transRole(
isEditable
? doc.user_role || doc.link_role
: Role.READER,
)}
&nbsp;·&nbsp;
</Text>
<Text $variation="600" $size="s">
{dateToDisplay}
</Text>
</>
)}
{!isDesktop && (
<Text $variation="400" $size="s">
{dateToDisplay}
</Text>
)}
<DocHeaderInfo doc={doc} />
</Box>
</Box>
{!isDeletedDoc && <DocToolBox doc={doc} />}
@@ -0,0 +1,80 @@
import { t } from 'i18next';
import React from 'react';
import { Text } from '@/components';
import { useConfig } from '@/core';
import { useDate } from '@/hook';
import { useResponsiveStore } from '@/stores';
import {
Doc,
Role,
useIsCollaborativeEditable,
useTrans,
} from '../../doc-management';
interface DocHeaderInfoProps {
doc: Doc;
}
export const DocHeaderInfo = ({ doc }: DocHeaderInfoProps) => {
const { isDesktop } = useResponsiveStore();
const { transRole } = useTrans();
const { isEditable } = useIsCollaborativeEditable(doc);
const { relativeDate, calculateDaysLeft } = useDate();
const { data: config } = useConfig();
const childrenCount = doc.numchild ?? 0;
const relativeOnly = relativeDate(doc.updated_at);
let dateToDisplay = t('Last update: {{update}}', {
update: relativeOnly,
});
if (config?.TRASHBIN_CUTOFF_DAYS && doc.deleted_at) {
const daysLeft = calculateDaysLeft(
doc.deleted_at,
config.TRASHBIN_CUTOFF_DAYS,
);
dateToDisplay = `${t('Days remaining:')} ${daysLeft} ${t('days', { count: daysLeft })}`;
}
const hasChildren = childrenCount > 0;
if (isDesktop) {
return (
<>
<Text
$variation="600"
$size="s"
$weight="bold"
$theme={isEditable ? 'greyscale' : 'warning'}
>
{transRole(isEditable ? doc.user_role || doc.link_role : Role.READER)}
&nbsp;·&nbsp;
</Text>
<Text $variation="600" $size="s">
{dateToDisplay}
</Text>
</>
);
}
return (
<>
<Text $variation="400" $size="s">
{hasChildren ? relativeOnly : dateToDisplay}
</Text>
{hasChildren && (
<Text $variation="400" $size="s">
&nbsp;&nbsp;
{t('Contains {{count}} sub-documents', {
count: childrenCount,
})}
</Text>
)}
</>
);
};
@@ -1,16 +1,19 @@
import {
Button,
ButtonElement,
Modal,
ModalSize,
VariantType,
useToastProvider,
} from '@openfun/cunningham-react';
import { useRouter } from 'next/router';
import { useEffect, useRef } from 'react';
import { Trans, useTranslation } from 'react-i18next';
import { Box, ButtonCloseModal, Text, TextErrors } from '@/components';
import { useConfig } from '@/core';
import { KEY_LIST_DOC_TRASHBIN } from '@/docs/docs-grid';
import { useKeyboardAction } from '@/hooks';
import { KEY_LIST_DOC } from '../api/useDocs';
import { useRemoveDoc } from '../api/useRemoveDoc';
@@ -34,6 +37,7 @@ export const ModalRemoveDoc = ({
const trashBinCutoffDays = config?.TRASHBIN_CUTOFF_DAYS || 30;
const { push } = useRouter();
const { hasChildren } = useDocUtils(doc);
const cancelButtonRef = useRef<ButtonElement>(null);
const {
mutate: removeDoc,
isError,
@@ -57,20 +61,47 @@ export const ModalRemoveDoc = ({
},
});
useEffect(() => {
const TIMEOUT_MODAL_MOUNTING = 100;
const timeoutId = setTimeout(() => {
const buttonElement = cancelButtonRef.current;
if (buttonElement) {
buttonElement.focus();
}
}, TIMEOUT_MODAL_MOUNTING);
return () => clearTimeout(timeoutId);
}, []);
const keyboardAction = useKeyboardAction();
const handleClose = () => {
onClose();
};
const handleDelete = () => {
removeDoc({ docId: doc.id });
};
const handleCloseKeyDown = keyboardAction(handleClose);
const handleDeleteKeyDown = keyboardAction(handleDelete);
return (
<Modal
isOpen
closeOnClickOutside
hideCloseButton
onClose={() => onClose()}
onClose={handleClose}
aria-describedby="modal-remove-doc-title"
rightActions={
<>
<Button
ref={cancelButtonRef}
aria-label={t('Cancel the deletion')}
color="secondary"
fullWidth
onClick={() => onClose()}
onClick={handleClose}
onKeyDown={handleCloseKeyDown}
>
{t('Cancel')}
</Button>
@@ -78,11 +109,8 @@ export const ModalRemoveDoc = ({
aria-label={t('Delete document')}
color="danger"
fullWidth
onClick={() =>
removeDoc({
docId: doc.id,
})
}
onClick={handleDelete}
onKeyDown={handleDeleteKeyDown}
>
{t('Delete')}
</Button>
@@ -108,7 +136,8 @@ export const ModalRemoveDoc = ({
</Text>
<ButtonCloseModal
aria-label={t('Close the delete modal')}
onClick={() => onClose()}
onClick={handleClose}
onKeyDown={handleCloseKeyDown}
/>
</Box>
}
@@ -132,5 +132,6 @@ export interface AccessRequest {
partial_update: boolean;
retrieve: boolean;
accept: boolean;
set_role_to: Role[];
};
}
@@ -90,12 +90,14 @@ export const DocRoleDropdown = ({
const roles: DropdownMenuOption[] = Object.keys(translatedRoles).map(
(key, index) => {
const isLast = index === Object.keys(translatedRoles).length - 1;
const isRoleAllowed = rolesAllowed?.includes(key as Role) ?? true;
return {
label: transRole(key as Role),
callback: () => onSelectRole?.(key as Role),
isSelected: currentRole === (key as Role),
showSeparator: isLast,
disabled: isLastOwner && key !== 'owner',
disabled: (isLastOwner && key !== 'owner') || !isRoleAllowed,
};
},
);
@@ -77,6 +77,7 @@ const DocShareAccessRequestItem = ({ doc, accessRequest }: Props) => {
currentRole={role}
onSelectRole={setRole}
canUpdate={doc.abilities.accesses_manage}
rolesAllowed={accessRequest.abilities.set_role_to}
/>
<Button
color="tertiary"
@@ -1,4 +1,5 @@
import { useState } from 'react';
import { css } from 'styled-components';
import { BoxButton, Text } from '@/components';
import { useCunninghamTheme } from '@/cunningham';
@@ -40,7 +41,6 @@ export const Heading = ({
<BoxButton
id={`heading-${headingId}`}
$width="100%"
key={headingId}
onMouseOver={() => setIsHover(true)}
onMouseLeave={() => setIsHover(false)}
onClick={() => {
@@ -59,8 +59,19 @@ export const Heading = ({
}}
$radius="4px"
$background={isActive ? `${colorsTokens['greyscale-100']}` : 'none'}
$css="text-align: left;"
$css={css`
text-align: left;
&:focus-visible {
/* Scoped focus style: same footprint as hover, with theme shadow */
outline: none;
box-shadow: 0 0 0 2px ${colorsTokens['primary-400']};
border-radius: 4px;
}
`}
className="--docs--table-content-heading"
aria-label={text}
aria-selected={isHighlight}
aria-current={isHighlight ? 'true' : undefined}
>
<Text
$width="100%"
@@ -12,7 +12,7 @@ import { Heading } from './Heading';
export const TableContent = () => {
const { headings } = useHeadingStore();
const { editor } = useEditorStore();
const { spacingsTokens } = useCunninghamTheme();
const { spacingsTokens, colorsTokens } = useCunninghamTheme();
const [headingIdHighlight, setHeadingIdHighlight] = useState<string>();
@@ -99,33 +99,63 @@ export const TableContent = () => {
return (
<Box
as="nav"
id="summaryContainer"
$width={!isHover ? '40px' : '200px'}
$height={!isHover ? '40px' : 'auto'}
$maxHeight="calc(50vh - 60px)"
$zIndex={1000}
$align="center"
$padding="xs"
$padding={isHover ? 'xs' : '0'}
$justify="center"
$position="sticky"
aria-label={t('Summary')}
$css={css`
border: 1px solid #ccc;
top: 0;
border: 1px solid ${colorsTokens['greyscale-300']};
overflow: hidden;
border-radius: var(--c--theme--spacings--3xs);
background: var(--c--theme--colors--greyscale-000);
border-radius: ${spacingsTokens['3xs']};
background: ${colorsTokens['greyscale-000']};
${isHover &&
css`
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: flex-start;
gap: var(--c--theme--spacings--2xs);
gap: ${spacingsTokens['2xs']};
`}
`}
className="--docs--table-content"
>
{!isHover && (
<BoxButton onClick={onOpen} $justify="center" $align="center">
<Icon iconName="list" $theme="primary" $variation="800" />
<BoxButton
onClick={onOpen}
$width="100%"
$height="100%"
$justify="center"
$align="center"
aria-label={t('Summary')}
aria-expanded={isHover}
aria-controls="toc-list"
$css={css`
&:hover {
background: ${colorsTokens['primary-100']};
}
&:focus-visible {
outline: none;
box-shadow: 0 0 0 4px ${colorsTokens['primary-400']};
background: ${colorsTokens['primary-100']};
width: 90%;
height: 90%;
}
`}
>
<Icon
iconName="list"
$theme="primary"
$variation="800"
variant="symbols-outlined"
/>
</BoxButton>
)}
{isHover && (
@@ -134,10 +164,11 @@ export const TableContent = () => {
$overflow="hidden"
$css={css`
user-select: none;
padding: ${spacingsTokens['4xs']};
`}
>
<Box
$margin={{ bottom: '10px' }}
$margin={{ bottom: spacingsTokens.xs }}
$direction="row"
$justify="space-between"
$align="center"
@@ -149,30 +180,46 @@ export const TableContent = () => {
onClick={onClose}
$justify="center"
$align="center"
aria-label={t('Summary')}
aria-expanded={isHover}
aria-controls="toc-list"
$css={css`
transition: none !important;
transform: rotate(180deg);
&:focus-visible {
outline: none;
box-shadow: 0 0 0 2px ${colorsTokens['primary-400']};
border-radius: 4px;
}
`}
>
<Icon iconName="menu_open" $theme="primary" $variation="800" />
</BoxButton>
</Box>
<Box
as="ul"
id="toc-list"
role="list"
$gap={spacingsTokens['3xs']}
$css={css`
overflow-y: auto;
list-style: none;
padding: ${spacingsTokens['3xs']};
margin: 0;
`}
>
{headings?.map(
(heading) =>
heading.contentText && (
<Heading
editor={editor}
headingId={heading.id}
level={heading.props.level}
text={heading.contentText}
key={heading.id}
isHighlight={headingIdHighlight === heading.id}
/>
<Box as="li" role="listitem" key={heading.id}>
<Heading
editor={editor}
headingId={heading.id}
level={heading.props.level}
text={heading.contentText}
isHighlight={headingIdHighlight === heading.id}
/>
</Box>
),
)}
</Box>
@@ -152,23 +152,23 @@ export const DocsGrid = ({
<DocGridContentList docs={docs} />
)}
</Box>
{hasNextPage && !loading && (
<InView
data-testid="infinite-scroll-trigger"
as="div"
onChange={loadMore}
>
{!isFetching && hasNextPage && (
<Button
onClick={() => void fetchNextPage()}
color="primary-text"
>
{t('More docs')}
</Button>
)}
</InView>
)}
</Box>
{hasNextPage && !loading && (
<InView
data-testid="infinite-scroll-trigger"
as="div"
onChange={loadMore}
>
{!isFetching && hasNextPage && (
<Button
onClick={() => void fetchNextPage()}
color="primary-text"
>
{t('More docs')}
</Button>
)}
</InView>
)}
</Box>
)}
</Card>
@@ -19,7 +19,7 @@ export const Draggable = <T,>(props: DraggableProps<T>) => {
{...attributes}
data-testid={`draggable-doc-${props.id}`}
className="--docs--grid-draggable"
role="presentation"
role="none"
>
{props.children}
</div>
@@ -35,7 +35,7 @@ export const Droppable = ({
<Box
ref={setNodeRef}
data-testid={`droppable-doc-${id}`}
role="presentation"
role="none"
$css={css`
border-radius: 4px;
background-color: ${enableHover
@@ -0,0 +1 @@
export * from './useKeyboardAction';
@@ -0,0 +1,22 @@
import { KeyboardEvent, useCallback } from 'react';
type KeyboardActionCallback = () => void | Promise<unknown>;
type KeyboardActionHandler = (event: KeyboardEvent<HTMLElement>) => void;
/**
* Hook to create keyboard handlers that trigger the provided callback
* when the user presses Enter or Space.
*/
export const useKeyboardAction = () => {
return useCallback(
(callback: KeyboardActionCallback): KeyboardActionHandler =>
(event: KeyboardEvent<HTMLElement>) => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
event.stopPropagation();
void callback();
}
},
[],
);
};
@@ -77,7 +77,6 @@
"Docx": "Docx",
"Download": "Pellgargañ",
"Download anyway": "Pellgargañ memestra",
"Download your document in a .docx or .pdf format.": "Pellgargañ ho restr dindan ur stumm .docx pe .pdf.",
"Duplicate": "Eilañ",
"Editing": "Oc'h aozañ",
"Editor": "Embanner",
@@ -244,16 +243,25 @@
"translation": {
"\"{{email}}\" is already invited to the document.": "\"{{email}}\" ist bereits zum Dokument eingeladen.",
"\"{{email}}\" is already member of the document.": "\"{{email}}\" ist bereits Mitglied des Dokuments.",
"401 Unauthorized": "401 Nicht autorisiert",
"A new way to organize knowledge.": "Wissen organisieren: mal ganz anders.",
"AI Actions": "KI-Aktionen",
"AI seems busy! Please try again.": "KI scheint beschäftigt! Bitte versuchen Sie es erneut.",
"Access Denied - Error 403": "Zugriff verweigert - Fehler 403",
"Access Requests": "Zugriffsanfragen",
"Access request sent successfully.": "Zugriffsanfrage erfolgreich versendet.",
"Accessible to anyone": "Für alle zugänglich",
"Accessible to authenticated users": "Für authentifizierte Benutzer zugänglich",
"Actions for {{title}}": "Aktionen für {{title}}",
"Add": "Hinzufügen",
"Add PDF": "PDF hinzufügen",
"Add a callout block": "Hinweis-Block hinzufügen",
"Add a sub page": "Eine Unterseite hinzufügen",
"Add emoji": "Dokumenten-Symbol hinzufügen",
"Administrator": "Administrator",
"Alert deleted document": "Warnmeldung bei gelöschtem Dokument",
"All docs": "Alle Dokumente",
"An error occurred while restoring the document: {{error}}": "Fehler beim Wiederherstellen des Dokuments: {{error}}",
"An uncompromising writing experience.": "Ein kompromissloses Schreiberlebnis.",
"Analyzing file...": "Analysiere Datei...",
"Anonymous": "Gast",
@@ -262,26 +270,46 @@
"Anyone with the link can see the document": "Jeder mit dem Link kann das Dokument ansehen",
"Anyone with the link can view the document if they are logged in": "Jeder mit dem Link kann das Dokument ansehen, wenn er angemeldet ist",
"Approve": "Freigeben",
"As this is a sub-document, please request access to the parent document to enable these features.": "Dies ist ein untergeordnetes Dokument, fordern Sie bitte den Zugriff auf das übergeordnete Dokument an, um diese Funktionen zu ermöglichen.",
"Available soon": "Bald verfügbar",
"Back to homepage": "Zurück zur Startseite",
"Banner image": "Bannerbild",
"Beautify": "Verschönern",
"By moving this document to <strong>{{targetDocumentTitle}}</strong>, it will lose its current access rights and inherit the permissions of that document. <strong>This access change cannot be undone.</strong>": "Wenn Sie dieses Dokument unterordnen, übernimmt es die Zugriffsrechte von <strong>{{targetDocumentTitle}}</strong>. <strong>Diese Änderung kann nicht rückgängig gemacht werden.</strong>",
"Callout": "Hinweis",
"Can't load this page, please check your internet connection.": "Diese Seite kann nicht geladen werden. Bitte überprüfen Sie Ihre Internetverbindung.",
"Cancel": "Abbrechen",
"Cancel the deletion": "Löschvorgang abbrechen",
"Cancel the download": "Download abbrechen",
"Close the access request modal": "Zugriffsanforderungsfenster schließen",
"Close the delete modal": "Lösch-Fenster schließen",
"Close the download modal": "Export-Fenster schließen",
"Close the search modal": "Such-Fenster schließen",
"Close the share modal": "Teilen-Fenster schließen",
"Close the version history modal": "Versionsverlauf-Fenster schließen",
"Collaborate": "Zusammenarbeiten",
"Collaborate and write in real time, without layout constraints.": "In Echtzeit und ohne Layout-Beschränkungen schreiben und zusammenarbeiten.",
"Collaborative writing, Simplified.": "Kollaboratives Schreiben, vereinfacht.",
"Confirm": "Bestätigen",
"Connected": "Angemeldet",
"Contains {{count}} sub-documents_many": "Enthält {{count}} Unter-Dokumente",
"Contains {{count}} sub-documents_one": "Enthält {{count}} Unter-Dokument",
"Contains {{count}} sub-documents_other": "Enthält {{count}} Unter-Dokumente",
"Content modal to explain why the user cannot edit": "Fenster um zu erklären, warum der Benutzer nicht bearbeiten kann",
"Content modal to export the document": "Inhalte zum Exportieren des Dokuments",
"Convert Markdown": "Markdown konvertieren",
"Copied to clipboard": "In die Zwischenablage kopiert",
"Copy as {{format}}": "Als {{format}} kopieren",
"Copy link": "Link kopieren",
"Correct": "Korrigieren",
"Create a new sub-doc": "Erzeuge ein neues Unter-Dokument",
"Current doc": "Aktuelles Dokument",
"Days remaining": "Tage verbleibend",
"Days remaining:": "Tage verbleibend:",
"Delete": "Löschen",
"Delete a doc": "Dokument löschen",
"Delete document": "Dokument löschen",
"Delete sub-document": "Unter-Dokument löschen",
"Doc visibility card": "Dokumenten-Sichtbarkeitskarte",
"Docs": "Docs",
"Docs Logo": "Docs-Logo",
@@ -290,24 +318,39 @@
"Docs offers an intuitive writing experience. Its minimalist interface favors content over layout, while offering the essentials: media import, offline mode and keyboard shortcuts for greater efficiency.": "Docs bietet ein intuitives Schreiberlebnis. Seine minimalistische Oberfläche bevorzugt Inhalte über Layout, bietet aber das Wesentliche: Medien-Import, Offline-Modus und Tastaturkürzel für mehr Effizienz.",
"Docs transforms your documents into knowledge bases thanks to subpages, powerful search and the ability to pin your important documents.": "Dank Unterseiten, leistungsstarker Suche und der Möglichkeit, wichtige Dokumente zu fixieren, verwandelt Docs Ihre Dokumente in Wissensdatenbanken.",
"Docs: Your new companion to collaborate on documents efficiently, intuitively, and securely.": "Docs: Ihr neuer Begleiter für eine effiziente, intuitive und sichere Zusammenarbeit bei Dokumenten.",
"Document access mode": "Zugriffsrechte",
"Document accessible to any connected person": "Dokument für jeden angemeldeten Benutzer zugänglich",
"Document deleted": "Dokument wurde gelöscht",
"Document duplicated successfully!": "Dokument erfolgreich dupliziert!",
"Document editor": "Dokumenten-Editor",
"Document owner": "Besitzer des Dokuments",
"Document role text": "Rolle",
"Document sections": "Linke Seitenleiste",
"Document title": "Titel des Dokuments",
"Document tree": "Dokumentenbaum",
"Document version viewer": "Dokumentenversion-Betrachter",
"Document visibility": "Sichtbarkeit",
"Documents grid": "Dokumentenübersicht",
"Docx": "Docx",
"Download": "Herunterladen",
"Download anyway": "Trotzdem herunterladen",
"Download your document in a .docx or .pdf format.": "Ihr Dokument als .docx- oder .pdf-Datei herunterladen.",
"Download your document in a .docx, .odt or .pdf format.": "Dokument als DOCX-, ODT- oder PDF-Datei exportieren.",
"Drag and drop status": "Drag and Drop Status",
"Duplicate": "Duplizieren",
"Edit document emoji": "Dokumenten-Symbol bearbeiten",
"Editing": "Bearbeiten",
"Editor": "Mitbearbeiter",
"Editor unavailable": "Editor nicht verfügbar",
"Embed a PDF file": "Eine PDF-Datei einbetten",
"Emojify": "Emojifizieren",
"Empty template": "Leere Vorlage",
"Error during delete invitation": "Fehler beim Löschen der Einladung",
"Error during update invitation": "Fehler beim Aktualisieren der Einladung",
"Error while deleting invitation": "Fehler beim Löschen der Einladung",
"Error while removing the request.": "Fehler beim Zurückziehen der Anfrage.",
"Error while updating the member role.": "Fehler beim Aktualisieren der Mitglieds-Rolle.",
"Export": "Exportieren",
"Export the document": "Dokument exportieren",
"Failed to add the member in the document.": "Fehler beim Hinzufügen des Mitglieds zum Dokument.",
"Failed to copy link": "Link konnte nicht kopiert werden",
"Failed to copy to clipboard": "Fehler beim Kopieren in die Zwischenablage",
@@ -320,8 +363,10 @@
"Home": "Start",
"I understand": "Verstanden",
"If a member is editing, his works can be lost.": "Wenn ein Mitglied editiert, können seine Änderungen verloren gehen.",
"If you wish to be able to co-edit in real-time, contact your Information Systems Security Manager about allowing WebSockets.": "Wenn Sie an der kollaborativen Mitarbeit teilhaben möchten, wenden Sie sich an Ihren IT-Support um Web Sockets zuzulassen.",
"Illustration": "Abbildung",
"Image 403": "Bild 403",
"Image: {{title}}": "Abbildung: {{title}}",
"Insufficient access rights to view the document.": "Unzureichende Zugriffsrechte zur Ansicht des Dokuments.",
"Invite": "Einladen",
"It is the card information about the document.": "Es handelt sich um die Karteninformationen zum Dokument.",
@@ -331,32 +376,50 @@
"Last update: {{update}}": "Zuletzt aktualisiert: {{update}}",
"Learn more": "Mehr erfahren",
"Link Copied !": "Link kopiert!",
"Link a doc": "Dokument verlinken",
"Link settings": "Link-Einstellungen",
"Link this doc to another doc": "Verlinke dieses Dokument mit einem anderen Dokument",
"Links": "Links",
"List invitation card": "Einladungsliste anzeigen",
"List members card": "Mitgliederliste anzeigen",
"List request access card": "Karte mit einer Liste der Zugriffsanfragen",
"List search user result card": "Ergebnis der Benutzersuche",
"Load more": "Mehr laden",
"Log in to access the document.": "Zum Zugriff Anmeldung erforderlich.",
"Login": "Anmelden",
"Logo": "Logo",
"Logout": "Abmelden",
"Main content": "Hauptinhalt",
"Modal confirmation to download the attachment": "Modale Bestätigung zum Herunterladen des Anhangs",
"More docs": "Weitere Dokumente",
"More options": "Mehr Einstellungen",
"Move": "Verschieben",
"Move document": "Dokument verschieben",
"Move to my docs": "In \"Meine Dokumente\" verschieben",
"My docs": "Meine Dokumente",
"Name": "Name",
"New doc": "Neues Dok",
"New doc": "Neues Dokument",
"New sub-doc": "Neues Unter-Dokument",
"No active search": "Keine aktive Suche",
"No document found": "Kein Dokument gefunden",
"No documents found": "Keine Dokumente gefunden",
"No text selected": "Kein Text ausgewählt",
"No versions": "Keine Versionen",
"ODT": "ODT",
"OK": "OK",
"Offline ?!": "Offline?!",
"Only invited people can access": "Nur eingeladene Personen haben Zugriff",
"Open Source": "Open Source",
"Open document {{title}}": "Öffne Dokument: {{title}}",
"Open document: {{title}}": "Öffne Dokument: {{title}}",
"Open invitation actions menu": "Öffne Freigabemenü",
"Open root document": "Öffne Wurzel-Dokument",
"Open the document options": "Öffnen Sie die Dokumentoptionen",
"Open the header menu": "Öffne das Kopfzeilen-Menü",
"Open the menu of actions for the document: {{title}}": "Öffne das Aktionsmenü für das Dokument: {{title}}",
"Open the sharing settings for the document": "Öffne die Freigabeeinstellungen für das Dokument",
"Organize": "Organisieren",
"Others are editing this document. Unfortunately your network blocks WebSockets, the technology enabling real-time co-editing.": "Andere bearbeiten dieses Dokument. Leider blockieren Sie Web Sockets, die Technologie, die kollaborative Mitarbeit ermöglicht.",
"Others are editing. Your network prevent changes.": "Ihre Änderung konnte nicht übernommen werden, da andere Benutzer diesen Bereich zurzeit bearbeiten.",
"Owner": "Besitzer",
"PDF": "PDF",
@@ -374,17 +437,26 @@
"Reader": "Leser",
"Reading": "Lesen",
"Remove access": "Zugriff entziehen",
"Remove emoji": "Dokumenten-Symbol entfernen",
"Rename": "Umbenennen",
"Rephrase": "Umformulieren",
"Request access": "Zugriff anfragen",
"Reset": "Zurücksetzen",
"Restore": "Wiederherstellen",
"Root document {{title}}": "Hauptdokument {{title}}",
"Search": "Suchen",
"Search by title": "Nach Titel suchen",
"Search docs": "Dokumente durchsuchen",
"Search modal": "Suche Modal",
"Search results": "Suchergebnisse",
"Search user result": "Suchergebnis",
"Select a doc": "Dokument auswählen",
"Select a document": "Dokument auswählen",
"Select a version on the right to restore": "Wählen Sie rechts eine Version zum Wiederherstellen aus",
"Select language": "Sprache wählen",
"Share": "Teilen",
"Share button": "Teilen",
"Share modal content": "Teilen-Fenster Inhalt",
"Share the document": "Dokument teilen",
"Share with {{count}} users_many": "Teilen mit {{count}} Benutzern",
"Share with {{count}} users_one": "Mit {{count}} Benutzern teilen",
@@ -395,6 +467,7 @@
"Shared with {{count}} users_other": "Geteilt mit {{count}} Benutzern",
"Show more": "Mehr zeigen",
"Simple and secure collaboration.": "Einfache und sichere Zusammenarbeit.",
"Simple document icon": "Standard Dokumenten-Symbol",
"Something bad happens, please retry.": "Etwas ist schiefgelaufen, bitte versuchen Sie es erneut.",
"Start Writing": "Beginne das Schreiben",
"Summarize": "Zusammenfassen",
@@ -402,18 +475,26 @@
"Template": "Vorlage",
"The antivirus has detected an anomaly in your file.": "Das Antivirus hat eine Anomalie in Ihrer Datei festgestellt.",
"The document has been deleted.": "Das Dokument wurde gelöscht.",
"The document has been restored.": "Das Dokument wurde wiederhergestellt.",
"The document visibility has been updated.": "Die Sichtbarkeit des Dokuments wurde aktualisiert.",
"The export failed": "Export fehlgeschlagen",
"The link sharing rules differ from the parent document": "Die Linkfreigabe unterscheidet sich von der des übergeordneten Dokuments",
"This document and <strong>any sub-documents</strong> will be placed in the trashbin. You can restore it within {{days}} days.": "Dieses und <strong>alle untergeordneten Dokumente</strong> werden in den Papierkorb gelegt. Sie können es innerhalb der nächsten {{days}} Tage wiederherstellen.",
"This document will be placed in the trashbin. You can restore it within {{days}} days.": "Dieses Dokument wird in den Papierkorb gelegt. Sie können es innerhalb der nächsten {{days}} Tage wiederherstellen.",
"This file is flagged as unsafe.": "Diese Datei wurde als unsicher markiert.",
"This means you can't edit until others leave.": "Dies bedeutet, dass Sie nicht bearbeiten können, bis andere die Bearbeitung verlassen.",
"This user has access inherited from a parent page.": "Dieser Benutzer erbt den Zugriff von einem übergeordneten Dokument.",
"To facilitate the circulation of documents, Docs allows you to export your content to the most common formats: PDF, Word or OpenDocument.": "Docs erleichtert die Verbreitung von Dokumenten, da es den Export in die gängigsten Formate unterstützt: PDF, Word und OpenDocument.",
"Too many requests. Please wait 60 seconds.": "Zu viele Anfragen. Bitte warten Sie 60 Sekunden.",
"Trashbin": "Papierkorb",
"Type a name or email": "Geben Sie einen Namen oder eine E-Mail-Adresse ein",
"Type the name of a document": "Geben Sie den Namen eines Dokuments ein",
"Unnamed document": "Unbenanntes Dokument",
"Unpin": "Lösen",
"Untitled document": "Unbenanntes Dokument",
"Updated": "Aktualisiert",
"Updated at": "Aktualisiert am",
"Upload PDF": "PDF hochladen",
"Use as prompt": "Als Prompt verwenden",
"Version history": "Versionsverlauf",
"Version restored successfully": "Version erfolgreich wiederhergestellt",
@@ -421,17 +502,29 @@
"Why you can't edit the document?": "Warum können Sie dieses Dokument nicht bearbeiten?",
"Write": "Schreiben",
"You are the sole owner of this group, make another member the group owner before you can change your own role or be removed from your document.": "Sie sind der einzige Besitzer dieser Gruppe. Machen Sie ein anderes Mitglied zum Gruppenbesitzer, bevor Sie Ihre eigene Rolle ändern oder aus Ihrem Dokument entfernen können.",
"You can view this document but need additional access to see its members or modify settings.": "Sie können dieses Dokument lesen, benötigen aber zusätzliche Rechte, um Mitglieder zu sehen oder Einstellungen zu ändern.",
"You cannot restrict access to a subpage relative to its parent page.": "Unterseiten können keine strengeren Zugriffsbeschränkungen haben als übergeordnete Seiten.",
"You must be at least the administrator of the target document": "Sie müssen dafür mindestens die Rolle \"Administrator\" haben",
"You must be the owner to move the document": "Sie müssen Besitzer des Dokuments sein, um es zu verschieben",
"You're currently viewing a sub-document. To gain access, please request permission from the main document.": "Sie sehen derzeit ein untergeordnetes Dokument. Um Zugriff zu erhalten, fordern Sie bitte Berechtigung am Wurzeldokument an.",
"Your access request for this document is pending.": "Ihre Zugriffsanfrage für dieses Dokument steht noch aus.",
"Your current document will revert to this version.": "Ihr aktuelles Dokument wird auf diese Version zurückgesetzt.",
"Your {{format}} was downloaded succesfully": "Ihr {{format}} wurde erfolgreich heruntergeladen",
"days_many": "Tage",
"days_one": "Tag",
"days_other": "Tage",
"document": "Dokument",
"embed": "Einbetten",
"file": "Datei",
"home-content-open-source-part1": "Doms ist auf <2>Django Rest Framework</2> und <6>Next.js</6> aufgebaut. Wir verwenden auch <9>Yjs</9> und <13>BlockNote.js</13>, zwei Projekte, die wir mit Stolz sponsern.",
"home-content-open-source-part2": "Sie können Docs ganz einfach selbst hosten (lesen Sie unsere <2>Installationsdokumentation</2>).<br/>Docs verwendet eine <7>Lizenz</7> (MIT), die auf Innovation und Unternehmen zugeschnitten ist.<br/>Beiträge sind willkommen (lesen Sie unsere Roadmap <13>hier</13>).\n",
"home-content-open-source-part3": "Docs ist das Ergebnis einer gemeinsamen Anstrengung, die von der französischen Regierung 🇫🇷🥖 <1>(DINUM)</1> und der deutschen Regierung 🇩🇪🥨 <5>(ZenDiS)</5> geleitet wurde. “, „home-content-open-source-part3“: „Docs ist das Ergebnis einer gemeinsamen Anstrengung, die von der französischen Regierung 🇫🇷🥖 <1>(DINUM)</1> und der deutschen Regierung 🇩🇪🥨 <5>(ZenDiS)</5> geleitet wird."
"home-content-open-source-part3": "Docs ist das Ergebnis einer gemeinsamen Anstrengung, die von der französischen Regierung 🇫🇷🥖 <1>(DINUM)</1> und der deutschen Regierung 🇩🇪🥨 <5>(ZenDiS)</5> geleitet wurde. “, „home-content-open-source-part3“: „Docs ist das Ergebnis einer gemeinsamen Anstrengung, die von der französischen Regierung 🇫🇷🥖 <1>(DINUM)</1> und der deutschen Regierung 🇩🇪🥨 <5>(ZenDiS)</5> geleitet wird.",
"pdf": "pdf"
}
},
"en": {
"translation": {
"Contains {{count}} sub-documents_one": "Contains {{count}} sub-document",
"Share with {{count}} users_one": "Share with {{count}} user",
"Shared with {{count}} users_many": "Shared with {{count}} users",
"Shared with {{count}} users_one": "Shared with {{count}} user",
@@ -494,7 +587,6 @@
"Docx": "Docx",
"Download": "Descargar",
"Download anyway": "Descargar de todos modos",
"Download your document in a .docx or .pdf format.": "Descargue su documento en formato .docx o .pdf.",
"Editor": "Editor",
"Editor unavailable": "Editor no disponible",
"Emojify": "Emojizar",
@@ -661,6 +753,9 @@
"Collaborative writing, Simplified.": "L'écriture collaborative simplifiée.",
"Confirm": "Confirmez",
"Connected": "Connecté",
"Contains {{count}} sub-documents_many": "Contient {{count}} sous-documents",
"Contains {{count}} sub-documents_one": "Contient {{count}} sous-document",
"Contains {{count}} sub-documents_other": "Contient {{count}} sous-documents",
"Content modal to explain why the user cannot edit": "Contenu modal pour expliquer pourquoi l'utilisateur ne peut pas modifier",
"Content modal to export the document": "Contenu modal pour exporter le document",
"Convert Markdown": "Convertir le Markdown",
@@ -688,17 +783,19 @@
"Document accessible to any connected person": "Document accessible à toute personne connectée",
"Document deleted": "Document supprimé",
"Document duplicated successfully!": "Document dupliqué avec succès !",
"Document editor": "Éditeur de document",
"Document owner": "Propriétaire du document",
"Document role text": "Texte du rôle du document",
"Document sections": "Sections du document",
"Document title": "Titre du document",
"Document tree": "Arborescence du document",
"Document version viewer": "Visionneuse de version du document",
"Document visibility": "Visibilité du document",
"Documents grid": "Grille des documents",
"Docx": "Docx",
"Download": "Télécharger",
"Download anyway": "Télécharger malgré tout",
"Download your document in a .docx or .pdf format.": "Téléchargez votre document au format .docx ou .pdf.",
"Download your document in a .docx, .odt or .pdf format.": "Téléchargez votre document au format .docx, .odt ou .pdf.",
"Drag and drop status": "État du glisser-déposer",
"Duplicate": "Dupliquer",
"Edit document emoji": "Modifier l'émoticône du document",
@@ -769,6 +866,7 @@
"No documents found": "Aucun document trouvé",
"No text selected": "Aucun texte sélectionné",
"No versions": "Aucune version",
"ODT": "ODT",
"OK": "OK",
"Offline ?!": "Hors-ligne ?!",
"Only invited people can access": "Seules les personnes invitées peuvent accéder",
@@ -930,7 +1028,6 @@
"Docx": "Docx",
"Download": "Scarica",
"Download anyway": "Scarica comunque",
"Download your document in a .docx or .pdf format.": "Scarica il tuo documento in formato .docx o .pdf",
"Editor": "Editor",
"Editor unavailable": "Editor non disponibile",
"Emojify": "Emojify",
@@ -1080,6 +1177,9 @@
"Collaborative writing, Simplified.": "Gezamenlijk schrijven, Vereenvoudigd.",
"Confirm": "Bevestigen",
"Connected": "Ingelogd",
"Contains {{count}} sub-documents_many": "Bevat {{count}} subdocumenten",
"Contains {{count}} sub-documents_one": "Bevat {{count}} subdocumenten",
"Contains {{count}} sub-documents_other": "Bevat {{count}} subdocumenten",
"Content modal to explain why the user cannot edit": "Content modal om uit te leggen waarom de gebruiker niet kan bewerken",
"Content modal to export the document": "Content venster om document te exporteren",
"Convert Markdown": "Converteer naar Markdown formaat",
@@ -1107,17 +1207,19 @@
"Document accessible to any connected person": "Document is toegankelijk voor ieder ingelogde persoon",
"Document deleted": "Document verwijderd",
"Document duplicated successfully!": "Document met succes gedupliceerd!",
"Document editor": "Documenteditor",
"Document owner": "Document eigenaar",
"Document role text": "Document roluitleg",
"Document sections": "Document secties",
"Document title": "Documenttitel",
"Document tree": "Boomstructuur document",
"Document version viewer": "Document versie viewer",
"Document visibility": "Document toegankelijkheid",
"Documents grid": "Documenten overzicht",
"Docx": "Docx",
"Download": "Download",
"Download anyway": "Download alsnog",
"Download your document in a .docx or .pdf format.": "Download jouw document in .docx of .pdf formaat.",
"Download your document in a .docx, .odt or .pdf format.": "Download uw document in een .docx, .odt of .pdf formaat.",
"Drag and drop status": "Drag & drop status",
"Duplicate": "Dupliceer",
"Edit document emoji": "Bewerk document emoji",
@@ -1188,6 +1290,7 @@
"No documents found": "Geen documenten gevonden",
"No text selected": "Geen tekst geselecteerd",
"No versions": "Geen versies",
"ODT": "ODT",
"OK": "OK",
"Offline ?!": "Offline?!",
"Only invited people can access": "Alleen uitgenodigde gebruikers hebben toegang",
@@ -1323,6 +1426,7 @@
"Add PDF": "Добавить PDF-файл",
"Add a callout block": "Добавить блок выноски",
"Add a sub page": "Добавить вложенную страницу",
"Add emoji": "Добавь эмодзи",
"Administrator": "Администратор",
"Alert deleted document": "Оповещение об удалённом документе",
"All docs": "Все документы",
@@ -1357,6 +1461,9 @@
"Collaborative writing, Simplified.": "Простой совместный доступ к документам.",
"Confirm": "Подтвердить",
"Connected": "Подключённая",
"Contains {{count}} sub-documents_many": "Содержит {{count}} вложенных документов",
"Contains {{count}} sub-documents_one": "Содержит {{count}} вложенных документов",
"Contains {{count}} sub-documents_other": "Содержит {{count}} вложенных документов",
"Content modal to explain why the user cannot edit": "Пояснение, почему пользователь не может редактировать документ",
"Content modal to export the document": "Подтверждение экспорта документа",
"Convert Markdown": "Преобразовать разметку",
@@ -1384,19 +1491,22 @@
"Document accessible to any connected person": "Документ доступен всем, кто присоединится",
"Document deleted": "Документ удалён",
"Document duplicated successfully!": "Документ успешно дублирован!",
"Document editor": "Редактор документа",
"Document owner": "Владелец документа",
"Document role text": "Текст роли документа",
"Document sections": "Разделы документа",
"Document title": "Заголовок документа",
"Document tree": "Иерархия документа",
"Document version viewer": "Просмотрщик версий документа",
"Document visibility": "Видимость документа",
"Documents grid": "Сетка документов",
"Docx": "Docx",
"Download": "Загрузить",
"Download anyway": "Всё равно загрузить",
"Download your document in a .docx or .pdf format.": "Загрузите свой документ в формате .docx или .pdf.",
"Download your document in a .docx, .odt or .pdf format.": "Загрузить документ в формате .docx, .odt или .pdf.",
"Drag and drop status": "Состояние перетаскивания",
"Duplicate": "Дублировать",
"Edit document emoji": "Изменить документ emoji",
"Editing": "Редактирование",
"Editor": "Редактор",
"Editor unavailable": "Редактор недоступен",
@@ -1464,6 +1574,7 @@
"No documents found": "Документы не найдены",
"No text selected": "Текст не выбран",
"No versions": "Нет версий",
"ODT": "ODT",
"OK": "ОК",
"Offline ?!": "Не в сети?!",
"Only invited people can access": "Имеют доступ только приглашенные люди",
@@ -1574,9 +1685,9 @@
"document": "документ",
"embed": "вложение",
"file": "файл",
"home-content-open-source-part1": "home-content-open-source-part1",
"home-content-open-source-part2": "home-content-open-source-part2",
"home-content-open-source-part3": "home-content-open-source-part3",
"home-content-open-source-part1": "текст ещё не готов",
"home-content-open-source-part2": "текст ещё не готов",
"home-content-open-source-part3": "текст ещё не готов",
"pdf": "pdf"
}
},
@@ -1642,7 +1753,6 @@
"Docx": "Docx",
"Download": "İndir",
"Download anyway": "Yine de indir",
"Download your document in a .docx or .pdf format.": "Belgenizi .docx veya .pdf formatında indirin.",
"Editor": "Editör",
"Editor unavailable": "Editör mevcut değil",
"Emojify": "Emojileştir",
@@ -1703,6 +1813,7 @@
"Add PDF": "Додати файл PDF",
"Add a callout block": "Додати блок винесення",
"Add a sub page": "Додати вкладену сторінку",
"Add emoji": "Додати емодзі",
"Administrator": "Адміністратор",
"Alert deleted document": "Сповіщення про видалений документ",
"All docs": "Всі документи",
@@ -1737,6 +1848,9 @@
"Collaborative writing, Simplified.": "Простий спільний доступ до документів.",
"Confirm": "Підтвердити",
"Connected": "Під'єднане",
"Contains {{count}} sub-documents_many": "Містить {{count}} вкладених документів",
"Contains {{count}} sub-documents_one": "Містить {{count}} вкладених документів",
"Contains {{count}} sub-documents_other": "Містить {{count}} вкладених документів",
"Content modal to explain why the user cannot edit": "Пояснення, чому користувач не може редагувати",
"Content modal to export the document": "Підтвердження експорту документа",
"Convert Markdown": "Перетворити розмітку",
@@ -1764,19 +1878,22 @@
"Document accessible to any connected person": "Документ, доступний для будь-якої особи, що приєдналася",
"Document deleted": "Документ видалено",
"Document duplicated successfully!": "Документ успішно продубльовано!",
"Document editor": "Редактор документа",
"Document owner": "Власник документа",
"Document role text": "Текст ролі документа",
"Document sections": "Розділи документу",
"Document title": "Назва документа",
"Document tree": "Дерево документа",
"Document version viewer": "Переглядач версії документа",
"Document visibility": "Видимість документа",
"Documents grid": "Сітка документів",
"Docx": "Docx",
"Download": "Завантажити",
"Download anyway": "Все одно завантажити",
"Download your document in a .docx or .pdf format.": "Завантажте ваш документ у форматі .docx або .pdf.",
"Download your document in a .docx, .odt or .pdf format.": "Завантажити документ у форматі .docx, .odt або .pdf.",
"Drag and drop status": "Стан перетягування",
"Duplicate": "Дублювати",
"Edit document emoji": "Редагувати документ емодзі",
"Editing": "Редагування",
"Editor": "Редактор",
"Editor unavailable": "Редактор недоступний",
@@ -1844,6 +1961,7 @@
"No documents found": "Документів не знайдено",
"No text selected": "Текст не вибрано",
"No versions": "Немає версій",
"ODT": "ODT",
"OK": "OK",
"Offline ?!": "Не в мережі?!",
"Only invited people can access": "Лише запрошені люди можуть мати доступ",
@@ -1954,9 +2072,9 @@
"document": "документ",
"embed": "вкладення",
"file": "файл",
"home-content-open-source-part1": "home-content-open-source-part1",
"home-content-open-source-part2": "home-content-open-source-part2",
"home-content-open-source-part3": "home-content-open-source-part3",
"home-content-open-source-part1": "текст ще не готовий",
"home-content-open-source-part2": "текст ще не готовий",
"home-content-open-source-part3": "текст ще не готовий",
"pdf": "pdf"
}
},
@@ -2018,7 +2136,6 @@
"Docx": "Doc",
"Download": "下载",
"Download anyway": "仍要下载",
"Download your document in a .docx or .pdf format.": "以doc或者pdf格式下载。",
"Editing": "正在编辑",
"Editor": "编辑者",
"Editor unavailable": "编辑功能不可用",
+6 -1
View File
@@ -1,6 +1,6 @@
{
"name": "impress",
"version": "3.9.0",
"version": "3.10.0",
"private": true,
"repository": "https://github.com/suitenumerique/docs",
"author": "DINUM",
@@ -31,6 +31,7 @@
"server:test": "yarn COLLABORATION_SERVER run test"
},
"resolutions": {
"@tiptap/extensions": "3.10.2",
"@types/node": "24.10.0",
"@types/react": "19.2.2",
"@types/react-dom": "19.2.2",
@@ -38,6 +39,10 @@
"@typescript-eslint/parser": "8.46.2",
"docx": "9.5.0",
"eslint": "9.39.0",
"prosemirror-state": "1.4.4",
"prosemirror-view": "1.41.3",
"prosemirror-model": "1.25.4",
"prosemirror-transform": "1.10.4",
"react": "19.2.0",
"react-dom": "19.2.0",
"typescript": "5.9.3",
@@ -1,6 +1,6 @@
{
"name": "eslint-plugin-docs",
"version": "3.9.0",
"version": "3.10.0",
"repository": "https://github.com/suitenumerique/docs",
"author": "DINUM",
"license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "packages-i18n",
"version": "3.9.0",
"version": "3.10.0",
"repository": "https://github.com/suitenumerique/docs",
"author": "DINUM",
"license": "MIT",
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "server-y-provider",
"version": "3.9.0",
"version": "3.10.0",
"description": "Y.js provider for docs",
"repository": "https://github.com/suitenumerique/docs",
"license": "MIT",
@@ -16,7 +16,7 @@
"node": ">=22"
},
"dependencies": {
"@blocknote/server-util": "0.41.1",
"@blocknote/server-util": "0.42.3",
"@hocuspocus/server": "3.4.0",
"@sentry/node": "10.22.0",
"@sentry/profiling-node": "10.22.0",
@@ -30,7 +30,7 @@
"yjs": "*"
},
"devDependencies": {
"@blocknote/core": "0.41.1",
"@blocknote/core": "0.42.3",
"@hocuspocus/provider": "3.4.0",
"@types/cors": "2.8.19",
"@types/express": "5.0.5",
+221 -202
View File
@@ -1070,49 +1070,49 @@
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
"@blocknote/code-block@0.41.1":
version "0.41.1"
resolved "https://registry.yarnpkg.com/@blocknote/code-block/-/code-block-0.41.1.tgz#bfe0597d3a995b8a2a08307753ec858306781a2d"
integrity sha512-GrvRW0Q9Y6HHCC7tbA76YoGafZPwFqbJgflOVl/kj0h142v8NMH5dgtO/FR/8DOSdnoe9dSUBqUott5ATQo/qg==
"@blocknote/code-block@0.42.3":
version "0.42.3"
resolved "https://registry.yarnpkg.com/@blocknote/code-block/-/code-block-0.42.3.tgz#7b1a3ed0b4f2d75835c422c04f52e824aa0845cf"
integrity sha512-kPdHABXJdH7lxB1Fxqg/bxWmtO/5y3REgRcuppEpCkrfXlwV+RPCChCU2jnMTUBe6CZyFQwum6/D9oUGVJKRqw==
dependencies:
"@blocknote/core" "0.41.1"
"@shikijs/core" "^3.2.1"
"@shikijs/engine-javascript" "^3.2.1"
"@shikijs/langs" "^3.2.1"
"@shikijs/langs-precompiled" "^3.2.1"
"@shikijs/themes" "^3.2.1"
"@blocknote/core" "0.42.3"
"@shikijs/core" "^3.13.0"
"@shikijs/engine-javascript" "^3.13.0"
"@shikijs/langs" "^3.13.0"
"@shikijs/langs-precompiled" "^3.13.0"
"@shikijs/themes" "^3.13.0"
"@shikijs/types" "^3.13.0"
"@blocknote/core@0.41.1":
version "0.41.1"
resolved "https://registry.yarnpkg.com/@blocknote/core/-/core-0.41.1.tgz#63959297ced039874e5661138bfdffa43605f6ad"
integrity sha512-p/wxXzpl0/c9QwqXWcZ4KXzI+OjVzQOzSNaO5KrtDPDi7M1Bj6sc9L0+/V/8Wyo+XTY+tZOrtu6qCXVYIEJ/Rw==
"@blocknote/core@0.42.3":
version "0.42.3"
resolved "https://registry.yarnpkg.com/@blocknote/core/-/core-0.42.3.tgz#2ac1654a04df65d4618440e520ad187d0c070169"
integrity sha512-wtZki6Gok5Ac9Ek6QTQztcDymstEQgVCisJwiUZTWXh8CD4UKfnIxM7C9+6eEnZMmQ8GNTvRf1HXFl+E4N78VA==
dependencies:
"@emoji-mart/data" "^1.2.1"
"@shikijs/types" "3.13.0"
"@tiptap/core" "^3.4.3"
"@tiptap/extension-bold" "^3"
"@tiptap/extension-code" "^3"
"@tiptap/extension-gapcursor" "^3"
"@tiptap/extension-history" "^3"
"@tiptap/extension-horizontal-rule" "^3"
"@tiptap/extension-italic" "^3"
"@tiptap/extension-link" "^3"
"@tiptap/extension-paragraph" "^3"
"@tiptap/extension-strike" "^3"
"@tiptap/extension-text" "^3"
"@tiptap/extension-underline" "^3"
"@tiptap/pm" "^3.4.3"
"@tiptap/core" "^3.11.0"
"@tiptap/extension-bold" "^3.7.2"
"@tiptap/extension-code" "^3.7.2"
"@tiptap/extension-gapcursor" "^3.7.2"
"@tiptap/extension-history" "^3.7.2"
"@tiptap/extension-horizontal-rule" "^3.7.2"
"@tiptap/extension-italic" "^3.7.2"
"@tiptap/extension-link" "^3.7.2"
"@tiptap/extension-paragraph" "^3.7.2"
"@tiptap/extension-strike" "^3.7.2"
"@tiptap/extension-text" "^3.7.2"
"@tiptap/extension-underline" "^3.7.2"
"@tiptap/pm" "^3.11.0"
emoji-mart "^5.6.0"
fast-deep-equal "^3"
fast-deep-equal "^3.1.3"
hast-util-from-dom "^5.0.1"
prosemirror-dropcursor "^1.8.2"
prosemirror-highlight "^0.13.0"
prosemirror-model "^1.25.3"
prosemirror-state "^1.4.3"
prosemirror-tables "^1.6.4"
prosemirror-transform "^1.10.4"
prosemirror-view "^1.41.2"
prosemirror-model "^1.25.4"
prosemirror-state "^1.4.4"
prosemirror-tables "^1.8.1"
prosemirror-transform "^1.10.5"
prosemirror-view "^1.41.3"
rehype-format "^5.0.1"
rehype-parse "^9.0.1"
rehype-remark "^10.0.1"
@@ -1128,81 +1128,92 @@
y-protocols "^1.0.6"
yjs "^13.6.27"
"@blocknote/mantine@0.41.1":
version "0.41.1"
resolved "https://registry.yarnpkg.com/@blocknote/mantine/-/mantine-0.41.1.tgz#3bd2030d79376df5a3c44ec3cdf601eea50d9bdf"
integrity sha512-0geMa5zRd3d67xpDCGAclW4y0yQ8Hn0ldjzUz7ilB/9NpYt+f9Y5uuuaK4DwchwYuMmCLDrtjtAh/jfmdSnnjw==
"@blocknote/mantine@0.42.3":
version "0.42.3"
resolved "https://registry.yarnpkg.com/@blocknote/mantine/-/mantine-0.42.3.tgz#3cd0b13e6ecb40b0b086a4877c2b2361a2fad266"
integrity sha512-xzLweZG1KfFoOp/aSHTXE10IrfEHnhDlP0C2Qt2eNO2IHHa7l8XZJpIGhCoVMsn0yylm91OSynNfTO7JkZZi8w==
dependencies:
"@blocknote/core" "0.41.1"
"@blocknote/react" "0.41.1"
react-icons "^5.2.1"
"@blocknote/core" "0.42.3"
"@blocknote/react" "0.42.3"
react-icons "^5.5.0"
"@blocknote/react@0.41.1":
version "0.41.1"
resolved "https://registry.yarnpkg.com/@blocknote/react/-/react-0.41.1.tgz#4f35f8314ad001903cf9793e7dc9673b05ec3344"
integrity sha512-W1lRcyjpgNOZzASIbdX/fvEQ3ZML7FzHyS2xA9CskxOPrGvxHWREn+vper/hkchlTZ4I2dTx/IWwGAECT+2AvA==
"@blocknote/react@0.42.3":
version "0.42.3"
resolved "https://registry.yarnpkg.com/@blocknote/react/-/react-0.42.3.tgz#c2ec737083e7341a18084c59de2021e64ad5f665"
integrity sha512-YnrQ1uyezDbaxYcFstWOJ2r8BMxqwwEc7QAhrEjCMEyBAiOxSCPnrM4/GE2mOgCS0Xa9wIp2LDoPQP2Syv+2EA==
dependencies:
"@blocknote/core" "0.41.1"
"@blocknote/core" "0.42.3"
"@emoji-mart/data" "^1.2.1"
"@floating-ui/react" "^0.27.16"
"@tiptap/core" "^3.4.3"
"@tiptap/pm" "^3.4.3"
"@tiptap/react" "^3.4.3"
"@tiptap/core" "^3.11.0"
"@tiptap/pm" "^3.11.0"
"@tiptap/react" "^3.11.0"
emoji-mart "^5.6.0"
lodash.merge "^4.6.2"
react-icons "^5.2.1"
react-icons "^5.5.0"
"@blocknote/server-util@0.41.1":
version "0.41.1"
resolved "https://registry.yarnpkg.com/@blocknote/server-util/-/server-util-0.41.1.tgz#db738ea33039a9dd54c0a92e9c3ed72bfa354cb3"
integrity sha512-xZaj/jwKq4rVdOxaNyBmJIJTZ0c8++Ttvy6Zp9W7B2XLxT9baGbsAtXCTra+lBHCf/XyqvA12UuuyB4KrA5bnQ==
"@blocknote/server-util@0.42.3":
version "0.42.3"
resolved "https://registry.yarnpkg.com/@blocknote/server-util/-/server-util-0.42.3.tgz#113fc33cabc4e6a9fa776183182dfee6fef7e6ff"
integrity sha512-M+jtKeC2aHOYBp6GQ0YR19iv0/0f1HElrrnKwlaSPbwR6bw6tg+yb3yQkaJJioLTpd2X2Z/RwcEvxSJGnlZ81w==
dependencies:
"@blocknote/core" "0.41.1"
"@blocknote/react" "0.41.1"
"@tiptap/core" "^3.4.3"
"@tiptap/pm" "^3.4.3"
"@blocknote/core" "0.42.3"
"@blocknote/react" "0.42.3"
"@tiptap/core" "^3.11.0"
"@tiptap/pm" "^3.11.0"
jsdom "^25.0.1"
y-prosemirror "^1.3.7"
y-protocols "^1.0.6"
yjs "^13.6.27"
"@blocknote/xl-docx-exporter@0.41.1":
version "0.41.1"
resolved "https://registry.yarnpkg.com/@blocknote/xl-docx-exporter/-/xl-docx-exporter-0.41.1.tgz#498f68c7eb9a42c2c3606f61d972c815f92cb0e5"
integrity sha512-dyMY/jcxTlZCKpV1ABve7me4gCZHQzkcY5sqzvZtbqmv0IslqD4xq06ZU9KYUoCLnrJTEuRXN9dxnaCp08ipRQ==
"@blocknote/xl-docx-exporter@0.42.3":
version "0.42.3"
resolved "https://registry.yarnpkg.com/@blocknote/xl-docx-exporter/-/xl-docx-exporter-0.42.3.tgz#e276b1a22c34a2b5d0837606d7b96cc3acaba92e"
integrity sha512-VpotYcG+fQFzC2gtqTlBJDi0GKQQ6RygzeyzBBDGeMKSH3P72TDKVYVqN4Ert7HxXz41aCLGgtaf6x9zlox26g==
dependencies:
"@blocknote/core" "0.41.1"
"@blocknote/xl-multi-column" "0.41.1"
"@blocknote/core" "0.42.3"
"@blocknote/xl-multi-column" "0.42.3"
buffer "^6.0.3"
docx "^9.0.2"
image-meta "^0.2.1"
docx "^9.5.1"
image-meta "^0.2.2"
"@blocknote/xl-multi-column@0.41.1":
version "0.41.1"
resolved "https://registry.yarnpkg.com/@blocknote/xl-multi-column/-/xl-multi-column-0.41.1.tgz#c2ab05c5cb6c1666c28e405689d6f85ec0a2cd0b"
integrity sha512-SAYyKLpvdWfiuWPRAg7eR9/XfyX51f1o8tptcKWwfCk0FBND+VmYK7IJuxq1N789wdm+msuxUedSZWzKiSErpA==
"@blocknote/xl-multi-column@0.42.3":
version "0.42.3"
resolved "https://registry.yarnpkg.com/@blocknote/xl-multi-column/-/xl-multi-column-0.42.3.tgz#39fad4ef51f26c4af15423f44334de8edf06fe3c"
integrity sha512-7ylZYlOOVNMJ3u4C07yiE6qr04kcEYnxY3UfcFBSyV8H+N0LHGLFFJIz6JPKWvji4fu5lvbxXqv0IcGbCQ0/cA==
dependencies:
"@blocknote/core" "0.41.1"
"@blocknote/react" "0.41.1"
"@tiptap/core" "^3.4.3"
prosemirror-model "^1.25.3"
prosemirror-state "^1.4.3"
prosemirror-tables "^1.3.7"
prosemirror-transform "^1.10.4"
prosemirror-view "^1.41.2"
react-icons "^5.2.1"
"@blocknote/core" "0.42.3"
"@blocknote/react" "0.42.3"
"@tiptap/core" "^3.11.0"
prosemirror-model "^1.25.4"
prosemirror-state "^1.4.4"
prosemirror-tables "^1.8.1"
prosemirror-transform "^1.10.5"
prosemirror-view "^1.41.3"
react-icons "^5.5.0"
"@blocknote/xl-pdf-exporter@0.41.1":
version "0.41.1"
resolved "https://registry.yarnpkg.com/@blocknote/xl-pdf-exporter/-/xl-pdf-exporter-0.41.1.tgz#b55c7e8c6a21ae069a42671b1391eab9d4119195"
integrity sha512-Ixhlm2iV9a82AroMvW7w2RTNp4H7aEkYO/ar6bcMTIZul2kmg06akkw+tQc3rH+CLYQTiFXr9iKwGdnLxoIDww==
"@blocknote/xl-odt-exporter@0.42.3":
version "0.42.3"
resolved "https://registry.yarnpkg.com/@blocknote/xl-odt-exporter/-/xl-odt-exporter-0.42.3.tgz#fee0f142ca8ae5ce8bb969f9de0f79e2a92e1c6a"
integrity sha512-wW1Zxd3Y14IG5X/mi0OBoGV/EFxeO5Alsd0HVsBo0imk+GLSKx2YCU02plUG5l8IOQOUeWBHamm4OT+7sgj9Ow==
dependencies:
"@blocknote/core" "0.41.1"
"@blocknote/react" "0.41.1"
"@blocknote/xl-multi-column" "0.41.1"
"@blocknote/core" "0.42.3"
"@blocknote/xl-multi-column" "0.42.3"
"@zip.js/zip.js" "^2.8.8"
buffer "^6.0.3"
image-meta "^0.2.2"
"@blocknote/xl-pdf-exporter@0.42.3":
version "0.42.3"
resolved "https://registry.yarnpkg.com/@blocknote/xl-pdf-exporter/-/xl-pdf-exporter-0.42.3.tgz#667c0c2756e8300f0dc134718e6d2833947b799f"
integrity sha512-ZPGVHovDWhwu++vkVzDEh6KOoHK6q8iLFo9fGLcQ8oKjNCJoBr344Z42AdMMxcoCDddQmC+5yqzUN8J/9xnE1Q==
dependencies:
"@blocknote/core" "0.42.3"
"@blocknote/react" "0.42.3"
"@blocknote/xl-multi-column" "0.42.3"
"@react-pdf/renderer" "^4.3.0"
buffer "^6.0.3"
docx "^9.0.2"
docx "^9.5.1"
"@cacheable/memoize@^2.0.3":
version "2.0.3"
@@ -4953,46 +4964,46 @@
unplugin "1.0.1"
uuid "^9.0.0"
"@shikijs/core@^3.2.1":
version "3.13.0"
resolved "https://registry.yarnpkg.com/@shikijs/core/-/core-3.13.0.tgz#73503364a1eb51b65cf904115c62fed7a47df596"
integrity sha512-3P8rGsg2Eh2qIHekwuQjzWhKI4jV97PhvYjYUzGqjvJfqdQPz+nMlfWahU24GZAyW1FxFI1sYjyhfh5CoLmIUA==
"@shikijs/core@^3.13.0":
version "3.15.0"
resolved "https://registry.yarnpkg.com/@shikijs/core/-/core-3.15.0.tgz#eee251070b4e39b59e108266cbcd50c85d738d54"
integrity sha512-8TOG6yG557q+fMsSVa8nkEDOZNTSxjbbR8l6lF2gyr6Np+jrPlslqDxQkN6rMXCECQ3isNPZAGszAfYoJOPGlg==
dependencies:
"@shikijs/types" "3.13.0"
"@shikijs/types" "3.15.0"
"@shikijs/vscode-textmate" "^10.0.2"
"@types/hast" "^3.0.4"
hast-util-to-html "^9.0.5"
"@shikijs/engine-javascript@^3.2.1":
version "3.13.0"
resolved "https://registry.yarnpkg.com/@shikijs/engine-javascript/-/engine-javascript-3.13.0.tgz#d25cefdac378216a95fefdf0b3a560550393ea65"
integrity sha512-Ty7xv32XCp8u0eQt8rItpMs6rU9Ki6LJ1dQOW3V/56PKDcpvfHPnYFbsx5FFUP2Yim34m/UkazidamMNVR4vKg==
"@shikijs/engine-javascript@^3.13.0":
version "3.15.0"
resolved "https://registry.yarnpkg.com/@shikijs/engine-javascript/-/engine-javascript-3.15.0.tgz#478dd4feb3b4b7e91f148cc9e7ebc0b7de5fbb18"
integrity sha512-ZedbOFpopibdLmvTz2sJPJgns8Xvyabe2QbmqMTz07kt1pTzfEvKZc5IqPVO/XFiEbbNyaOpjPBkkr1vlwS+qg==
dependencies:
"@shikijs/types" "3.13.0"
"@shikijs/types" "3.15.0"
"@shikijs/vscode-textmate" "^10.0.2"
oniguruma-to-es "^4.3.3"
"@shikijs/langs-precompiled@^3.2.1":
version "3.13.0"
resolved "https://registry.yarnpkg.com/@shikijs/langs-precompiled/-/langs-precompiled-3.13.0.tgz#6ac03cc178fc246e0ddae386a8339b4bd5499c31"
integrity sha512-B2xmXar8IdCy2Gf+VtWmcv8tWpfeFPxPP3eKDa13dKshERbxHHVe0gCV+NrlcWbyVxBm22IUqqj7TIewJstNBQ==
"@shikijs/langs-precompiled@^3.13.0":
version "3.15.0"
resolved "https://registry.yarnpkg.com/@shikijs/langs-precompiled/-/langs-precompiled-3.15.0.tgz#7ad88657a4658eba1c261f074d2eff12980f748e"
integrity sha512-APb/UJeT1FPttKYyi2qMsN9OtGSU14xXME9ecSjb9uNchxo5Kszw+BLufBS6I9/5SFaUDmKxunZV1OIm/Pe3ug==
dependencies:
"@shikijs/types" "3.13.0"
"@shikijs/types" "3.15.0"
oniguruma-to-es "^4.3.3"
"@shikijs/langs@^3.2.1":
version "3.13.0"
resolved "https://registry.yarnpkg.com/@shikijs/langs/-/langs-3.13.0.tgz#51a927c8089dffb2560ac8d7549297de9d081b91"
integrity sha512-672c3WAETDYHwrRP0yLy3W1QYB89Hbpj+pO4KhxK6FzIrDI2FoEXNiNCut6BQmEApYLfuYfpgOZaqbY+E9b8wQ==
"@shikijs/langs@^3.13.0":
version "3.15.0"
resolved "https://registry.yarnpkg.com/@shikijs/langs/-/langs-3.15.0.tgz#d8385a9ca66ce9923149c650336444b1d25fc248"
integrity sha512-WpRvEFvkVvO65uKYW4Rzxs+IG0gToyM8SARQMtGGsH4GDMNZrr60qdggXrFOsdfOVssG/QQGEl3FnJ3EZ+8w8A==
dependencies:
"@shikijs/types" "3.13.0"
"@shikijs/types" "3.15.0"
"@shikijs/themes@^3.2.1":
version "3.13.0"
resolved "https://registry.yarnpkg.com/@shikijs/themes/-/themes-3.13.0.tgz#ee92780f0580d4ffa8ed619b52c5eb4a95d012a3"
integrity sha512-Vxw1Nm1/Od8jyA7QuAenaV78BG2nSr3/gCGdBkLpfLscddCkzkL36Q5b67SrLLfvAJTOUzW39x4FHVCFriPVgg==
"@shikijs/themes@^3.13.0":
version "3.15.0"
resolved "https://registry.yarnpkg.com/@shikijs/themes/-/themes-3.15.0.tgz#6093a90191b89654045c72636ddd35c04273658f"
integrity sha512-8ow2zWb1IDvCKjYb0KiLNrK4offFdkfNVPXb1OZykpLCzRU6j+efkY+Y7VQjNlNFXonSw+4AOdGYtmqykDbRiQ==
dependencies:
"@shikijs/types" "3.13.0"
"@shikijs/types" "3.15.0"
"@shikijs/types@3.13.0", "@shikijs/types@^3.13.0":
version "3.13.0"
@@ -5002,6 +5013,14 @@
"@shikijs/vscode-textmate" "^10.0.2"
"@types/hast" "^3.0.4"
"@shikijs/types@3.15.0":
version "3.15.0"
resolved "https://registry.yarnpkg.com/@shikijs/types/-/types-3.15.0.tgz#4e025b4dea98e1603243b1f00677854e07e5eda1"
integrity sha512-BnP+y/EQnhihgHy4oIAN+6FFtmfTekwOLsQbRw9hOKwqgNy8Bdsjq8B05oAt/ZgvIWWFrshV71ytOrlPfYjIJw==
dependencies:
"@shikijs/vscode-textmate" "^10.0.2"
"@types/hast" "^3.0.4"
"@shikijs/vscode-textmate@^10.0.2":
version "10.0.2"
resolved "https://registry.yarnpkg.com/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz#a90ab31d0cc1dfb54c66a69e515bf624fa7b2224"
@@ -5242,94 +5261,89 @@
resolved "https://registry.yarnpkg.com/@testing-library/user-event/-/user-event-14.6.1.tgz#13e09a32d7a8b7060fe38304788ebf4197cd2149"
integrity sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==
"@tiptap/core@^3.4.3":
version "3.7.2"
resolved "https://registry.yarnpkg.com/@tiptap/core/-/core-3.7.2.tgz#f9333a9dce628d72c05f919abca1148896161338"
integrity sha512-fJwNpTx0aq4UU0HNkxPvPYfNBcTHQ/q5xBUdOB5Mgu6clwGES38jVsNNSudB8g53APUmJIS+2fJbkxl3V+0jww==
"@tiptap/core@^3.11.0":
version "3.11.0"
resolved "https://registry.yarnpkg.com/@tiptap/core/-/core-3.11.0.tgz#122a1db7852c9cea48221290210e713bb4efd66e"
integrity sha512-kmS7ZVpHm1EMnW1Wmft9H5ZLM7E0G0NGBx+aGEHGDcNxZBXD2ZUa76CuWjIhOGpwsPbELp684ZdpF2JWoNi4Dg==
"@tiptap/extension-bold@^3":
version "3.7.2"
resolved "https://registry.yarnpkg.com/@tiptap/extension-bold/-/extension-bold-3.7.2.tgz#cbc94eb7239ef36e1f5581f93e0001bac013c4c6"
integrity sha512-bwCn9lQEXnEi7LfIx3G/oaH4I0ZapAgrHzLCNJH/tNgRKVWym1H1Oa8PlkiFDbalWOdUkbgeAUqUaIB13k408Q==
"@tiptap/extension-bold@^3.7.2":
version "3.10.2"
resolved "https://registry.yarnpkg.com/@tiptap/extension-bold/-/extension-bold-3.10.2.tgz#c2160024bcd672bf399ddb97a409c3dc6ba3ceb0"
integrity sha512-lgUpWuBhlZwf+/pVKfqVUpHfA5PDECDyobcXmMrRSpreM+58psZtWDZMZ21K94SmJukRidW7vdNWoTSRSEiY4Q==
"@tiptap/extension-bubble-menu@^3.7.2":
version "3.7.2"
resolved "https://registry.yarnpkg.com/@tiptap/extension-bubble-menu/-/extension-bubble-menu-3.7.2.tgz#c6cac8a2db5ef12d1fb12538413ec2ae578a0dad"
integrity sha512-rCJu/X7sZEYWkOwLO342JP06f4giVBECPzr/SzG/fQdAidPW96eilPk3L82w5j24kS9odTlxSLlFlIf6UZ2b9w==
"@tiptap/extension-bubble-menu@^3.11.0":
version "3.11.0"
resolved "https://registry.yarnpkg.com/@tiptap/extension-bubble-menu/-/extension-bubble-menu-3.11.0.tgz#2ce7820c9aecd0f4ce36c2668353aa8194ea55a5"
integrity sha512-P3j9lQ+EZ5Zg/isJzLpCPX7bp7WUBmz8GPs/HPlyMyN2su8LqXntITBZr8IP1JNBlB/wR83k/W0XqdC57mG7cA==
dependencies:
"@floating-ui/dom" "^1.0.0"
"@tiptap/extension-code@^3":
version "3.7.2"
resolved "https://registry.yarnpkg.com/@tiptap/extension-code/-/extension-code-3.7.2.tgz#80b43d1590cff592c25b7552c3706b4f4fbbece3"
integrity sha512-J8FaCiKJJnHvQiPcbfbUtc5RNmGx/Gui/K5CDMPc17jhCiQ9JhR9idRPREV24Z2t7GujWX7LG6ZDDR82pSns+g==
"@tiptap/extension-code@^3.7.2":
version "3.10.2"
resolved "https://registry.yarnpkg.com/@tiptap/extension-code/-/extension-code-3.10.2.tgz#11a057d9a649dc9e914e32fbfb23e7748a1f1d67"
integrity sha512-+oA2fuQPQDzZb3q0pQeObPrhWXPh9JxybnAAGFoGenZsMsoUdN8x/KdtrXGWDMoB9XIg7XwE1xO6EZAH+eLe8Q==
"@tiptap/extension-floating-menu@^3.7.2":
version "3.7.2"
resolved "https://registry.yarnpkg.com/@tiptap/extension-floating-menu/-/extension-floating-menu-3.7.2.tgz#927217d238b45695621be75398795974c3fa3737"
integrity sha512-g19ratrXlplYDS29VLQa1y/IM/ro0UFhSS4fQokiQKkazwnA1ZVnebjw8ERYg5lkMm/hiImqstpgdO0LtoivvQ==
"@tiptap/extension-floating-menu@^3.11.0":
version "3.11.0"
resolved "https://registry.yarnpkg.com/@tiptap/extension-floating-menu/-/extension-floating-menu-3.11.0.tgz#521109d9c0d5f6dc5fb6f2fd8181367af8a91be2"
integrity sha512-nEHdWZHEJYX1II1oJQ4aeZ8O/Kss4BRbYFXQFGIvPelCfCYEATpUJh3aq3767ARSq40bOWyu+Dcd4SCW0We6Sw==
"@tiptap/extension-gapcursor@^3":
version "3.7.2"
resolved "https://registry.yarnpkg.com/@tiptap/extension-gapcursor/-/extension-gapcursor-3.7.2.tgz#2e2a673a42c87854ecb989d2aebf213514e04d63"
integrity sha512-vCLo2dL2SfeWjh/gJKDiu0/fz6OF7obGTJvHg/yStkoUqlAEiwKoyHP/NXeTGYJMzZzUi0kY9DtTEJdGFvphuQ==
"@tiptap/extension-gapcursor@^3.7.2":
version "3.10.2"
resolved "https://registry.yarnpkg.com/@tiptap/extension-gapcursor/-/extension-gapcursor-3.10.2.tgz#d9b66196cae2ebfe7e1747a67b21713463832697"
integrity sha512-sBCu8enXm3W3BjnvvGBrzAiSuQSVZyhbQAgaFKjHJKBQRbek55EEbRA0ETUmHcSQbYf0D8hmDt2++HAyEASEsQ==
"@tiptap/extension-history@^3":
version "3.7.2"
resolved "https://registry.yarnpkg.com/@tiptap/extension-history/-/extension-history-3.7.2.tgz#4210b78a726ad6763b1cf1a506bac56e5dfafcff"
integrity sha512-iAGTUxAr7r+tQ/PtIG94jqTJLy/S/VwE43USfWzXCHvbLn60cPJJG7MTCZxYbd+ZuivZVhEhp3EbzCNmHxjp8A==
"@tiptap/extension-history@^3.7.2":
version "3.10.2"
resolved "https://registry.yarnpkg.com/@tiptap/extension-history/-/extension-history-3.10.2.tgz#f19073e57f4bb8c2e5578c8ac0d7e71ae59d79be"
integrity sha512-1u65sQt0vAyXDOyA2YRgyMcPv6pEt60JEU3IOlt1flVYbIcTFy9X8FILmXlq5MC+bRyJXWn7SfjnJWhWbVv7zA==
"@tiptap/extension-horizontal-rule@^3":
version "3.7.2"
resolved "https://registry.yarnpkg.com/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-3.7.2.tgz#5d8e9d70e7c6ba91b3e4d9a7e1693232c37ce161"
integrity sha512-pN+1hJAVVP3uqtpZ5Rm7z5XUB/NGprK6wExJ04xG117E4rTVcaEb1FnMILY3J3A5XbdC3vHX+cblR8mOl1PAMw==
"@tiptap/extension-horizontal-rule@^3.7.2":
version "3.10.2"
resolved "https://registry.yarnpkg.com/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-3.10.2.tgz#4250f704bb5da0e9c05f0930cd3adc63571a7066"
integrity sha512-EkVomzUGfhTp6LF/6jKXKAHiR3bDnZRBVbegocGn5mAZB+5nItxafa7s37zzcPdPI+prnw/C9DRGsZf6pVb4dQ==
"@tiptap/extension-italic@^3":
version "3.7.2"
resolved "https://registry.yarnpkg.com/@tiptap/extension-italic/-/extension-italic-3.7.2.tgz#8425d27a67e1d70172ebebcc0cfa7fd8afe5b50b"
integrity sha512-1tfF37LvKgA5hg09UBgOjdMLNRb1C6keIOBF0r5oHKeWPYOf4z3j5IU9PsFUoOn53XRMb1aiD/TNbGPyoT3Fyw==
"@tiptap/extension-italic@^3.7.2":
version "3.10.2"
resolved "https://registry.yarnpkg.com/@tiptap/extension-italic/-/extension-italic-3.10.2.tgz#c6fd3676ba5e2cc63266135ea7a01120a0ca8417"
integrity sha512-MnRbTSNtjLE56E7k0CFprIIfr2yaT0Yd0dwYH7pvWePmSYeVFQDwu9CcVOzF58iv5BasyXc3sO2yhWlXRTY7Ig==
"@tiptap/extension-link@^3":
version "3.7.2"
resolved "https://registry.yarnpkg.com/@tiptap/extension-link/-/extension-link-3.7.2.tgz#6a2f6b979eb86905c8776df12a71838fb5e2007c"
integrity sha512-9K54PxBiDSWAMfICqkb8jcQ6cL7vDAtjTk0zqBw4d+XuaUy0FC9QUdbx7r1Pkbf36K1/ApbvM9a7qpOirWk8Xw==
"@tiptap/extension-link@^3.7.2":
version "3.10.2"
resolved "https://registry.yarnpkg.com/@tiptap/extension-link/-/extension-link-3.10.2.tgz#46d7f6647c555ac660c29b93d1952936033cd45c"
integrity sha512-c7ZvinwECBEn3IVI9XpUJKEwvrLtZDiEaYNAjBQgShF1EUCf7JVcNK9wcrFm/oDw9es1cq0yrKqsbBh/bvGO2Q==
dependencies:
linkifyjs "^4.3.2"
"@tiptap/extension-paragraph@^3":
version "3.7.2"
resolved "https://registry.yarnpkg.com/@tiptap/extension-paragraph/-/extension-paragraph-3.7.2.tgz#4e9a84d36c96a92cccc7c6bec9f4f744beef413c"
integrity sha512-HmDuAixTcvP4A/v6OLkh/C6nB86i7/DRNswBf/Udak8TgWUIcSUK0iActxxm5+B3MZTSf3U87JzyI6IeuElLIQ==
"@tiptap/extension-paragraph@^3.7.2":
version "3.10.2"
resolved "https://registry.yarnpkg.com/@tiptap/extension-paragraph/-/extension-paragraph-3.10.2.tgz#f37cc4fd8982b31c7a3088f4360cc25f4120e2f3"
integrity sha512-k84BMUxpeFTEIoUil4tnXF5viY4oUHXq4wz4JkO/LMEW6lAkO/PhJnJMMrcEJu0sox4aoNppcnS236RNXCiPpg==
"@tiptap/extension-strike@^3":
version "3.7.2"
resolved "https://registry.yarnpkg.com/@tiptap/extension-strike/-/extension-strike-3.7.2.tgz#fd82bc48e47413bb80caa53b6fb9d90c4edeff3c"
integrity sha512-I1G+4vZbCBTpAMmyVwaO8cLBJgXEf1DyEzc0B+HhTJiBa9qA9OKgRQEGFgisxu1kggjbzB6+d0+taHfjsZC1SQ==
"@tiptap/extension-strike@^3.7.2":
version "3.10.2"
resolved "https://registry.yarnpkg.com/@tiptap/extension-strike/-/extension-strike-3.10.2.tgz#f638fcd08c0d07d8b4b7a1414f78effb8db5641b"
integrity sha512-e6+WaEhWlsbV3mw8kSMwgq7Tty8BWoRGFGQj5B6Tg7bZUg3qgdE0Kp2s6MGNNikpuDchOebbIZxOk/qfVqgUbw==
"@tiptap/extension-text@^3":
version "3.7.2"
resolved "https://registry.yarnpkg.com/@tiptap/extension-text/-/extension-text-3.7.2.tgz#fbaf60768a2d3ef77f51e5e070614bdebf371be2"
integrity sha512-sKaeGYNP1+bAe2rvmzWLW5qH9DsSFOJlOUEOFchR0OX0rC7bbGS6/KuyAq0w6UkL+cMJnDyAbv3KeD2WEA192w==
"@tiptap/extension-text@^3.7.2":
version "3.10.2"
resolved "https://registry.yarnpkg.com/@tiptap/extension-text/-/extension-text-3.10.2.tgz#5f6d409c891b30360a46ca389c7a9da683000ad8"
integrity sha512-5gHtEh7eIjFYtwIYvjJp1Sg7qlS1ObOLIkYGOm763t0JJbePXnkA5EnyfxAq3g+wfPajK7qgs3uqArCjlHA33w==
"@tiptap/extension-underline@^3":
version "3.7.2"
resolved "https://registry.yarnpkg.com/@tiptap/extension-underline/-/extension-underline-3.7.2.tgz#e3831c7211501ba4b8bbd1f588c6b86f351fa13a"
integrity sha512-GDpUZllTD7uIdHjTzYJ6i4jUgCeviW40SCpLVVv1xH0gj1t1xu0Rnxmk+bXkF2XNe8jPXkMCgYNr6DR6eO8roQ==
"@tiptap/extension-underline@^3.7.2":
version "3.10.2"
resolved "https://registry.yarnpkg.com/@tiptap/extension-underline/-/extension-underline-3.10.2.tgz#a84dfaf57c8f2dc06078d3611b481b78e92dd1d3"
integrity sha512-/n/+YXYYGmOOQl8zPZiyZFRtOmsnTe1TfEjNcsJcIUGW4X5teddp4lVTcvGO3aaufH48FcYnSVJya7A9dw3ABg==
"@tiptap/extensions@*":
version "3.7.2"
resolved "https://registry.yarnpkg.com/@tiptap/extensions/-/extensions-3.7.2.tgz#fd2d53e59343505487e9fb6ccad8c53945d5c494"
integrity sha512-FaToSdU9fhQk2swkaXrAQNgdaE0dwLbUHcvilW5F4xTpQfZ3J535u5U2TUYf+f9KKSV5fTmD4QGNY9qxY7ihTg==
"@tiptap/extensions@*", "@tiptap/extensions@3.10.2":
version "3.10.2"
resolved "https://registry.yarnpkg.com/@tiptap/extensions/-/extensions-3.10.2.tgz#5f5ba24a109894f19acc443c57963c32f60e16f1"
integrity sha512-XyvMn6B6PCPsgV6VMLiS1QXI1OKarBAYwXmqsE+gCzzYyXxYX4sLUlQ8JKysREyIGMHxSg5vgOajsgXgFMrvyA==
"@tiptap/extensions@3.10.1":
version "3.10.1"
resolved "https://registry.yarnpkg.com/@tiptap/extensions/-/extensions-3.10.1.tgz#7faab67917a779a77ec89f588ad5fb7670b4dbff"
integrity sha512-tZZ1IGIcch4ezuoid3iPSirh0s2GQuSKY6ceWRJCVeZ2gT2LsN3i10tqfidcYrsmyQRMuM7QUfRmH5HOKJZ73Q==
"@tiptap/pm@^3.4.3":
version "3.7.2"
resolved "https://registry.yarnpkg.com/@tiptap/pm/-/pm-3.7.2.tgz#28953dc6e310445250b5b858286186c5ada14a2f"
integrity sha512-i2fvXDapwo/TWfHM6STYEbkYyF3qyfN6KEBKPrleX/Z80G5bLxom0gB79TsjLNxTLi6mdf0vTHgAcXMG1avc2g==
"@tiptap/pm@^3.11.0":
version "3.11.0"
resolved "https://registry.yarnpkg.com/@tiptap/pm/-/pm-3.11.0.tgz#c9d2bef0db08a5a5b2c6cce035fe893a475ee638"
integrity sha512-plCQDLCZIOc92cizB8NNhBRN0szvYR3cx9i5IXo6v9Xsgcun8KHNcJkesc2AyeqdIs0BtOJZaqQ9adHThz8UDw==
dependencies:
prosemirror-changeset "^2.3.0"
prosemirror-collab "^1.3.1"
@@ -5350,17 +5364,17 @@
prosemirror-transform "^1.10.2"
prosemirror-view "^1.38.1"
"@tiptap/react@^3.4.3":
version "3.7.2"
resolved "https://registry.yarnpkg.com/@tiptap/react/-/react-3.7.2.tgz#bf12f0f33f76407abf06b8530cb9461335a1e659"
integrity sha512-tka4ioSmsGI4TyGZ7jAUoIw8t8DVjr1It0B38vZVLqg8M/ZFgR1NkF50TJ6qAkhy8Uz12AO50so0v79tV2pmEA==
"@tiptap/react@^3.11.0":
version "3.11.0"
resolved "https://registry.yarnpkg.com/@tiptap/react/-/react-3.11.0.tgz#b9dd344101cd64df45cb7a5785f98c7d3a689f72"
integrity sha512-SDGei/2DjwmhzsxIQNr6dkB6NxLgXZjQ6hF36NfDm4937r5NLrWrNk5tCsoDQiKZ0DHEzuJ6yZM5C7I7LZLB6w==
dependencies:
"@types/use-sync-external-store" "^0.0.6"
fast-deep-equal "^3.1.3"
use-sync-external-store "^1.4.0"
optionalDependencies:
"@tiptap/extension-bubble-menu" "^3.7.2"
"@tiptap/extension-floating-menu" "^3.7.2"
"@tiptap/extension-bubble-menu" "^3.11.0"
"@tiptap/extension-floating-menu" "^3.11.0"
"@trysound/sax@0.2.0":
version "0.2.0"
@@ -6253,6 +6267,11 @@
resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d"
integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==
"@zip.js/zip.js@^2.8.8":
version "2.8.10"
resolved "https://registry.yarnpkg.com/@zip.js/zip.js/-/zip.js-2.8.10.tgz#98a0cc7fdef9d6e227236271af412db02b18a5b2"
integrity sha512-WVywWK8HSttmFFYSih7lUjjaV4zGzMxy992y0tHrZY4Wf9x/uNBA/XJ50RvfGjuuJKti4yueEHA2ol2pOq6VDg==
abs-svg-path@^0.1.1:
version "0.1.1"
resolved "https://registry.yarnpkg.com/abs-svg-path/-/abs-svg-path-0.1.1.tgz#df601c8e8d2ba10d4a76d625e236a9a39c2723bf"
@@ -7665,7 +7684,7 @@ doctrine@^2.1.0:
dependencies:
esutils "^2.0.2"
docx@*, docx@9.5.0, docx@^9.0.2:
docx@*, docx@9.5.0, docx@^9.5.1:
version "9.5.0"
resolved "https://registry.yarnpkg.com/docx/-/docx-9.5.0.tgz#586990c4ecf1c7e83290529997b33f2c029bbe68"
integrity sha512-WZggg9vVujFcTyyzfIVBBIxlCk51QvhLWl87wtI2zuBdz8C8C0mpRhEVwA2DZd7dXyY0AVejcEVDT9vn7Xm9FA==
@@ -8450,7 +8469,7 @@ extend@^3.0.0:
resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"
integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==
fast-deep-equal@^3, fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
version "3.1.3"
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
@@ -9485,7 +9504,7 @@ ignore@^7.0.0, ignore@^7.0.5:
resolved "https://registry.yarnpkg.com/ignore/-/ignore-7.0.5.tgz#4cb5f6cd7d4c7ab0365738c7aea888baa6d7efd9"
integrity sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==
image-meta@^0.2.1:
image-meta@^0.2.2:
version "0.2.2"
resolved "https://registry.yarnpkg.com/image-meta/-/image-meta-0.2.2.tgz#a88dbdf1983d7c23a80c3e71d3b8acdb5379f5e0"
integrity sha512-3MOLanc3sb3LNGWQl1RlQlNWURE5g32aUphrDyFeCsxBTk08iE3VNe4CwsUZ0Qs1X+EfX0+r29Sxdpza4B+yRA==
@@ -12127,7 +12146,7 @@ prosemirror-menu@^1.2.4:
prosemirror-history "^1.0.0"
prosemirror-state "^1.0.0"
prosemirror-model@^1.0.0, prosemirror-model@^1.20.0, prosemirror-model@^1.21.0, prosemirror-model@^1.24.1, prosemirror-model@^1.25.0, prosemirror-model@^1.25.3:
prosemirror-model@1.25.4, prosemirror-model@^1.0.0, prosemirror-model@^1.20.0, prosemirror-model@^1.21.0, prosemirror-model@^1.24.1, prosemirror-model@^1.25.0, prosemirror-model@^1.25.4:
version "1.25.4"
resolved "https://registry.yarnpkg.com/prosemirror-model/-/prosemirror-model-1.25.4.tgz#8ebfbe29ecbee9e5e2e4048c4fe8e363fcd56e7c"
integrity sha512-PIM7E43PBxKce8OQeezAs9j4TP+5yDpZVbuurd1h5phUxEKIu+G2a+EUZzIC5nS1mJktDJWzbqS23n1tsAf5QA==
@@ -12150,16 +12169,16 @@ prosemirror-schema-list@^1.5.0:
prosemirror-state "^1.0.0"
prosemirror-transform "^1.7.3"
prosemirror-state@^1.0.0, prosemirror-state@^1.2.2, prosemirror-state@^1.4.3:
version "1.4.3"
resolved "https://registry.yarnpkg.com/prosemirror-state/-/prosemirror-state-1.4.3.tgz#94aecf3ffd54ec37e87aa7179d13508da181a080"
integrity sha512-goFKORVbvPuAQaXhpbemJFRKJ2aixr+AZMGiquiqKxaucC6hlpHNZHWgz5R7dS4roHiwq9vDctE//CZ++o0W1Q==
prosemirror-state@1.4.4, prosemirror-state@^1.0.0, prosemirror-state@^1.2.2, prosemirror-state@^1.4.3, prosemirror-state@^1.4.4:
version "1.4.4"
resolved "https://registry.yarnpkg.com/prosemirror-state/-/prosemirror-state-1.4.4.tgz#72b5e926f9e92dcee12b62a05fcc8a2de3bf5b39"
integrity sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==
dependencies:
prosemirror-model "^1.0.0"
prosemirror-transform "^1.0.0"
prosemirror-view "^1.27.0"
prosemirror-tables@^1.3.7, prosemirror-tables@^1.6.4:
prosemirror-tables@^1.6.4, prosemirror-tables@^1.8.1:
version "1.8.1"
resolved "https://registry.yarnpkg.com/prosemirror-tables/-/prosemirror-tables-1.8.1.tgz#896a234e3e18240b629b747a871369dae78c8a9a"
integrity sha512-DAgDoUYHCcc6tOGpLVPSU1k84kCUWTWnfWX3UDy2Delv4ryH0KqTD6RBI6k4yi9j9I8gl3j8MkPpRD/vWPZbug==
@@ -12178,14 +12197,14 @@ prosemirror-trailing-node@^3.0.0:
"@remirror/core-constants" "3.0.0"
escape-string-regexp "^4.0.0"
prosemirror-transform@^1.0.0, prosemirror-transform@^1.1.0, prosemirror-transform@^1.10.2, prosemirror-transform@^1.10.3, prosemirror-transform@^1.10.4, prosemirror-transform@^1.7.3:
prosemirror-transform@1.10.4, prosemirror-transform@^1.0.0, prosemirror-transform@^1.1.0, prosemirror-transform@^1.10.2, prosemirror-transform@^1.10.3, prosemirror-transform@^1.10.5, prosemirror-transform@^1.7.3:
version "1.10.4"
resolved "https://registry.yarnpkg.com/prosemirror-transform/-/prosemirror-transform-1.10.4.tgz#56419eac14f9f56612c806ae46f9238648f3f02e"
integrity sha512-pwDy22nAnGqNR1feOQKHxoFkkUtepoFAd3r2hbEDsnf4wp57kKA36hXsB3njA9FtONBEwSDnDeCiJe+ItD+ykw==
dependencies:
prosemirror-model "^1.21.0"
prosemirror-view@^1.0.0, prosemirror-view@^1.1.0, prosemirror-view@^1.27.0, prosemirror-view@^1.31.0, prosemirror-view@^1.38.1, prosemirror-view@^1.39.1, prosemirror-view@^1.41.2:
prosemirror-view@1.41.3, prosemirror-view@^1.0.0, prosemirror-view@^1.1.0, prosemirror-view@^1.27.0, prosemirror-view@^1.31.0, prosemirror-view@^1.38.1, prosemirror-view@^1.39.1, prosemirror-view@^1.41.3:
version "1.41.3"
resolved "https://registry.yarnpkg.com/prosemirror-view/-/prosemirror-view-1.41.3.tgz#753a37ebe172a3e313ad2c3d85496f9ed1b2c256"
integrity sha512-SqMiYMUQNNBP9kfPhLO8WXEk/fon47vc52FQsUiJzTBuyjKgEcoAwMyF04eQ4WZ2ArMn7+ReypYL60aKngbACQ==
@@ -12502,16 +12521,16 @@ react-dom@*, react-dom@19.2.0:
dependencies:
scheduler "^0.27.0"
react-i18next@16.2.3:
version "16.2.3"
resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-16.2.3.tgz#079e3c54c85334ce9ab9732be3553516a7b7b42d"
integrity sha512-O0t2zvmIz7nHWKNfIL+O/NTIbpTaOPY0vZov779hegbep3IZ+xcqkeVPKWBSXwzdkiv77q8zmq9toKIUys1x3A==
react-i18next@16.3.3:
version "16.3.3"
resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-16.3.3.tgz#098ff5443d0436a78692ca76303b2219aca32989"
integrity sha512-IaY2W+ueVd/fe7H6Wj2S4bTuLNChnajFUlZFfCTrTHWzGcOrUHlVzW55oXRSl+J51U8Onn6EvIhQ+Bar9FUcjw==
dependencies:
"@babel/runtime" "^7.27.6"
html-parse-stringify "^3.0.1"
use-sync-external-store "^1.6.0"
react-icons@^5.2.1:
react-icons@^5.5.0:
version "5.5.0"
resolved "https://registry.yarnpkg.com/react-icons/-/react-icons-5.5.0.tgz#8aa25d3543ff84231685d3331164c00299cdfaf2"
integrity sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==
+1
View File
@@ -0,0 +1 @@
helmfile.yaml
+2 -2
View File
@@ -1,10 +1,10 @@
environments:
dev:
values:
- version: 3.9.0
- version: 3.10.0
feature:
values:
- version: 3.9.0
- version: 3.10.0
feature: ci
domain: example.com
imageTag: demo
+1 -1
View File
@@ -1,5 +1,5 @@
apiVersion: v2
type: application
name: docs
version: 3.9.0
version: 3.10.0
appVersion: latest
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "mail_mjml",
"version": "3.9.0",
"version": "3.10.0",
"description": "An util to generate html and text django's templates from mjml templates",
"type": "module",
"dependencies": {